@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/css.js ADDED
@@ -0,0 +1,150 @@
1
+ /**
2
+ * CSS extraction pipeline for SignalX Lynx.
3
+ *
4
+ * Mirrors the behaviour of `@lynx-js/react-rsbuild-plugin`'s `applyCSS()`:
5
+ * 1. Disables `style-loader` (forces CSS extraction via CssExtractPlugin).
6
+ * 2. Replaces the rsbuild-default CssExtract plugin with
7
+ * `@lynx-js/css-extract-webpack-plugin` which emits Lynx-compatible CSS.
8
+ * 3. Removes `lightningcss-loader` (Lynx has its own CSS processor).
9
+ * 4. Configures the Main-Thread layer to ignore CSS entirely.
10
+ */
11
+ import path from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+ import { LAYERS } from './layers.js';
14
+ const _dirname = path.dirname(fileURLToPath(import.meta.url));
15
+ export function applyCSS(api, options) {
16
+ const { enableCSSSelector, enableCSSInvalidation } = options;
17
+ // ① Force CSS extraction (disable style-loader, enable CssExtractPlugin).
18
+ // Without this, rsbuild injects CSS via JS — useless in Lynx's native env.
19
+ api.modifyRsbuildConfig((config, { mergeRsbuildConfig }) => {
20
+ return mergeRsbuildConfig(config, {
21
+ output: { injectStyles: false },
22
+ });
23
+ });
24
+ // ② Replace the rsbuild-default CSS extraction plugin with the Lynx-aware
25
+ // one, configure loaders per layer, and remove lightningcss.
26
+ api.modifyBundlerChain(async function handler(chain, { CHAIN_ID }) {
27
+ const { CssExtractRspackPlugin, CssExtractWebpackPlugin } = await import('@lynx-js/css-extract-webpack-plugin');
28
+ const CssExtractPlugin = api.context.bundlerType === 'rspack'
29
+ ? CssExtractRspackPlugin
30
+ : CssExtractWebpackPlugin;
31
+ const cssRules = [
32
+ CHAIN_ID.RULE.CSS,
33
+ CHAIN_ID.RULE.SASS,
34
+ CHAIN_ID.RULE.LESS,
35
+ CHAIN_ID.RULE.STYLUS,
36
+ ];
37
+ cssRules
38
+ .filter((rule) => chain.module.rules.has(rule))
39
+ .forEach((ruleName) => {
40
+ const rule = chain.module.rule(ruleName);
41
+ // Remove lightningcss-loader — Lynx processes CSS natively.
42
+ removeLightningCSS(rule, CHAIN_ID);
43
+ // Use the Lynx CssExtract loader for the Background layer.
44
+ rule
45
+ .issuerLayer(LAYERS.BACKGROUND)
46
+ .use(CHAIN_ID.USE.MINI_CSS_EXTRACT)
47
+ .loader(CssExtractPlugin.loader)
48
+ .end();
49
+ // Clone the existing CSS rule chain for the Main-Thread layer.
50
+ // Main-Thread bundles never contain user CSS — only the PAPI
51
+ // bootstrap code. We replace all loaders with ignore-css + a
52
+ // css-loader configured for `exportOnlyLocals: true`.
53
+ const uses = rule.uses.entries();
54
+ const ruleEntries = rule.entries();
55
+ const cssLoaderRule = uses[CHAIN_ID.USE.CSS]?.entries();
56
+ chain.module
57
+ .rule(`${ruleName}:${LAYERS.MAIN_THREAD}`)
58
+ .merge(ruleEntries)
59
+ .issuerLayer(LAYERS.MAIN_THREAD)
60
+ .use(CHAIN_ID.USE.IGNORE_CSS)
61
+ .loader(path.resolve(_dirname, './loaders/ignore-css-loader'))
62
+ .end()
63
+ .uses.merge(uses)
64
+ .delete(CHAIN_ID.USE.MINI_CSS_EXTRACT)
65
+ .delete(CHAIN_ID.USE.LIGHTNINGCSS)
66
+ .delete(CHAIN_ID.USE.CSS)
67
+ .end();
68
+ // Re-add css-loader with exportOnlyLocals for main-thread
69
+ if (cssLoaderRule) {
70
+ chain.module
71
+ .rule(`${ruleName}:${LAYERS.MAIN_THREAD}`)
72
+ .use(CHAIN_ID.USE.CSS)
73
+ .after(CHAIN_ID.USE.IGNORE_CSS)
74
+ .merge(cssLoaderRule)
75
+ .options(normalizeCssLoaderOptions(cssLoaderRule.options, true))
76
+ .end();
77
+ }
78
+ });
79
+ // Also strip lightningcss from inline CSS rules (Rsbuild ≥1.3.0).
80
+ const RULE = CHAIN_ID.RULE;
81
+ const inlineCSSRuleNames = [
82
+ 'CSS_INLINE',
83
+ 'SASS_INLINE',
84
+ 'LESS_INLINE',
85
+ 'STYLUS_INLINE',
86
+ ];
87
+ inlineCSSRuleNames
88
+ .map((key) => RULE[key])
89
+ .filter((ruleName) => !!ruleName && chain.module.rules.has(ruleName))
90
+ .forEach((ruleName) => {
91
+ removeLightningCSS(chain.module.rule(ruleName), CHAIN_ID);
92
+ });
93
+ // ③ Replace the CssExtract plugin instance with the Lynx-aware one
94
+ // and pass through the CSS selector / invalidation options.
95
+ chain
96
+ .plugin(CHAIN_ID.PLUGIN.MINI_CSS_EXTRACT)
97
+ .tap((args) => {
98
+ const [pluginOptions] = args;
99
+ return [
100
+ {
101
+ ...pluginOptions,
102
+ enableRemoveCSSScope: true,
103
+ enableCSSSelector,
104
+ enableCSSInvalidation,
105
+ cssPlugins: [],
106
+ },
107
+ ];
108
+ })
109
+ .init((_, args) => {
110
+ return new CssExtractPlugin(...args);
111
+ })
112
+ .end()
113
+ .end();
114
+ function removeLightningCSS(rule, ids) {
115
+ if (rule.uses.has(ids.USE.LIGHTNINGCSS)) {
116
+ rule.uses.delete(ids.USE.LIGHTNINGCSS);
117
+ }
118
+ }
119
+ });
120
+ }
121
+ /**
122
+ * Force `exportOnlyLocals: true` on the css-loader modules config.
123
+ * Copied from rsbuild internals — required when the target is not `web`
124
+ * and CSS modules are enabled.
125
+ */
126
+ const normalizeCssLoaderOptions = (options, exportOnlyLocals) => {
127
+ if (options.modules && exportOnlyLocals) {
128
+ let { modules } = options;
129
+ if (modules === true) {
130
+ modules = { exportOnlyLocals: true };
131
+ }
132
+ else if (typeof modules === 'string') {
133
+ modules = {
134
+ mode: modules,
135
+ exportOnlyLocals: true,
136
+ };
137
+ }
138
+ else {
139
+ modules = {
140
+ ...modules,
141
+ exportOnlyLocals: true,
142
+ };
143
+ }
144
+ return {
145
+ ...options,
146
+ modules,
147
+ };
148
+ }
149
+ return options;
150
+ };
package/dist/entry.js ADDED
@@ -0,0 +1,443 @@
1
+ /**
2
+ * Dual-thread entry splitting for SignalX Lynx.
3
+ *
4
+ * For each user-defined rsbuild entry, creates two webpack entries:
5
+ * - `<name>__main-thread` on the MAIN_THREAD layer (PAPI bootstrap via @sigx/lynx-runtime-main)
6
+ * - `<name>` on the BACKGROUND layer (sigx renderer + user app)
7
+ *
8
+ * Then registers @lynx-js/template-webpack-plugin to stitch both bundles
9
+ * into a single .lynx template.
10
+ */
11
+ import path from 'node:path';
12
+ import { existsSync } from 'node:fs';
13
+ import { fileURLToPath } from 'node:url';
14
+ import { createRequire } from 'node:module';
15
+ import { LAYERS } from './layers.js';
16
+ const PLUGIN_TEMPLATE = 'lynx:sigx-template';
17
+ const PLUGIN_MARK_MAIN_THREAD = 'lynx:sigx-mark-main-thread';
18
+ const PLUGIN_ENCODE = 'lynx:sigx-encode';
19
+ const DEFAULT_INTERMEDIATE = '.rspeedy';
20
+ const _dirname = path.dirname(fileURLToPath(import.meta.url));
21
+ // sigx lynx-plugin package root — the plugin lives at <pkgRoot>/dist/,
22
+ // so we resolve one level up from _dirname.
23
+ const sigxLynxRoot = path.resolve(_dirname, '..');
24
+ /**
25
+ * SigxMarkMainThreadPlugin forces webpack to generate startup code for MT
26
+ * entry chunks and marks their assets with `lynx:main-thread: true` so
27
+ * LynxTemplatePlugin routes them to lepusCode.root (Lepus bytecode).
28
+ */
29
+ class SigxMarkMainThreadPlugin {
30
+ mainThreadFilenames;
31
+ constructor(mainThreadFilenames) {
32
+ this.mainThreadFilenames = mainThreadFilenames;
33
+ }
34
+ apply(compiler) {
35
+ const { RuntimeGlobals } = compiler.webpack;
36
+ compiler.hooks.thisCompilation.tap(PLUGIN_MARK_MAIN_THREAD, (compilation) => {
37
+ // Force startup code generation for MT entry chunks.
38
+ compilation.hooks.additionalTreeRuntimeRequirements.tap(PLUGIN_MARK_MAIN_THREAD, (chunk, set) => {
39
+ const entryOptions = chunk.getEntryOptions();
40
+ if (entryOptions?.layer === LAYERS.MAIN_THREAD) {
41
+ set.add(RuntimeGlobals.startup);
42
+ set.add(RuntimeGlobals.require);
43
+ }
44
+ });
45
+ // Mark MT assets with lynx:main-thread: true for LynxTemplatePlugin.
46
+ compilation.hooks.processAssets.tap({
47
+ name: PLUGIN_MARK_MAIN_THREAD,
48
+ stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL,
49
+ }, () => {
50
+ for (const filename of this.mainThreadFilenames) {
51
+ const asset = compilation.getAsset(filename);
52
+ if (asset) {
53
+ compilation.updateAsset(filename, asset.source, {
54
+ ...asset.info,
55
+ 'lynx:main-thread': true,
56
+ });
57
+ }
58
+ }
59
+ });
60
+ });
61
+ }
62
+ }
63
+ export async function applyEntry(api, opts = {}) {
64
+ // Preload @lynx-js/template-webpack-plugin via dynamic ESM import.
65
+ // rsbuild bundlerChain callbacks are sync, and template-webpack-plugin
66
+ // is pure-ESM (no "require" condition in its exports map), so createRequire
67
+ // fails. Stash the module in closure scope for the sync callback below.
68
+ let templateMod;
69
+ try {
70
+ templateMod = await import('@lynx-js/template-webpack-plugin');
71
+ }
72
+ catch {
73
+ // Optional peer — if missing, we'll still emit the two JS bundles.
74
+ }
75
+ // Preload @lynx-js/runtime-wrapper-webpack-plugin. This wraps the BG bundle
76
+ // in `__init_card_bundle__(lynxCoreInject, lynx, ...)` so user code inside
77
+ // can reference `lynx` and `lynxCoreInject` as bare identifiers — that's
78
+ // how the BG transport (lynx.getNativeApp().callLepusMethod) and the event
79
+ // dispatcher (lynxCoreInject.tt.publishEvent) get installed properly.
80
+ // Without this wrapper we'd be forced to spelunk through globalThis.multiApps.
81
+ let wrapperMod;
82
+ try {
83
+ wrapperMod = (await import('@lynx-js/runtime-wrapper-webpack-plugin'));
84
+ }
85
+ catch {
86
+ // Optional peer — if missing, lynx-runtime will still attempt the
87
+ // multiApps[appId]._nativeApp fallback, but proper hosts need the wrapper.
88
+ }
89
+ // Default to all-in-one chunk splitting to avoid async chunks that break
90
+ // Lynx's single-file bundle requirement.
91
+ api.modifyRsbuildConfig((config, { mergeRsbuildConfig }) => {
92
+ const userConfig = api.getRsbuildConfig('original');
93
+ if (!userConfig.performance?.chunkSplit?.strategy) {
94
+ return mergeRsbuildConfig(config, {
95
+ performance: { chunkSplit: { strategy: 'all-in-one' } },
96
+ });
97
+ }
98
+ return config;
99
+ });
100
+ // Exclude main-thread chunks from chunk splitting so each remains
101
+ // self-contained.
102
+ api.modifyRspackConfig((rspackConfig) => {
103
+ if (!rspackConfig.optimization)
104
+ return rspackConfig;
105
+ if (rspackConfig.optimization.splitChunks === false) {
106
+ rspackConfig.optimization.splitChunks = {};
107
+ }
108
+ if (rspackConfig.optimization.splitChunks) {
109
+ const prev = rspackConfig.optimization.splitChunks.chunks;
110
+ // biome-ignore lint/suspicious/noExplicitAny: rspack Chunk type not importable
111
+ rspackConfig.optimization.splitChunks.chunks = (chunk) => {
112
+ if (chunk.name?.includes('__main-thread'))
113
+ return false;
114
+ if (typeof prev === 'function')
115
+ return prev(chunk);
116
+ if (prev === 'all')
117
+ return true;
118
+ if (prev === 'initial')
119
+ return true;
120
+ return false;
121
+ };
122
+ }
123
+ return rspackConfig;
124
+ });
125
+ // Preload `@sigx/lynx-dev-client/install` — the JS-side console streamer.
126
+ // We resolve it eagerly (rather than relying on the bundler's resolver)
127
+ // so that:
128
+ // * absence of the package is detected once at config time (consumer may
129
+ // not depend on `@sigx/lynx-dev-client`), and
130
+ // * we can pass an absolute path to rspack's entry, sidestepping any
131
+ // subpath-export quirks.
132
+ //
133
+ // In linked / monorepo setups the plugin can live anywhere on disk, so we
134
+ // try multiple resolution bases — `api.context.rootPath`, the current
135
+ // process cwd, and finally the plugin's own location — and stop at the
136
+ // first one that finds it. This covers monorepo workspaces where the
137
+ // dev-client is hoisted to the workspace root as well as per-app installs.
138
+ //
139
+ // Returns `undefined` if the package isn't installed — the BG entry is
140
+ // then left alone and log streaming is a silent no-op for that project.
141
+ const resolveBases = [
142
+ path.join(api.context.rootPath, 'package.json'),
143
+ path.join(process.cwd(), 'package.json'),
144
+ ];
145
+ let devClientInstallPath;
146
+ for (const base of resolveBases) {
147
+ try {
148
+ devClientInstallPath = createRequire(base).resolve('@sigx/lynx-dev-client/install');
149
+ break;
150
+ }
151
+ catch {
152
+ // Subpath export may only declare `import` (Node CJS resolver wants
153
+ // `require`/`default`). Fall back to locating package.json and
154
+ // hand-constructing the path to dist/install.js.
155
+ try {
156
+ const pkgJson = createRequire(base).resolve('@sigx/lynx-dev-client/package.json');
157
+ const candidate = path.join(path.dirname(pkgJson), 'dist', 'install.js');
158
+ if (existsSync(candidate)) {
159
+ devClientInstallPath = candidate;
160
+ break;
161
+ }
162
+ }
163
+ catch {
164
+ // try next base
165
+ }
166
+ }
167
+ }
168
+ if (!devClientInstallPath) {
169
+ try {
170
+ devClientInstallPath = createRequire(import.meta.url).resolve('@sigx/lynx-dev-client/install');
171
+ }
172
+ catch {
173
+ devClientInstallPath = undefined;
174
+ }
175
+ }
176
+ if (devClientInstallPath) {
177
+ api.logger.info(`[sigx-lynx] device console log streaming → enabled`);
178
+ }
179
+ else {
180
+ api.logger.warn(`[sigx-lynx] device console log streaming → disabled (install @sigx/lynx-dev-client as a devDependency of this app). rootPath=${api.context.rootPath}, cwd=${process.cwd()}`);
181
+ }
182
+ api.modifyBundlerChain((chain, { environment, isProd }) => {
183
+ const isRspeedy = api.context.callerName === 'rspeedy';
184
+ if (!isRspeedy)
185
+ return;
186
+ const isDev = !isProd;
187
+ const isLynx = environment.name === 'lynx' || environment.name.startsWith('lynx-');
188
+ const isWeb = environment.name === 'web' || environment.name.startsWith('web-');
189
+ // HMR / Live Reload flags (same logic as vue-lynx / React plugin)
190
+ const { hmr, liveReload } = environment.config.dev ?? {};
191
+ const enabledHMR = isDev && !isWeb && hmr !== false;
192
+ const enabledLiveReload = isDev && !isWeb && liveReload !== false;
193
+ const entries = chain.entryPoints.entries() ?? {};
194
+ chain.entryPoints.clear();
195
+ // Collect all main-thread filenames to mark with lynx:main-thread
196
+ const mainThreadFilenames = [];
197
+ for (const [entryName, entryPoint] of Object.entries(entries)) {
198
+ // Collect user imports from the original entry
199
+ const imports = [];
200
+ const ep = entryPoint;
201
+ for (const val of ep.values()) {
202
+ if (typeof val === 'string') {
203
+ imports.push(val);
204
+ }
205
+ else if (typeof val === 'object' && val !== null && 'import' in val) {
206
+ const imp = val.import;
207
+ if (Array.isArray(imp))
208
+ imports.push(...imp);
209
+ else if (imp)
210
+ imports.push(imp);
211
+ }
212
+ }
213
+ // ----------------------------------------------------------------
214
+ // Filenames
215
+ // ----------------------------------------------------------------
216
+ const intermediate = isLynx ? DEFAULT_INTERMEDIATE : '';
217
+ const mainThreadEntry = `${entryName}__main-thread`;
218
+ const mainThreadName = path.posix.join(intermediate, `${entryName}/main-thread.js`);
219
+ const backgroundName = path.posix.join(intermediate, `${entryName}/background${isProd ? '.[contenthash:8]' : ''}.js`);
220
+ if (isLynx || isWeb) {
221
+ mainThreadFilenames.push(mainThreadName);
222
+ }
223
+ // ----------------------------------------------------------------
224
+ // Main Thread bundle – PAPI bootstrap only
225
+ // ----------------------------------------------------------------
226
+ // The MT entry ONLY imports @sigx/lynx-runtime-main, which registers
227
+ // globalThis.renderPage, processData, sigxPatchUpdate and bridges
228
+ // ops from the background thread.
229
+ //
230
+ // MT bundle evaluation order (critical):
231
+ // The bootstrap (entry-main → worklet-runtime → install-hybrid-worklet)
232
+ // is prepended to every user file by `worklet-loader-mt.ts` using
233
+ // absolute paths resolved from the loader's install location. That
234
+ // means we DON'T list those modules here as entry imports — the dep
235
+ // graph that the loader-emitted preamble creates pulls them in, in
236
+ // the right order, without forcing the user's app package.json to
237
+ // declare @lynx-js/react as a direct dep.
238
+ //
239
+ // So the MT entry list is just: user imports. (CSS HMR runtime in
240
+ // dev mode only.) Worklet registrations land via the dep graph.
241
+ const mainThreadImports = !enabledHMR
242
+ ? [...imports]
243
+ : [
244
+ '@lynx-js/css-extract-webpack-plugin/runtime/hotModuleReplacement.lepus.cjs',
245
+ ...imports,
246
+ ];
247
+ chain
248
+ .entry(mainThreadEntry)
249
+ .add({
250
+ layer: LAYERS.MAIN_THREAD,
251
+ import: mainThreadImports,
252
+ filename: mainThreadName,
253
+ })
254
+ .end();
255
+ // ----------------------------------------------------------------
256
+ // Background bundle – sigx renderer + user app
257
+ // ----------------------------------------------------------------
258
+ const bgImports = [];
259
+ bgImports.push(...imports);
260
+ const bgEntry = chain
261
+ .entry(entryName)
262
+ .add({
263
+ layer: LAYERS.BACKGROUND,
264
+ import: bgImports,
265
+ filename: backgroundName,
266
+ });
267
+ // Inject standard rspack HMR client + Lynx WebSocket transport into
268
+ // the BG entry (matching vue-lynx's approach). These must be prepended
269
+ // so they initialise before user code.
270
+ if (enabledHMR) {
271
+ bgEntry.prepend({
272
+ layer: LAYERS.BACKGROUND,
273
+ import: '@rspack/core/hot/dev-server',
274
+ });
275
+ // BG → MT hot-update bridge. Subscribes to the same `webpackHotUpdate`
276
+ // emitter event as `@rspack/core/hot/dev-server`, fetches the matching
277
+ // `main__main-thread.<hash>.hot-update.js`, and forwards extracted
278
+ // `registerWorkletInternal` calls to MT via `callLepusMethod`. Without
279
+ // this, MT's `_workletMap` keeps the old worklet IDs from the static
280
+ // bundle while BG sends ops referencing new content-hash IDs after a
281
+ // save → bind-of-undefined on tap.
282
+ bgEntry.prepend({
283
+ layer: LAYERS.BACKGROUND,
284
+ import: '@sigx/lynx-runtime/mt-hmr-bridge',
285
+ });
286
+ }
287
+ if (enabledHMR || enabledLiveReload) {
288
+ bgEntry.prepend({
289
+ layer: LAYERS.BACKGROUND,
290
+ import: '@lynx-js/webpack-dev-transport/client',
291
+ });
292
+ }
293
+ // Auto-install the console log streamer in dev. Prepended LAST so
294
+ // it runs FIRST at runtime (after webpack-dev-transport so the
295
+ // dev URL is plumbed). Skipped if the dev-client package isn't
296
+ // installed in the consuming project.
297
+ if (isDev && !isWeb && devClientInstallPath) {
298
+ bgEntry.prepend({
299
+ layer: LAYERS.BACKGROUND,
300
+ import: devClientInstallPath,
301
+ });
302
+ }
303
+ bgEntry.end();
304
+ // ----------------------------------------------------------------
305
+ // LynxTemplatePlugin – packages both bundles into .lynx template
306
+ // ----------------------------------------------------------------
307
+ if ((isLynx || isWeb) && templateMod) {
308
+ {
309
+ const { LynxTemplatePlugin } = templateMod;
310
+ const templateFilename = (typeof environment.config.output.filename === 'object'
311
+ ? environment.config.output.filename
312
+ .bundle
313
+ : environment.config.output.filename) ??
314
+ '[name].[platform].bundle';
315
+ chain
316
+ .plugin(`${PLUGIN_TEMPLATE}-${entryName}`)
317
+ .use(LynxTemplatePlugin, [
318
+ {
319
+ ...LynxTemplatePlugin.defaultOptions,
320
+ dsl: 'react_nodiff',
321
+ chunks: [mainThreadEntry, entryName],
322
+ filename: templateFilename
323
+ .replaceAll('[name]', entryName)
324
+ .replaceAll('[platform]', environment.name),
325
+ intermediate: path.posix.join(intermediate, entryName),
326
+ debugInfoOutside: opts.debugInfoOutside ?? true,
327
+ enableCSSSelector: opts.enableCSSSelector ?? true,
328
+ enableCSSInvalidation: opts.enableCSSSelector ?? true,
329
+ enableCSSInheritance: opts.enableCSSInheritance ?? false,
330
+ customCSSInheritanceList: opts.customCSSInheritanceList,
331
+ enableRemoveCSSScope: true,
332
+ enableNewGesture: true,
333
+ removeDescendantSelectorScope: true,
334
+ cssPlugins: [],
335
+ },
336
+ ])
337
+ .end();
338
+ }
339
+ }
340
+ }
341
+ // ------------------------------------------------------------------
342
+ // SigxMarkMainThreadPlugin – mark MT assets for LynxTemplatePlugin
343
+ // ------------------------------------------------------------------
344
+ if ((isLynx || isWeb) && mainThreadFilenames.length > 0) {
345
+ chain
346
+ .plugin(PLUGIN_MARK_MAIN_THREAD)
347
+ .use(SigxMarkMainThreadPlugin, [mainThreadFilenames])
348
+ .end();
349
+ }
350
+ // ------------------------------------------------------------------
351
+ // RuntimeWrapperWebpackPlugin – wrap BG bundle (NOT main-thread.js)
352
+ // in __init_card_bundle__(lynxCoreInject, lynx, ...). Inside the
353
+ // wrapper, lynx-runtime code can reference `lynx` and `lynxCoreInject`
354
+ // as bare identifiers, giving us the official BG → MT bridge and
355
+ // event dispatch hooks.
356
+ // ------------------------------------------------------------------
357
+ if (isLynx && wrapperMod) {
358
+ const { RuntimeWrapperWebpackPlugin } = wrapperMod;
359
+ chain
360
+ .plugin('lynx:sigx-runtime-wrapper')
361
+ .use(RuntimeWrapperWebpackPlugin, [
362
+ {
363
+ // Wrap everything except main-thread.js (and main-thread.[hash].js).
364
+ test: /^(?!.*main-thread(?:\.[A-Fa-f0-9]*)?\.js$).*\.js$/,
365
+ },
366
+ ])
367
+ .end();
368
+ }
369
+ // ------------------------------------------------------------------
370
+ // LynxEncodePlugin – binary-encode the .lynx template
371
+ // ------------------------------------------------------------------
372
+ if (isLynx && templateMod) {
373
+ const { LynxEncodePlugin } = templateMod;
374
+ chain
375
+ .plugin(PLUGIN_ENCODE)
376
+ .use(LynxEncodePlugin, [{}])
377
+ .end();
378
+ }
379
+ // ------------------------------------------------------------------
380
+ // HMR loader – inject registerHMRModule() + module.hot.accept()
381
+ // into component files on the BG layer so they self-accept hot
382
+ // updates and patch instances in-place (no structural tree ops).
383
+ // ------------------------------------------------------------------
384
+ if (enabledHMR) {
385
+ chain.module
386
+ .rule('sigx-hmr')
387
+ .test(/\.[jt]sx?$/)
388
+ .issuerLayer(LAYERS.BACKGROUND)
389
+ .exclude
390
+ .add(/node_modules/)
391
+ .add(/dist/)
392
+ .end()
393
+ .enforce('pre')
394
+ .use('sigx-hmr-loader')
395
+ .loader(path.resolve(_dirname, './loaders/hmr-loader'))
396
+ .end();
397
+ }
398
+ // ------------------------------------------------------------------
399
+ // Worklet loaders — both layers run @lynx-js/react/transform.
400
+ // BG layer: target='JS' replaces 'main thread' functions with
401
+ // { _wkltId, _c? } placeholders shipped via SET_WORKLET_EVENT.
402
+ // MT layer: target='LEPUS' produces registerWorkletInternal(...) calls;
403
+ // the loader extracts those + local-import edges.
404
+ //
405
+ // Rules run on every JS/TS file in their respective layer — no
406
+ // package allowlist and no `node_modules`/`dist` rule exclude. The
407
+ // loaders gate themselves on directive presence (cheap regex
408
+ // pre-filter, then SWC). The MT loader additionally branches on the
409
+ // file's path because rspack shares module identity across BG/MT
410
+ // layers — see the decision table in `worklet-loader-mt.ts` — so an
411
+ // MT-side body strip of a library file would wipe its named exports
412
+ // for BG consumers too. That MT-side preservation keeps
413
+ // `@sigx/lynx-runtime-main`'s MT globals (`processData`,
414
+ // `updateGlobalProps`, `sigxRunOnMT`) and lets cross-package
415
+ // consumers like `@sigx/lynx-daisyui` resolve named imports
416
+ // (`useTabs`, `useScreenChrome`) from worklet-shipping packages.
417
+ //
418
+ // The BG loader has no path branch; for directive-bearing files
419
+ // (user or library) it returns the JS-target transform output,
420
+ // which preserves exports while replacing worklet bodies with
421
+ // `{ _wkltId }` placeholders. New packages that ship `'main thread'`
422
+ // directives in their dist are picked up automatically — no
423
+ // manual opt-in.
424
+ chain.module
425
+ .rule('sigx-worklet')
426
+ .test(/\.[jt]sx?$/)
427
+ .issuerLayer(LAYERS.BACKGROUND)
428
+ .enforce('pre')
429
+ .use('sigx-worklet-loader')
430
+ .loader(path.resolve(_dirname, './loaders/worklet-loader'))
431
+ .end();
432
+ chain.module
433
+ .rule('sigx-worklet-mt')
434
+ .test(/\.[jt]sx?$/)
435
+ .issuerLayer(LAYERS.MAIN_THREAD)
436
+ .enforce('pre')
437
+ .use('sigx-worklet-mt-loader')
438
+ .loader(path.resolve(_dirname, './loaders/worklet-loader-mt'))
439
+ .end();
440
+ // Disable IIFE wrapping – Lynx handles module scoping itself
441
+ chain.output.set('iife', false);
442
+ });
443
+ }