Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
40fc09b
feat(react): add node index metadata to snapshots
upupming Mar 28, 2026
7a3cfbe
chore: update snapshot
upupming Mar 30, 2026
00bc4a0
feat(react): gate snapshot node index collection
upupming Mar 30, 2026
ec30d15
chore(react): update plugin react api report
upupming Mar 30, 2026
5f222e2
feat: make enableNodeIndex optional
upupming Mar 30, 2026
9457561
refactor(react): rename node index metadata to ui source map
upupming Mar 31, 2026
c3e4572
docs(changeset): align react metadata wording
upupming Mar 31, 2026
a33b0a0
fix(react): inject ui source map filenames at transform boundary
upupming Mar 31, 2026
e68ceb8
fix(react): clamp ui source map ids to int32
upupming Apr 1, 2026
dd44597
fix(react): keep ui source map ids non-negative
upupming Apr 1, 2026
32e9a05
feat(rspeedy): emit node index map assets
upupming Mar 30, 2026
1c75343
fix(example): satisfy node index config types
upupming Mar 30, 2026
068808f
chore(api): update react webpack plugin report
upupming Mar 30, 2026
ff735c8
chore(api): update react rsbuild plugin report
upupming Mar 30, 2026
3d21e93
fix(webpack): clean node index intermediate assets
upupming Mar 30, 2026
20d3b62
fix(example): write nodeIndexMapUrl to source content config
upupming Mar 31, 2026
08dc4ef
refactor(webpack): align node index mappings with sourcemap
upupming Mar 31, 2026
9d889c3
docs(changeset): align ui source map wording
upupming Mar 31, 2026
2876cf9
refactor(react): rename node index asset surface to ui source map
upupming Mar 31, 2026
806163f
refactor(webpack): emit debug metadata assets
upupming Mar 31, 2026
5818cee
test(react): update ui source map snapshots
upupming Apr 1, 2026
2f07380
chore: move example node-index to ui-sourcemap
upupming Apr 13, 2026
5812900
Merge remote-tracking branch 'origin/main' into codex/node-index-asset
upupming Apr 13, 2026
48fae34
chore: update changeset
upupming Apr 13, 2026
b69867e
fix: test
upupming Apr 13, 2026
2da6869
fix: test
upupming Apr 13, 2026
f61fd42
Merge branch 'codex/node-index-asset' of github.com:lynx-family/lynx-…
upupming Apr 13, 2026
8f5182a
Merge remote-tracking branch 'origin/main' into codex/node-index-asset
upupming Apr 13, 2026
cf55e95
fix: cr of codex
upupming Apr 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/debug-metadata.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lynx-js/template-webpack-plugin': patch
---

Introduce `LynxDebugMetadataPlugin` to emit debug-metadata assets.
5 changes: 5 additions & 0 deletions .changeset/soft-pumas-push.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lynx-js/react": patch
---

Add `nodeIndex` to generated FiberElement creation calls and expose React transform debug metadata as `uiSourceMapRecords`.
6 changes: 6 additions & 0 deletions .changeset/ten-lions-accept.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@lynx-js/react-rsbuild-plugin': patch
'@lynx-js/react-webpack-plugin': patch
---

Add `enableUiSourceMap` option to enable UI source map generation and debug-metadata asset emission.
Comment thread
upupming marked this conversation as resolved.
5 changes: 5 additions & 0 deletions .github/debug-metadata.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
applyTo: "packages/webpack/{react-webpack-plugin,template-webpack-plugin}/**/*"
---

Treat `debug-metadata.json` as the final unified debug asset, not as an early intermediate dump. Generate it only after every JS sourcemap that will be shipped or uploaded has already been finalized, including main-thread debug-info remapping and any late `processAssets` code transforms. When one template contains multiple runtimes, store JS sourcemaps as a `jsSourceMaps` collection rather than a single top-level map, and keep `sourceMapRelease` attached to each JS asset entry instead of the document root because Slardar matches sourcemaps per emitted JS file. Keep `uiSourceMap` and bytecode debug payloads as sibling debug documents inside the unified container instead of overloading the standard sourcemap top-level shape.
10 changes: 10 additions & 0 deletions .github/react-transform.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
applyTo: "packages/react/transform/**/*"
---

