Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions .changeset/six-readers-cover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@lynx-js/externals-loading-webpack-plugin": patch
"@lynx-js/react-refresh-webpack-plugin": patch
"@lynx-js/lynx-bundle-rslib-config": patch
---

Fix snapshot not found error when dev with external bundle
5 changes: 4 additions & 1 deletion examples/react-externals/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
"scripts": {
"build": "npm run build:comp-lib && npm run build:reactlynx && rspeedy build",
"build:comp-lib": "rslib build --config rslib-comp-lib.config.ts",
"build:comp-lib:dev": "cross-env NODE_ENV=development rslib build --config rslib-comp-lib.config.ts",
"build:reactlynx": "rslib build --config rslib-reactlynx.config.ts",
"build:reactlynx:dev": "cross-env NODE_ENV=development rslib build --config rslib-reactlynx.config.ts",
"dev": "rspeedy dev"
},
"dependencies": {
Expand All @@ -20,6 +22,7 @@
"@lynx-js/react-rsbuild-plugin": "workspace:*",
"@lynx-js/rspeedy": "workspace:*",
"@lynx-js/types": "3.7.0",
"@types/react": "^18.3.28"
"@types/react": "^18.3.28",
"cross-env": "^7.0.3"
}
}
1 change: 0 additions & 1 deletion examples/react-externals/rslib-comp-lib.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ export default defineExternalBundleRslibConfig({
'@lynx-js/react/debug': ['ReactLynx', 'ReactDebug'],
'preact': ['ReactLynx', 'Preact'],
},
minify: false,
globalObject: 'globalThis',
},
});
1 change: 0 additions & 1 deletion examples/react-externals/rslib-reactlynx.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export default defineExternalBundleRslibConfig({
],
output: {
cleanDistPath: false,
minify: false,
globalObject: 'globalThis',
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const defaultExternalBundleLibConfig: LibConfig = {
},
},
output: {
minify: {
minify: process.env['NODE_ENV'] === 'development' ? false : {
jsOptions: {
minimizerOptions: {
compress: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
// LICENSE file in the root directory of this source tree.
import type { BannerPlugin, Compiler } from 'webpack'

const PLUGIN_NAME = 'MainThreadRuntimeWrapperWebpackPlugin'

/**
* The options of {@link MainThreadRuntimeWrapperWebpackPlugin}.
*
Expand Down Expand Up @@ -46,5 +48,54 @@ export class MainThreadRuntimeWrapperWebpackPlugin {
})()`,
footer: true,
}).apply(compiler)

const { RuntimeGlobals, RuntimeModule } = compiler.webpack
class LoadingConsumerModulesRuntimeModule extends RuntimeModule {
constructor() {
super(
'Lynx externals loading consumer modules',
RuntimeModule.STAGE_ATTACH,
)
}
override generate() {
return `
__webpack_require__.i.push(function (options) {
var moduleId = options.id;
var globalModules = globalThis[Symbol.for('__LYNX_WEBPACK_MODULES__')];
if (globalModules && globalModules[moduleId]) {
if (!options.factory) {
options.factory = globalModules[moduleId];
}
}
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
`
}
}

const isDev = process.env['NODE_ENV'] === 'development'
|| compiler.options.mode === 'development'

if (isDev) {
compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
compilation.hooks.additionalTreeRuntimeRequirements.tap(
PLUGIN_NAME,
(_chunk, runtimeRequirements) => {
runtimeRequirements.add(RuntimeGlobals.interceptModuleExecution)
},
)

compilation.hooks.runtimeRequirementInTree
.for(RuntimeGlobals.interceptModuleExecution)
.tap(
PLUGIN_NAME,
(chunk) => {
compilation.addRuntimeModule(
chunk,
new LoadingConsumerModulesRuntimeModule(),
)
},
)
})
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,84 @@ describe('should build external bundle', () => {
'index__main-thread',
])
})

it('should include LoadingConsumerModulesRuntimeModule in the main-thread bundle', async () => {
vi.stubEnv('NODE_ENV', 'development')
const rslibConfig = defineExternalBundleRslibConfig({
source: {
entry: {
utils: path.join(__dirname, './fixtures/utils-lib/index.ts'),
},
},
id: 'utils-runtime-module',
output: {
distPath: {
root: path.join(fixtureDir, 'dist'),
},
minify: false,
},
plugins: [pluginReactLynx()],
})

await build(rslibConfig)

const decodedResult = await decodeTemplate(
path.join(fixtureDir, 'dist/utils-runtime-module.lynx.bundle'),
)

// Check if the runtime module code injected by LoadingConsumerModulesRuntimeModule is present
expect(decodedResult['custom-sections']['utils__main-thread']).toContain(
'var globalModules = globalThis[Symbol.for(\'__LYNX_WEBPACK_MODULES__\')];',
)
vi.unstubAllEnvs()
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})

describe('NODE_ENV configuration', () => {
const fixtureDir = path.join(__dirname, './fixtures/utils-lib')

const buildWithNodeEnv = async (
nodeEnv: 'development' | 'production',
id: string,
) => {
const prevNodeEnv = process.env['NODE_ENV']
process.env['NODE_ENV'] = nodeEnv
try {
const config = defineExternalBundleRslibConfig({
source: {
entry: {
utils: path.join(fixtureDir, 'index.ts'),
},
},
id,
output: {
distPath: { root: path.join(fixtureDir, 'dist') },
},
plugins: [pluginReactLynx()],
})
await build(config)
return await decodeTemplate(
path.join(fixtureDir, `dist/${id}.lynx.bundle`),
)
} finally {
process.env['NODE_ENV'] = prevNodeEnv
}
}

it('should output different artifacts for development and production NODE_ENV', async () => {
const devResult = await buildWithNodeEnv('development', 'utils-dev')
const prodResult = await buildWithNodeEnv('production', 'utils-prod')

const devMainThread = devResult['custom-sections']['utils__main-thread']!
const prodMainThread = prodResult['custom-sections']['utils__main-thread']!

// The produced artifacts should be different
expect(devMainThread).not.toBe(prodMainThread)

// __DEV__ macro should be replaced differently
expect(devMainThread).toMatch(/isDev:\s*(!0|true)/)
expect(prodMainThread).toMatch(/isDev:\s*(!1|false)/)
})
})

describe('debug mode artifacts', () => {
Expand Down
24 changes: 16 additions & 8 deletions packages/webpack/externals-loading-webpack-plugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,10 +328,14 @@ function createLoadExternalAsync(handler, sectionPath) {
isMainThreadLayer
? `
// TODO: Use configured layer suffix instead of hard-coded __main-thread for CSS section lookup.
const styleSheet = __LoadStyleSheet(sectionPath.replace('__main-thread', '') + ':CSS', response.url);
if (styleSheet !== null) {
__AdoptStyleSheet(styleSheet);
__FlushElementTree();
if (typeof __LoadStyleSheet === 'function') {
const styleSheet = __LoadStyleSheet(sectionPath.replace('__main-thread', '') + ':CSS', response.url);
if (styleSheet !== null) {
__AdoptStyleSheet(styleSheet);
__FlushElementTree();
}
} else {
console.warn('__LoadStyleSheet is not defined. Failed to load CSS for ' + sectionPath + ' in ' + response.url + '. __LoadStyleSheet is only available in LynxSDK >= 3.7');
}
`
: ''
Expand All @@ -356,10 +360,14 @@ function createLoadExternalSync(handler, sectionPath, timeout) {
isMainThreadLayer
? `
// TODO: Use configured layer suffix instead of hard-coded __main-thread for CSS section lookup.
const styleSheet = __LoadStyleSheet(sectionPath.replace('__main-thread', '') + ':CSS', response.url);
if (styleSheet !== null) {
__AdoptStyleSheet(styleSheet);
__FlushElementTree();
if (typeof __LoadStyleSheet === 'function') {
const styleSheet = __LoadStyleSheet(sectionPath.replace('__main-thread', '') + ':CSS', response.url);
if (styleSheet !== null) {
__AdoptStyleSheet(styleSheet);
__FlushElementTree();
}
} else {
console.warn('__LoadStyleSheet is not defined. Failed to load CSS for ' + sectionPath + ' in ' + response.url + '. __LoadStyleSheet is only available in LynxSDK >= 3.7');
}
`
: ''
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,5 @@ __webpack_require__.i.push(function(options) {
}
};
});

globalThis[Symbol.for('__LYNX_WEBPACK_MODULES__')] = __webpack_modules__;
129 changes: 129 additions & 0 deletions packages/webpack/react-refresh-webpack-plugin/test/intercept.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Copyright 2026 The Lynx Authors. All rights reserved.
// Licensed under the Apache License Version 2.0 that can be found in the
// LICENSE file in the root directory of this source tree.
import { describe, expect, it } from 'vitest';

import { ReactRefreshRspackPlugin } from '../src/ReactRefreshRspackPlugin.js';
import { ReactRefreshWebpackPlugin } from '../src/ReactRefreshWebpackPlugin.js';

describe('ReactRefresh plugins', () => {
it('ReactRefreshWebpackPlugin intercept code generation in dev', () => {
const plugin = new ReactRefreshWebpackPlugin();
let generateResult = '';

const mockCompiler = {
options: { mode: 'development' },
context: '/test',
webpack: {
EntryPlugin: class {
apply() {
/* noop */
}
},
ProvidePlugin: class {
apply() {
/* noop */
}
},
RuntimeGlobals: {
interceptModuleExecution: 'interceptModuleExecution',
},
RuntimeModule: class {
name: string;
stage: number;
constructor(name: string, stage: number) {
this.name = name;
this.stage = stage;
}
},
},
hooks: {
compilation: {
tap: (_name: string, cb: (compilation: unknown) => void) => {
const compilation = {
compiler: mockCompiler,
mainTemplate: {
hooks: {
localVars: {
tap: () => {
/* noop */
},
},
},
},
hooks: {
additionalTreeRuntimeRequirements: {
tap: (
_name: string,
reqCb: (chunk: string, requirements: Set<string>) => void,
) => {
const requirements = new Set<string>();
reqCb('chunk', requirements);
},
},
},
addRuntimeModule: (
_chunk: unknown,
module: { generate: () => string },
) => {
generateResult = module.generate();
},
};
cb(compilation);
},
},
},
};

plugin.apply(mockCompiler);
expect(generateResult).toContain('__webpack_modules__');
expect(generateResult).toContain(
'globalThis[Symbol.for(\'__LYNX_WEBPACK_MODULES__\')] = __webpack_modules__;',
);
});

it('ReactRefreshRspackPlugin intercept code generation in dev', () => {
const plugin = new ReactRefreshRspackPlugin();
let generateResult = '';

const mockCompiler = {
options: { mode: 'development' },
webpack: {
ProvidePlugin: class {
apply() {
/* noop */
}
},
},
hooks: {
thisCompilation: {
tap: (_name: string, cb: (compilation: unknown) => void) => {
const compilation = {
hooks: {
runtimeModule: {
tap: (_name: string, moduleCb: (module: unknown) => void) => {
const module = {
name: 'hot_module_replacement',
source: { source: Buffer.from('') },
};
moduleCb(module);

generateResult = module.source.source.toString();
},
},
},
};
cb(compilation);
},
},
},
};

// @ts-expect-error test mock
plugin.apply(mockCompiler);
expect(generateResult).toContain('__webpack_modules__');
expect(generateResult).toContain(
'globalThis[Symbol.for(\'__LYNX_WEBPACK_MODULES__\')] = __webpack_modules__;',
);
});
});
Loading
Loading