@sigx/lynx-plugin 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/layers.js ADDED
@@ -0,0 +1,5 @@
1
+ /** Webpack module layers used to separate the dual-thread bundles. */
2
+ export const LAYERS = {
3
+ BACKGROUND: 'sigx:background',
4
+ MAIN_THREAD: 'sigx:main-thread',
5
+ };
@@ -1,20 +1,71 @@
1
- //#region src/loaders/hmr-loader.ts
2
- var e = /\bcomponent\s*[<(]/, t = /import\s*\{([^}]*)\bsignal\b([^}]*)\}\s*from\s*(['"])@sigx\/lynx\3/;
3
- function n(n) {
4
- if (this.cacheable(!1), !e.test(n)) return n;
5
- let r = this.resourcePath.replace(/\\/g, "/").replace(/\.(tsx?|jsx)$/, ""), i = !!n.match(t), a = n;
6
- i && (a = n.replace(t, (e, t, n, r) => `import {${t}signal as __origSignal${n}} from ${r}@sigx/lynx${r}`));
7
- let o = [
8
- "import { __registerComponentPlugin, __setCurrentInstanceForHMR } from '@sigx/lynx';",
9
- "import { initHMR, registerHMRModule } from '@sigx/lynx-runtime/hmr';",
10
- "initHMR(__registerComponentPlugin, __setCurrentInstanceForHMR);",
11
- `registerHMRModule(${JSON.stringify(r)});`
12
- ];
13
- i && o.push("var __hmrSigPrev = (typeof module !== 'undefined' && module.hot && module.hot.data && module.hot.data.__hmrSigs) || {};", "var __hmrSigStore = {};", "var __hmrSigIdx = 0;", "function signal() {", " var k = 's' + __hmrSigIdx++;", " if (k in __hmrSigPrev) { __hmrSigStore[k] = __hmrSigPrev[k]; return __hmrSigPrev[k]; }", " var s = __origSignal.apply(null, arguments);", " __hmrSigStore[k] = s;", " return s;", "}"), o.push("");
14
- let s = [""];
15
- return i ? s.push("if (typeof module !== 'undefined' && module.hot) {", " module.hot.dispose(function(data) { data.__hmrSigs = __hmrSigStore; });", " module.hot.accept();", "}") : s.push("if (typeof module !== 'undefined' && module.hot) { module.hot.accept(); }"), o.join("\n") + a + s.join("\n");
1
+ /**
2
+ * Rspack loader that injects HMR self-acceptance into component files.
3
+ *
4
+ * For every file that contains a `component(` or `component<` call, the
5
+ * loader prepends:
6
+ * 1. An import of `__registerComponentPlugin` from `@sigx/lynx` — this
7
+ * is the *same* bundle the app uses, so the plugin array is shared.
8
+ * 2. An import of `initHMR` + `registerHMRModule` from `@sigx/lynx-runtime/hmr`.
9
+ * 3. Calls to wire them up: `initHMR(__registerComponentPlugin)` (idempotent)
10
+ * and `registerHMRModule(moduleId)`.
11
+ * 4. (If `signal` is imported) A wrapper that preserves signal objects
12
+ * across HMR cycles via `module.hot.data`, so module-level reactive
13
+ * state (e.g., a route signal) doesn't reset and cause structural tree
14
+ * mutations that crash the Lynx native engine.
15
+ *
16
+ * And appends `module.hot.accept()` + a dispose handler that snapshots
17
+ * preserved signals into `module.hot.data` for the next execution.
18
+ *
19
+ * On re-execution the HMR runtime patches existing component instances
20
+ * in-place — only property-level ops are emitted, avoiding the structural
21
+ * tree mutations that crash Lynx's native engine.
22
+ */
23
+ // Matches `component(` or `component<`
24
+ const COMPONENT_RE = /\bcomponent\s*[<(]/;
25
+ // Matches an import statement from @sigx/lynx that destructures `signal`.
26
+ // Captures: (1) before-signal names, (2) after-signal names, (3) quote char
27
+ // e.g. import { signal, component } from '@sigx/lynx';
28
+ // group 1: "" group 2: ", component " group 3: "'"
29
+ const SIGNAL_IMPORT_RE = /import\s*\{([^}]*)\bsignal\b([^}]*)\}\s*from\s*(['"])@sigx\/lynx\3/;
30
+ export default function hmrLoader(source) {
31
+ this.cacheable(false);
32
+ if (!COMPONENT_RE.test(source)) {
33
+ return source;
34
+ }
35
+ const moduleId = this.resourcePath
36
+ .replace(/\\/g, '/')
37
+ .replace(/\.(tsx?|jsx)$/, '');
38
+ // Check if the file imports `signal` from @sigx/lynx
39
+ const signalImportMatch = source.match(SIGNAL_IMPORT_RE);
40
+ const hasSignalImport = !!signalImportMatch;
41
+ let transformedSource = source;
42
+ if (hasSignalImport) {
43
+ // Rename `signal` → `__origSignal` in the import statement so we can
44
+ // shadow it with a preserving wrapper. All call-sites automatically
45
+ // use the wrapper because the local binding name stays `signal`.
46
+ transformedSource = source.replace(SIGNAL_IMPORT_RE, (_match, before, after, quote) => `import {${before}signal as __origSignal${after}} from ${quote}@sigx/lynx${quote}`);
47
+ }
48
+ const header = [
49
+ `import { __registerComponentPlugin, __setCurrentInstanceForHMR } from '@sigx/lynx';`,
50
+ `import { initHMR, registerHMRModule } from '@sigx/lynx-runtime/hmr';`,
51
+ `initHMR(__registerComponentPlugin, __setCurrentInstanceForHMR);`,
52
+ `registerHMRModule(${JSON.stringify(moduleId)});`,
53
+ ];
54
+ if (hasSignalImport) {
55
+ // Inject a `signal` wrapper that returns the cached signal object from
56
+ // the previous HMR cycle when one exists, or delegates to the real
57
+ // `signal()` for new signals. This preserves module-level reactive
58
+ // state (and component-level state inside setup — matching React Fast
59
+ // Refresh behaviour where hooks state is preserved across HMR).
60
+ header.push(`var __hmrSigPrev = (typeof module !== 'undefined' && module.hot && module.hot.data && module.hot.data.__hmrSigs) || {};`, `var __hmrSigStore = {};`, `var __hmrSigIdx = 0;`, `function signal() {`, ` var k = 's' + __hmrSigIdx++;`, ` if (k in __hmrSigPrev) { __hmrSigStore[k] = __hmrSigPrev[k]; return __hmrSigPrev[k]; }`, ` var s = __origSignal.apply(null, arguments);`, ` __hmrSigStore[k] = s;`, ` return s;`, `}`);
61
+ }
62
+ header.push('');
63
+ const footer = [''];
64
+ if (hasSignalImport) {
65
+ footer.push(`if (typeof module !== 'undefined' && module.hot) {`, ` module.hot.dispose(function(data) { data.__hmrSigs = __hmrSigStore; });`, ` module.hot.accept();`, `}`);
66
+ }
67
+ else {
68
+ footer.push(`if (typeof module !== 'undefined' && module.hot) { module.hot.accept(); }`);
69
+ }
70
+ return header.join('\n') + transformedSource + footer.join('\n');
16
71
  }
17
- //#endregion
18
- export { n as default };
19
-
20
- //# sourceMappingURL=hmr-loader.js.map
@@ -1,8 +1,17 @@
1
- //#region src/loaders/ignore-css-loader.ts
2
- function e(e) {
3
- return this.cacheable(!0), e.includes("___CSS_LOADER_EXPORT___") ? "export {}" : e;
1
+ /**
2
+ * Webpack/Rspack loader that strips CSS from Main-Thread bundles.
3
+ *
4
+ * In the Lynx dual-thread model, only the Background Thread needs CSS.
5
+ * The Main-Thread layer uses `exportOnlyLocals` from css-loader to get
6
+ * CSS module class name mappings without actual styles.
7
+ */
8
+ export default function ignoreCssLoader(source) {
9
+ this.cacheable(true);
10
+ // If the source contains ___CSS_LOADER_EXPORT___, it is not a CSS Modules
11
+ // file (exportOnlyLocals is enabled), so we don't need to preserve it.
12
+ if (source.includes('___CSS_LOADER_EXPORT___')) {
13
+ return 'export {}';
14
+ }
15
+ // Preserve CSS modules export for background layer.
16
+ return source;
4
17
  }
5
- //#endregion
6
- export { e as default };
7
-
8
- //# sourceMappingURL=ignore-css-loader.js.map
@@ -1,64 +1,144 @@
1
- import { dirname as e, join as t } from "node:path";
2
- import { createRequire as n } from "node:module";
3
- import { transformReactLynxSync as r } from "@lynx-js/react/transform";
4
- //#region src/loaders/worklet-utils.ts
5
- function i(e) {
6
- let t = e.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/[^\n]*/g, "$1"), n = /* @__PURE__ */ new Set(), r = /(?:from|import)\s+['"]((?:\.[^'"]+)|(?:@sigx\/[^'"]+))['"]/g, i;
7
- for (; (i = r.exec(t)) !== null;) n.add(i[1]);
8
- return n.size === 0 ? "" : [...n].map((e) => `import '${e}';`).join("\n");
1
+ /**
2
+ * MT-layer rspack loader for the `'main thread'` worklet directive.
3
+ *
4
+ * Runs on every JS/TS file in the MT layer — no rule-level exclude.
5
+ * Decides per-file what to emit based on directive presence and whether
6
+ * the file is library code (`node_modules/` or `dist/`) or user code:
7
+ *
8
+ * | Origin | Directive? | Output |
9
+ * |--------------|------------|------------------------------------------------|
10
+ * | user code | no | bootstrap preamble + side-effect imports |
11
+ * | | | (drops body — sigx's Lepus never executes |
12
+ * | | | React components; `entry-main.ts`'s |
13
+ * | | | `renderPage` builds a placeholder and ops |
14
+ * | | | arrive from BG via `sigxPatchUpdate`) |
15
+ * | user code | yes | bootstrap preamble + side-effect imports |
16
+ * | | | + extracted `registerWorkletInternal(...)` |
17
+ * | library | no | source unchanged |
18
+ * | | | (preserves `@sigx/lynx-runtime-main`'s MT |
19
+ * | | | globals — `processData`, `updateGlobalProps`,|
20
+ * | | | `sigxRunOnMT` — and barrel re-exports that |
21
+ * | | | BG-side consumers like daisyui import by |
22
+ * | | | name; rspack shares module identity across |
23
+ * | | | BG/MT layers) |
24
+ * | library | yes | source unchanged + appended registrations |
25
+ * | | | (keeps named exports AND registers worklets) |
26
+ *
27
+ * Library files skip the bootstrap preamble — the user entry's
28
+ * preamble has already pulled runtime-main in before any library code
29
+ * evaluates.
30
+ *
31
+ * For files WITH a directive, the LEPUS transform's output is sliced
32
+ * via `extractRegistrations` so only the `registerWorkletInternal(...)`
33
+ * calls are kept. The `loadWorkletRuntime` import that upstream emits
34
+ * is dropped — `registerWorkletInternal` is installed as a global by
35
+ * `entry-main.ts`.
36
+ *
37
+ * Mirrors vue-lynx's `worklet-loader-mt.ts`, minus the `?vue` sub-module
38
+ * branch (sigx has no Vue SFC pipeline) and minus the shared-imports path
39
+ * (deferred to Phase 1c).
40
+ */
41
+ import { transformReactLynxSync } from '@lynx-js/react/transform';
42
+ import { createRequire } from 'node:module';
43
+ import { dirname, join } from 'node:path';
44
+ import { extractLocalImports, extractRegistrations } from './worklet-utils.js';
45
+ // Same idiomatic-barrel matching as the BG loader (see worklet-loader.ts:23
46
+ // for the full rationale). The LEPUS pass emits an `import { loadWorkletRuntime
47
+ // } from '<runtimePkg>'`, but `extractRegistrations` slices the LEPUS output
48
+ // down to `registerWorkletInternal(...)` calls only — the import is dropped
49
+ // before bundling — so the runtimePkg's actual exports don't need to include
50
+ // loadWorkletRuntime. What matters here is identifier tracking: aligning
51
+ // with '@sigx/lynx' means SWC follows `runOnBackground as s` through
52
+ // '@sigx/lynx' barrel imports just like on the BG side.
53
+ const RUNTIME_PKG = '@sigx/lynx';
54
+ /**
55
+ * Prepended to every MT-layer file so webpack's dep graph guarantees the
56
+ * bootstrap modules evaluate BEFORE any user code that calls
57
+ * `registerWorkletInternal(...)`. Listing these as separate entries in
58
+ * `entry.ts` doesn't suffice — rspack walks the chunk graph, which can
59
+ * eval user code first when nothing else explicitly depends on bootstrap.
60
+ *
61
+ * Order matters: entry-main's module body sets globalThis.SystemInfo before
62
+ * worklet-runtime's IIFE reads it.
63
+ *
64
+ * Paths are resolved to absolute file URIs at loader-init time so user apps
65
+ * don't need to declare @lynx-js/react as a direct dep — pnpm hoists it into
66
+ * our package's node_modules and we hand rspack the resolved path. We resolve
67
+ * the package's `package.json` (always reachable, no exports-map games), read
68
+ * the dist subpath relative to it. createRequire(import.meta.url) lets us
69
+ * walk up from the loader's install location via Node's CJS resolver.
70
+ */
71
+ const _req = createRequire(import.meta.url);
72
+ function resolvePackageSubpath(pkgName, subpath) {
73
+ const pkgJsonPath = _req.resolve(`${pkgName}/package.json`);
74
+ return join(dirname(pkgJsonPath), subpath);
9
75
  }
10
- function a(e) {
11
- let t = [], n = 0;
12
- for (;;) {
13
- let r = e.indexOf("registerWorkletInternal(", n);
14
- if (r === -1) break;
15
- let i = 0, a = r + 24 - 1;
16
- for (; a < e.length; a++) {
17
- let t = e[a];
18
- if (t === "(") i++;
19
- else if (t === ")" && (i--, i === 0)) break;
20
- }
21
- let o = a + 1;
22
- o < e.length && e[o] === ";" && o++, t.push(e.slice(r, o)), n = o;
23
- }
24
- return t.join("\n");
76
+ const ENTRY_MAIN_PATH = resolvePackageSubpath('@sigx/lynx-runtime-main', 'dist/entry-main.js');
77
+ const WORKLET_RUNTIME_PATH = resolvePackageSubpath('@lynx-js/react', 'runtime/worklet-runtime/main.js');
78
+ const INSTALL_HYBRID_PATH = resolvePackageSubpath('@sigx/lynx-runtime-main', 'dist/install-hybrid-worklet.js');
79
+ const BOOTSTRAP_PREAMBLE = `import ${JSON.stringify(ENTRY_MAIN_PATH)};\n`
80
+ + `import ${JSON.stringify(WORKLET_RUNTIME_PATH)};\n`
81
+ + `import ${JSON.stringify(INSTALL_HYBRID_PATH)};\n`;
82
+ // Cheap pre-filter to skip the SWC parse for files that obviously don't
83
+ // contain a worklet directive. A directive is always followed immediately
84
+ // by a statement terminator — either `;` or a newline (ASI). Requiring
85
+ // one of those after the closing quote rejects substrings inside
86
+ // single-line error strings like
87
+ // `"...inside 'main thread' functions..."` from `@sigx/lynx-runtime`'s
88
+ // dist, where the next char is a space then `functions`.
89
+ //
90
+ // This is not a parser. A truly adversarial input (e.g. the literal
91
+ // string `"'main thread';"`) can slip through, and SWC is the final
92
+ // arbiter — for such files it produces no registrations and the MT
93
+ // output reduces to the library branch's source pass-through (for
94
+ // library paths) or to the no-directive user branch's strip (for user
95
+ // paths). Either way: correct, just with the parse work done.
96
+ const DIRECTIVE_RE = /['"]main thread['"]\s*(?:;|\n)/;
97
+ // Library paths (`node_modules/` and any `dist/`) get the body-preserve
98
+ // branches above. Rspack shares module identity across BG/MT layers, so
99
+ // stripping the MT-side body of a library file would wipe its named
100
+ // exports for BG consumers too — daisyui couldn't resolve `useTabs` from
101
+ // lynx-navigation, runtime-main's MT globals would disappear, etc.
102
+ const LIBRARY_PATH_RE = /[\\/](?:node_modules|dist)[\\/]/;
103
+ export default function workletLoaderMT(source) {
104
+ this.cacheable(true);
105
+ const localImports = extractLocalImports(source);
106
+ if (!DIRECTIVE_RE.test(source)) {
107
+ if (LIBRARY_PATH_RE.test(this.resourcePath)) {
108
+ return source;
109
+ }
110
+ return BOOTSTRAP_PREAMBLE + localImports;
111
+ }
112
+ const filename = this.resourcePath;
113
+ const result = transformReactLynxSync(source, {
114
+ pluginName: 'sigx:worklet-mt',
115
+ filename,
116
+ sourcemap: false,
117
+ cssScope: false,
118
+ shake: false,
119
+ compat: false,
120
+ refresh: false,
121
+ defineDCE: false,
122
+ directiveDCE: false,
123
+ snapshot: false,
124
+ worklet: { target: 'LEPUS', filename, runtimePkg: RUNTIME_PKG },
125
+ });
126
+ if (result.errors && result.errors.length > 0) {
127
+ for (const err of result.errors) {
128
+ this.emitError(new Error(`[sigx-worklet-mt] LEPUS transform: ${err.text}`));
129
+ }
130
+ return localImports;
131
+ }
132
+ const registrations = extractRegistrations(result.code);
133
+ // Library files with a directive (e.g. lynx-navigation's `EdgeBackHandle.js`,
134
+ // `navigator/core.js`): preserve the original source so its named exports
135
+ // survive cross-layer module identity (BG-side consumers like daisyui
136
+ // import them by name), and append the `registerWorkletInternal` calls so
137
+ // the directives are still registered on the MT side. No bootstrap
138
+ // preamble — user entry code's preamble has already pulled runtime-main
139
+ // in before any library code is evaluated.
140
+ if (LIBRARY_PATH_RE.test(this.resourcePath)) {
141
+ return registrations ? `${source}\n${registrations}` : source;
142
+ }
143
+ return BOOTSTRAP_PREAMBLE + [localImports, registrations].filter(Boolean).join('\n');
25
144
  }
26
- //#endregion
27
- //#region src/loaders/worklet-loader-mt.ts
28
- var o = "@sigx/lynx-runtime-main", s = n(import.meta.url);
29
- function c(n, r) {
30
- return t(e(s.resolve(`${n}/package.json`)), r);
31
- }
32
- var l = c("@sigx/lynx-runtime-main", "dist/entry-main.js"), u = c("@lynx-js/react", "runtime/worklet-runtime/main.js"), d = c("@sigx/lynx-runtime-main", "dist/install-hybrid-worklet.js"), f = `import ${JSON.stringify(l)};\nimport ${JSON.stringify(u)};\nimport ${JSON.stringify(d)};\n`, p = /['"]main thread['"]\s*(?:;|\n)/, m = /[\\/](?:node_modules|dist)[\\/]/;
33
- function h(e) {
34
- this.cacheable(!0);
35
- let t = i(e);
36
- if (!p.test(e)) return m.test(this.resourcePath) ? e : f + t;
37
- let n = this.resourcePath, s = r(e, {
38
- pluginName: "sigx:worklet-mt",
39
- filename: n,
40
- sourcemap: !1,
41
- cssScope: !1,
42
- shake: !1,
43
- compat: !1,
44
- refresh: !1,
45
- defineDCE: !1,
46
- directiveDCE: !1,
47
- snapshot: !1,
48
- worklet: {
49
- target: "LEPUS",
50
- filename: n,
51
- runtimePkg: o
52
- }
53
- });
54
- if (s.errors && s.errors.length > 0) {
55
- for (let e of s.errors) this.emitError(/* @__PURE__ */ Error(`[sigx-worklet-mt] LEPUS transform: ${e.text}`));
56
- return t;
57
- }
58
- let c = a(s.code);
59
- return m.test(this.resourcePath) ? c ? `${e}\n${c}` : e : f + [t, c].filter(Boolean).join("\n");
60
- }
61
- //#endregion
62
- export { h as default };
63
-
64
- //# sourceMappingURL=worklet-loader-mt.js.map
@@ -1,32 +1,70 @@
1
- import { transformReactLynxSync as e } from "@lynx-js/react/transform";
2
- //#region src/loaders/worklet-loader.ts
3
- var t = "@sigx/lynx-runtime", n = /['"]main thread['"]\s*(?:;|\n)/;
4
- function r(r) {
5
- if (this.cacheable(!0), !n.test(r)) return r;
6
- let i = this.resourcePath, a = e(r, {
7
- pluginName: "sigx:worklet",
8
- filename: i,
9
- sourcemap: !1,
10
- cssScope: !1,
11
- shake: !1,
12
- compat: !1,
13
- refresh: !1,
14
- defineDCE: !1,
15
- directiveDCE: !1,
16
- snapshot: !1,
17
- worklet: {
18
- target: "JS",
19
- filename: i,
20
- runtimePkg: t
21
- }
22
- });
23
- if (a.errors && a.errors.length > 0) {
24
- for (let e of a.errors) this.emitError(/* @__PURE__ */ Error(`[sigx-worklet] JS transform: ${e.text}`));
25
- return r;
26
- }
27
- return a.code;
1
+ /**
2
+ * BG-layer rspack loader for the `'main thread'` worklet directive.
3
+ *
4
+ * Delegates to upstream's `transformReactLynxSync` (a framework-agnostic SWC
5
+ * worklet transform shared with vue-lynx and react-lynx — see
6
+ * docs/mts-upstream-spike.md). With `target: 'JS'`, each `'main thread'`-marked
7
+ * function is replaced by a `{ _wkltId, _c? }` placeholder object that the
8
+ * sigx BG runtime ships through SET_WORKLET_EVENT to MT.
9
+ *
10
+ * Files without the directive are passed through unchanged. The MT side is
11
+ * handled by the sibling `worklet-loader-mt.ts`.
12
+ */
13
+ import { transformReactLynxSync } from '@lynx-js/react/transform';
14
+ // runtimePkg controls where SWC emits `import { transformToWorklet } from <X>`
15
+ // at the top of transformed BG output (used when `runOnBackground(fn)` is
16
+ // detected inside a `'main thread'` body). Points at @sigx/lynx — the public
17
+ // barrel that re-exports everything from @sigx/lynx-runtime — so the emitted
18
+ // import matches the idiomatic path apps use, with no need for the internal
19
+ // lynx-runtime specifier to be reachable from every consumer.
20
+ //
21
+ // SWC's identifier matching for `runOnBackground` itself is by literal symbol
22
+ // name (see lynx-stack/packages/react/transform/.../extract_ident.rs's
23
+ // `n.callee.sym != "runOnBackground"` check) — it does NOT follow renamed
24
+ // import bindings. Packages that ship precompiled worklet code must therefore
25
+ // preserve the `runOnBackground` identifier in their dist (no mangling). The
26
+ // canonical pattern is the one upstream `@lynx-js/react` uses: `tsc`-only
27
+ // per-file emit, no bundler, no minifier. `@sigx/lynx-gestures` follows that
28
+ // pattern for the same reason — see its package.json `build` script.
29
+ const RUNTIME_PKG = '@sigx/lynx';
30
+ // Cheap pre-filter to skip the SWC parse for files that obviously don't
31
+ // contain a worklet directive. A directive is always followed immediately
32
+ // by a statement terminator — either `;` or a newline (ASI). Requiring
33
+ // one of those after the closing quote rejects substrings inside
34
+ // single-line error strings like
35
+ // `"...inside 'main thread' functions..."` from `@sigx/lynx-runtime`'s
36
+ // dist, where the next char is a space then `functions`.
37
+ //
38
+ // This is not a parser. A truly adversarial input (e.g. the literal
39
+ // string `"'main thread';"`) can slip through, and the SWC transform
40
+ // is the final arbiter — for such files it produces no registrations
41
+ // and the BG output is identical to the no-directive path, just with
42
+ // the parse work done.
43
+ const DIRECTIVE_RE = /['"]main thread['"]\s*(?:;|\n)/;
44
+ export default function workletLoader(source) {
45
+ this.cacheable(true);
46
+ if (!DIRECTIVE_RE.test(source)) {
47
+ return source;
48
+ }
49
+ const filename = this.resourcePath;
50
+ const result = transformReactLynxSync(source, {
51
+ pluginName: 'sigx:worklet',
52
+ filename,
53
+ sourcemap: false,
54
+ cssScope: false,
55
+ shake: false,
56
+ compat: false,
57
+ refresh: false,
58
+ defineDCE: false,
59
+ directiveDCE: false,
60
+ snapshot: false,
61
+ worklet: { target: 'JS', filename, runtimePkg: RUNTIME_PKG },
62
+ });
63
+ if (result.errors && result.errors.length > 0) {
64
+ for (const err of result.errors) {
65
+ this.emitError(new Error(`[sigx-worklet] JS transform: ${err.text}`));
66
+ }
67
+ return source;
68
+ }
69
+ return result.code;
28
70
  }
29
- //#endregion
30
- export { r as default };
31
-
32
- //# sourceMappingURL=worklet-loader.js.map
@@ -28,19 +28,4 @@
28
28
  * _wkltId is looked up in `lynxWorkletImpl._workletMap`.
29
29
  */
30
30
  export declare function extractLocalImports(source: string): string;
31
- /**
32
- * Extract `registerWorkletInternal(...)` calls from a LEPUS-target transform output.
33
- *
34
- * Upstream's LEPUS output includes:
35
- * - `import { loadWorkletRuntime as __loadWorkletRuntime } from "<runtimePkg>";`
36
- * - `var loadWorkletRuntime = __loadWorkletRuntime;`
37
- * - the original component code (which we drop — never invoked on MT)
38
- * - `const __workletRuntimeLoaded = loadWorkletRuntime(...);`
39
- * - `__workletRuntimeLoaded && registerWorkletInternal("main-thread", "<id>", function(...) { ... });`
40
- *
41
- * We only need the `registerWorkletInternal(...)` calls — the loadWorkletRuntime
42
- * gating is redundant because we only inject the calls when the MT bundle is
43
- * actually being built. Bracket-depth counting handles nested braces in the
44
- * function body.
45
- */
46
31
  export declare function extractRegistrations(lepusCode: string): string;
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Helpers shared by worklet-loader (BG) and worklet-loader-mt (MT).
3
+ *
4
+ * Mirrors vue-lynx's `worklet-utils.ts` minus the `?vue` sub-module logic
5
+ * (sigx has no Vue Single-File-Component pipeline) and minus the
6
+ * `extractSharedImports` path (deferred to Phase 1c — see plan).
7
+ */
8
+ /**
9
+ * Extract import statements whose target may contain `'main thread'` worklets
10
+ * the MT bundle needs to register.
11
+ *
12
+ * Two import flavours qualify:
13
+ * - Relative paths (`./foo`, `../bar`) — user code split across files.
14
+ * - `@sigx/*` package paths — workspace component packages like
15
+ * `@sigx/gestures` that ship `<Pressable>`/`<Draggable>` etc. These resolve
16
+ * to workspace `src/` (not node_modules) so the MT loader's rule still
17
+ * processes them; we just need to preserve the edge so rspack walks there.
18
+ *
19
+ * Converts named/default/namespace imports to side-effect-only imports
20
+ * (`import '@sigx/gestures';`) so webpack still follows the dependency graph
21
+ * without executing user code on the MT layer.
22
+ *
23
+ * Critical for the MT loader: an entry like `main.tsx` may not contain any
24
+ * `'main thread'` directives itself, but it imports route/component files
25
+ * that do. Without preserving these edges, webpack would never reach the
26
+ * files with worklet registrations and MT-side hydration of BG worklet ctxs
27
+ * would throw "cannot read property bind of undefined" when the unregistered
28
+ * _wkltId is looked up in `lynxWorkletImpl._workletMap`.
29
+ */
30
+ export function extractLocalImports(source) {
31
+ // Strip line and block comments so docstring examples like
32
+ // `* import X from './foo'` don't get parsed as real imports.
33
+ // String contents are left intact (we still catch `import './foo'` inside
34
+ // a string template), but real source rarely encodes import-statement
35
+ // shapes inside strings, and the false-positive rate with comments stripped
36
+ // is acceptable for this preserve-edges-only loader.
37
+ const stripped = source
38
+ .replace(/\/\*[\s\S]*?\*\//g, '')
39
+ .replace(/(^|[^:])\/\/[^\n]*/g, '$1');
40
+ const specifiers = new Set();
41
+ // Relative paths (./foo, ../bar) and @sigx/* package paths.
42
+ const re = /(?:from|import)\s+['"]((?:\.[^'"]+)|(?:@sigx\/[^'"]+))['"]/g;
43
+ let match;
44
+ while ((match = re.exec(stripped)) !== null) {
45
+ specifiers.add(match[1]);
46
+ }
47
+ if (specifiers.size === 0)
48
+ return '';
49
+ return [...specifiers].map((s) => `import '${s}';`).join('\n');
50
+ }
51
+ /**
52
+ * Extract `registerWorkletInternal(...)` calls from a LEPUS-target transform output.
53
+ *
54
+ * Upstream's LEPUS output includes:
55
+ * - `import { loadWorkletRuntime as __loadWorkletRuntime } from "<runtimePkg>";`
56
+ * - `var loadWorkletRuntime = __loadWorkletRuntime;`
57
+ * - the original component code (which we drop — never invoked on MT)
58
+ * - `const __workletRuntimeLoaded = loadWorkletRuntime(...);`
59
+ * - `__workletRuntimeLoaded && registerWorkletInternal("main-thread", "<id>", function(...) { ... });`
60
+ *
61
+ * We only need the `registerWorkletInternal(...)` calls — the loadWorkletRuntime
62
+ * gating is redundant because we only inject the calls when the MT bundle is
63
+ * actually being built. Bracket-depth counting handles nested braces in the
64
+ * function body.
65
+ *
66
+ * SWC's LEPUS transform preserves JSDoc / comments verbatim. When source code
67
+ * carries documentation that *mentions* the literal token
68
+ * `registerWorkletInternal(...)` (as `lynx-runtime`'s `threading.ts` does), a
69
+ * naive scan would extract that doc text as a "registration" and append it to
70
+ * the MT bundle as invalid JS (`function(...) { ... }` isn't real syntax).
71
+ * Strip comments before scanning so only real statements survive.
72
+ */
73
+ function stripJsComments(code) {
74
+ // Two passes — block comments first, then line comments. Block-first
75
+ // is important: a `// inside */` sequence inside a `/* ... */` block
76
+ // would otherwise have the inner `//` eaten by the line-comment pass
77
+ // before the outer block is matched, leaving stray `*/` in the output.
78
+ // We don't try to be string-literal-aware (a comment-looking sequence
79
+ // inside a string would be wrongly stripped) because:
80
+ // 1. The input is SWC output, which doesn't put `//` or `/* */` inside
81
+ // strings except in trivial constant cases we don't care about.
82
+ // 2. `extractRegistrations` only cares about call shapes; the surrounding
83
+ // code is discarded anyway.
84
+ return code
85
+ .replace(/\/\*[\s\S]*?\*\//g, '')
86
+ .replace(/(^|[^:])\/\/[^\n]*/g, '$1');
87
+ }
88
+ export function extractRegistrations(lepusCode) {
89
+ const code = stripJsComments(lepusCode);
90
+ const out = [];
91
+ const marker = 'registerWorkletInternal(';
92
+ let from = 0;
93
+ while (true) {
94
+ const idx = code.indexOf(marker, from);
95
+ if (idx === -1)
96
+ break;
97
+ let depth = 0;
98
+ let i = idx + marker.length - 1; // points at the opening '('
99
+ for (; i < code.length; i++) {
100
+ const ch = code[i];
101
+ if (ch === '(')
102
+ depth++;
103
+ else if (ch === ')') {
104
+ depth--;
105
+ if (depth === 0)
106
+ break;
107
+ }
108
+ }
109
+ let end = i + 1;
110
+ if (end < code.length && code[end] === ';')
111
+ end++;
112
+ out.push(code.slice(idx, end));
113
+ from = end;
114
+ }
115
+ return out.join('\n');
116
+ }
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sigx/lynx-plugin",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Rspack/Rspeedy plugin for SignalX Lynx dual-thread rendering",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -33,8 +33,9 @@
33
33
  "url": "https://github.com/signalxjs/lynx/issues"
34
34
  },
35
35
  "dependencies": {
36
- "@lynx-js/react": "^0.120.0",
37
- "@sigx/lynx-runtime-internal": "0.4.0"
36
+ "@lynx-js/react": "^0.121.0",
37
+ "ws": "^8.18.3",
38
+ "@sigx/lynx-runtime-internal": "0.4.1"
38
39
  },
39
40
  "peerDependencies": {
40
41
  "@rspack/core": ">=1.0.0",
@@ -43,8 +44,8 @@
43
44
  "@lynx-js/template-webpack-plugin": ">=0.1.0",
44
45
  "@lynx-js/runtime-wrapper-webpack-plugin": ">=0.1.0",
45
46
  "@lynx-js/webpack-dev-transport": ">=0.1.0",
46
- "@sigx/lynx-cli": "^0.4.0",
47
- "@sigx/lynx-icons": "^0.4.0"
47
+ "@sigx/lynx-cli": "^0.4.1",
48
+ "@sigx/lynx-icons": "^0.4.1"
48
49
  },
49
50
  "peerDependenciesMeta": {
50
51
  "@rspack/core": {
@@ -71,18 +72,19 @@
71
72
  },
72
73
  "devDependencies": {
73
74
  "@rsbuild/core": "^1.7.5",
74
- "@sigx/vite": "^0.4.3",
75
75
  "@types/node": "^25.7.0",
76
+ "@types/ws": "^8.5.13",
77
+ "@typescript/native-preview": "7.0.0-dev.20260511.1",
76
78
  "typescript": "^6.0.3",
77
- "vite": "^8.0.12",
78
- "@sigx/lynx-cli": "0.4.0",
79
- "@sigx/lynx-icons": "0.4.0"
79
+ "@sigx/lynx-icons": "0.4.1",
80
+ "@sigx/lynx-cli": "0.4.1"
80
81
  },
81
82
  "publishConfig": {
82
83
  "access": "public"
83
84
  },
84
85
  "scripts": {
85
- "build": "vite build && tsgo --emitDeclarationOnly",
86
- "dev": "vite build --watch"
86
+ "build": "node ../../scripts/clean.mjs dist && tsgo",
87
+ "dev": "tsgo --watch",
88
+ "clean": "node ../../scripts/clean.mjs dist .turbo"
87
89
  }
88
90
  }