When a crate exposes both core Rust structs and `napi` wrapper structs with the same semantic shape, keep internal transform pipelines and shared `Rc<RefCell<...>>` state on the core types and convert to the `napi` types only at the JS boundary. Do not mix `swc_plugin_*::napi::*` record types into internal plugin wiring such as `.with_*_records(...)`, or wasm builds can fail with mismatched type errors.
When recording source locations from SWC spans, guard `SourceMap::lookup_char_pos` for synthetic spans such as `DUMMY_SP` (`span.lo == 0`). Compat and other transforms may synthesize JSX nodes with default spans, and wasm builds can surface panics from source map lookups on those spans as `RuntimeError: unreachable`.
Expose recorded columns as 1-based values so `uiSourceMapRecords` can be fed directly into editor locations such as VS Code without an extra offset conversion.
When compat wraps a component with a synthetic `<view>`, preserve the original component spans on the generated wrapper instead of using `DUMMY_SP` or `Default::default()`. Snapshot ui source map extraction reads `opening.span`, so preserved spans keep `uiSourceMapRecords` file, line, and column data intact.
Keep `snapshot.filename` stable for snapshot hashing semantics, even when callers want absolute paths in exported debug metadata. If `uiSourceMapRecords.filename` needs to use the top-level transform filename, inject it at the `react/transform/src/lib.rs` boundary instead of changing the snapshot plugin's internal filename.
If `swc_plugin_snapshot::JSXTransformer::new` gains a new constructor parameter, update every external callsite under `packages/react/transform/**` at the same time, including wrapper crates such as `swc-plugin-reactlynx`, not just the main `packages/react/transform/src/lib.rs` entrypoint.
5 changes: 5 additions & 0 deletions .github/rspeedy-core.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
applyTo: "packages/rspeedy/core/test/**/*"
---

Some rspeedy core test fixtures intentionally keep git-tracked files under fixture `node_modules` directories. When cleaning caches or build outputs, avoid deleting tracked fixture files under `packages/rspeedy/core/test/**/node_modules`; only remove untracked/generated artifacts.
8 changes: 8 additions & 0 deletions .github/webpack-node-index.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
applyTo: "packages/webpack/{react-webpack-plugin,template-webpack-plugin}/**/*"
---

When emitting React UI source map metadata during template generation, emit `debug-metadata.json` into the template plugin `intermediate` directory, not beside `template.js`. The file should keep the sourcemap payload under a top-level `uiSourceMap` field and place auxiliary data such as `templateDebug` and `git` under `meta`, instead of serializing raw `uiSourceMapRecords`.
Keep UI source map generation opt-in behind `pluginReactLynx({ enableUiSourceMap: true })`. When the flag is off, do not collect `uiSourceMapRecords`, do not emit `debug-metadata.json`, and do not inject `debugMetadataUrl` into encode data.
Collect `uiSourceMapRecords` from main-thread loader results by storing them on module `buildInfo`, then aggregate them per template entry group before emit. The emitted `uiSourceMap.sources` array should use project-root-relative POSIX paths, `uiSourceMap.mappings` should follow sourcemap-style source locations as `[sourceIndex, line, column]`, and `uiSourceMap.uiMaps` should be a parallel array where `uiMaps[i]` is the runtime `nodeIndex` for `mappings[i]`. Keep the emitted line and column values 0-based even if transform-time records are editor-friendly 1-based.
If a webpack plugin emits extra intermediate assets during `beforeEncode` such as `debug-metadata.json`, register their asset names on `args.intermediateAssets` so `LynxEncodePlugin` / `WebEncodePlugin` can clean them with the rest of the intermediate encode artifacts after template generation.
214 changes: 214 additions & 0 deletions examples/react-ui-sourcemap/lynx.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import { execFileSync } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

import { pluginQRCode } from '@lynx-js/qrcode-rsbuild-plugin';
import { pluginReactLynx } from '@lynx-js/react-rsbuild-plugin';
import { defineConfig } from '@lynx-js/rspeedy';
import type { RsbuildPlugin, Rspack } from '@lynx-js/rspeedy';
import type { LynxTemplatePlugin } from '@lynx-js/template-webpack-plugin';

const DEBUG_METADATA_ASSET = 'debug-metadata.json';
const MOCK_UPLOAD_BASE_URL = 'https://mock-debug-metadata-upload.lynx.dev/';
const projectRoot = path.dirname(fileURLToPath(import.meta.url));

interface GitMetadata {
branch: string;
commit: string;
commitUrl: string | null;
remoteUrl: string | null;
}

function runGit(args: string[]): string | null {
try {
return execFileSync('git', args, {
cwd: projectRoot,
encoding: 'utf8',
}).trim();
} catch {
return null;
}
}

function normalizeRepositoryUrl(remoteUrl: string | null): string | null {
if (!remoteUrl) {
return null;
}

if (remoteUrl.startsWith('git@github.com:')) {
return `https://github.com/${
remoteUrl.slice('git@github.com:'.length).replace(/\.git$/, '')
}`;
}

if (remoteUrl.startsWith('https://github.com/')) {
return remoteUrl.replace(/\.git$/, '');
}

return remoteUrl;
}

function getGitMetadata(): GitMetadata {
const commit = runGit(['rev-parse', 'HEAD']) ?? 'unknown';
const branch = runGit(['rev-parse', '--abbrev-ref', 'HEAD']) ?? 'unknown';
const remoteUrl = normalizeRepositoryUrl(
runGit(['config', '--get', 'remote.origin.url']),
);

return {
branch,
commit,
remoteUrl,
commitUrl: remoteUrl ? `${remoteUrl}/commit/${commit}` : null,
};
}

function mockUploadDebugMetadata(
filenameTemplate: string,
intermediate: string,
): string {
const normalizedTemplate = filenameTemplate.replaceAll(
path.win32.sep,
path.posix.sep,
);
const normalizedIntermediate = intermediate.replaceAll(
path.win32.sep,
path.posix.sep,
);
const assetPath = path.posix.join(
normalizedIntermediate.replace(/^\.\//, ''),
DEBUG_METADATA_ASSET,
);

return new URL(
`${assetPath}?template=${encodeURIComponent(normalizedTemplate)}`,
MOCK_UPLOAD_BASE_URL,
).toString();
}

function pluginMockDebugMetadataUpload(): RsbuildPlugin {
return {
name: 'example:mock-debug-metadata-upload',
setup(api) {
const git = getGitMetadata();

api.modifyBundlerChain(chain => {
const exposed = api.useExposed<
{ LynxTemplatePlugin: typeof LynxTemplatePlugin }
>(Symbol.for('LynxTemplatePlugin'));

if (!exposed) {
throw new Error(
'[example:mock-debug-metadata-upload] Missing exposed LynxTemplatePlugin',
);
}

chain.plugin('example:mock-debug-metadata-upload').use({
apply(compiler) {
compiler.hooks.thisCompilation.tap(
'example:mock-debug-metadata-upload',
compilation => {
const hooks = exposed.LynxTemplatePlugin
.getLynxTemplatePluginHooks(
compilation as unknown as Parameters<
typeof LynxTemplatePlugin.getLynxTemplatePluginHooks
>[0],
);

hooks.beforeEncode.tapPromise(
{
name: 'example:mock-debug-metadata-upload',
stage: 1000,
},
async args => {
const assetName = path.posix.format({
dir: args.intermediate,
base: DEBUG_METADATA_ASSET,
});
const debugMetadataAsset = compilation.getAsset(assetName);

if (debugMetadataAsset) {
const currentContent = debugMetadataAsset.source
.source()
.toString();
const debugMetadata = JSON.parse(
currentContent,
) as Record<
string,
unknown
>;
const currentMeta =
typeof debugMetadata['meta'] === 'object'
&& debugMetadata['meta'] !== null
? debugMetadata['meta'] as Record<string, unknown>
: {};

compilation.updateAsset(
assetName,
new compiler.webpack.sources.RawSource(
JSON.stringify(
{
...debugMetadata,
meta: {
...currentMeta,
git,
},
},
null,
2,
),
),
);
}

const debugMetadataUrl = await Promise.resolve(
mockUploadDebugMetadata(
args.filenameTemplate,
args.intermediate,
),
);

args.encodeData.sourceContent.config = {
...args.encodeData.sourceContent.config,
debugMetadataUrl,
};

return args;
},
);
},
);
},
} as Rspack.RspackPluginInstance);
});
},
};
}

export default defineConfig({
source: {
entry: {
main: path.join(projectRoot, 'src/index.tsx'),
},
},
output: {
distPath: {
root: path.join(projectRoot, 'dist'),
},
},
plugins: [
pluginReactLynx({
enableUiSourceMap: true,
}),
pluginMockDebugMetadataUpload(),
pluginQRCode({
schema(url) {
return `${url}?fullscreen=true`;
},
}),
],
environments: {
web: {},
lynx: {},
},
});
22 changes: 22 additions & 0 deletions examples/react-ui-sourcemap/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "@lynx-js/example-react-ui-sourcemap",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "rspeedy build",
"dev": "rspeedy dev"
},
"dependencies": {
"@lynx-js/react": "workspace:*"
},
"devDependencies": {
"@lynx-js/preact-devtools": "^5.0.1",
"@lynx-js/qrcode-rsbuild-plugin": "workspace:*",
"@lynx-js/react-rsbuild-plugin": "workspace:*",
"@lynx-js/rspeedy": "workspace:*",
"@lynx-js/template-webpack-plugin": "workspace:*",
"@lynx-js/types": "3.7.0",
"@types/react": "^18.3.28"
}
}
66 changes: 66 additions & 0 deletions examples/react-ui-sourcemap/src/App.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
.Screen {
min-height: 100%;
padding: 32px 24px;
background: linear-gradient(180deg, #0c1222 0%, #172544 100%);
}

.Hero {
margin-bottom: 24px;
}

.Eyebrow {
margin-bottom: 8px;
color: #89b4ff;
font-size: 24px;
}

.Title {
margin-bottom: 12px;
color: #ffffff;
font-size: 42px;
font-weight: 700;
}

.Description {
color: rgba(255, 255, 255, 0.82);
font-size: 26px;
line-height: 36px;
}

.Button {
margin-bottom: 24px;
padding: 20px 24px;
border-radius: 999px;
background-color: #7ee787;
}

.ButtonLabel {
color: #08210d;
font-size: 24px;
font-weight: 600;
}

.Hint {
color: rgba(255, 255, 255, 0.7);
font-size: 24px;
line-height: 34px;
}

.Card {
padding: 24px;
border-radius: 24px;
background-color: rgba(255, 255, 255, 0.12);
}

.CardTitle {
margin-bottom: 12px;
color: #ffffff;
font-size: 30px;
font-weight: 600;
}

.CardBody {
color: rgba(255, 255, 255, 0.78);
font-size: 24px;
line-height: 34px;
}
Loading
Loading