From 7d6df7b3121835651ca91135c422c3510d0129a6 Mon Sep 17 00:00:00 2001 From: Yiming Li Date: Wed, 21 Jan 2026 17:08:45 +0800 Subject: [PATCH 1/2] feat: allow globalObject config and reuse fetchBundle result --- .changeset/wet-fans-sleep.md | 7 ++ examples/react-externals/lynx.config.js | 1 + .../react-externals/rslib-comp-lib.config.ts | 1 + .../react-externals/rslib-reactlynx.config.ts | 1 + .../src/externalBundleRslibConfig.ts | 18 ++++- .../test/external-bundle.test.ts | 78 +++++++++++++++++++ .../etc/external-bundle-rsbuild-plugin.api.md | 2 +- .../plugin-external-bundle/src/index.ts | 3 +- .../plugin-external-bundle/test/index.test.ts | 47 +++++++++++ .../externals-loading-webpack-plugin.api.md | 2 + .../src/index.ts | 43 ++++++---- .../filter-duplicate-externals/index.js | 4 +- .../globalObject-customize/index.js | 29 +++++++ .../globalObject-customize/rspack.config.js | 26 +++++++ .../globalObject-customize/test.config.cjs | 7 ++ 15 files changed, 249 insertions(+), 20 deletions(-) create mode 100644 .changeset/wet-fans-sleep.md create mode 100644 packages/webpack/externals-loading-webpack-plugin/test/cases/externals-loading/globalObject-customize/index.js create mode 100644 packages/webpack/externals-loading-webpack-plugin/test/cases/externals-loading/globalObject-customize/rspack.config.js create mode 100644 packages/webpack/externals-loading-webpack-plugin/test/cases/externals-loading/globalObject-customize/test.config.cjs diff --git a/.changeset/wet-fans-sleep.md b/.changeset/wet-fans-sleep.md new file mode 100644 index 0000000000..9f9fa2e90c --- /dev/null +++ b/.changeset/wet-fans-sleep.md @@ -0,0 +1,7 @@ +--- +"@lynx-js/externals-loading-webpack-plugin": patch +"@lynx-js/lynx-bundle-rslib-config": patch +"@lynx-js/external-bundle-rsbuild-plugin": patch +--- + +Add [`globalObject`](https://webpack.js.org/configuration/output/#outputglobalobject) config for external bundle loading, user can config it to `globalThis` for BTS external bundle sharing. diff --git a/examples/react-externals/lynx.config.js b/examples/react-externals/lynx.config.js index 86040fc918..2280877b0e 100644 --- a/examples/react-externals/lynx.config.js +++ b/examples/react-externals/lynx.config.js @@ -109,6 +109,7 @@ export default defineConfig({ async: true, }, }, + globalObject: 'globalThis', }), ], environments: { diff --git a/examples/react-externals/rslib-comp-lib.config.ts b/examples/react-externals/rslib-comp-lib.config.ts index 7a93c001f0..a5181db1d6 100644 --- a/examples/react-externals/rslib-comp-lib.config.ts +++ b/examples/react-externals/rslib-comp-lib.config.ts @@ -41,5 +41,6 @@ export default defineExternalBundleRslibConfig({ 'preact': ['ReactLynx', 'Preact'], }, minify: false, + globalObject: 'globalThis', }, }); diff --git a/examples/react-externals/rslib-reactlynx.config.ts b/examples/react-externals/rslib-reactlynx.config.ts index e29823f389..19c6a3a8a9 100644 --- a/examples/react-externals/rslib-reactlynx.config.ts +++ b/examples/react-externals/rslib-reactlynx.config.ts @@ -14,5 +14,6 @@ export default defineExternalBundleRslibConfig({ output: { cleanDistPath: false, minify: false, + globalObject: 'globalThis', }, }); diff --git a/packages/rspeedy/lynx-bundle-rslib-config/src/externalBundleRslibConfig.ts b/packages/rspeedy/lynx-bundle-rslib-config/src/externalBundleRslibConfig.ts index 12010500e5..c53c963192 100644 --- a/packages/rspeedy/lynx-bundle-rslib-config/src/externalBundleRslibConfig.ts +++ b/packages/rspeedy/lynx-bundle-rslib-config/src/externalBundleRslibConfig.ts @@ -71,6 +71,16 @@ export type LibOutputConfig = Required['output'] export interface OutputConfig extends LibOutputConfig { externals?: Externals + /** + * This option indicates what global object will be used to mount the library. + * + * In Lynx, the library will be mounted to `lynx[Symbol.for("__LYNX_EXTERNAL_GLOBAL__")]` by default. + * + * If you have enabled share js context and want to reuse the library by mounting to the global object, you can set this option to `'globalThis'`. + * + * @default 'lynx' + */ + globalObject?: 'lynx' | 'globalThis' } export interface ExternalBundleLibConfig extends LibConfig { @@ -79,6 +89,7 @@ export interface ExternalBundleLibConfig extends LibConfig { function transformExternals( externals?: Externals, + globalObject?: string, ): Required['externals'] { if (!externals) return {} @@ -88,7 +99,7 @@ function transformExternals( if (!libraryName) return callback() callback(undefined, [ - 'lynx[Symbol.for("__LYNX_EXTERNAL_GLOBAL__")]', + `${globalObject ?? 'lynx'}[Symbol.for("__LYNX_EXTERNAL_GLOBAL__")]`, ...(Array.isArray(libraryName) ? libraryName : [libraryName]), ], 'var') } @@ -177,7 +188,10 @@ export function defineExternalBundleRslibConfig( ...userLibConfig, output: { ...userLibConfig.output, - externals: transformExternals(userLibConfig.output?.externals), + externals: transformExternals( + userLibConfig.output?.externals, + userLibConfig.output?.globalObject, + ), }, }, ), diff --git a/packages/rspeedy/lynx-bundle-rslib-config/test/external-bundle.test.ts b/packages/rspeedy/lynx-bundle-rslib-config/test/external-bundle.test.ts index 301c6d20bc..ba18d87b27 100644 --- a/packages/rspeedy/lynx-bundle-rslib-config/test/external-bundle.test.ts +++ b/packages/rspeedy/lynx-bundle-rslib-config/test/external-bundle.test.ts @@ -218,6 +218,84 @@ describe('debug mode artifacts', () => { }) }) +describe('mount externals library', () => { + it('should mount externals library to lynx by default', async () => { + const fixtureDir = path.join(__dirname, './fixtures/utils-lib') + const rslibConfig = defineExternalBundleRslibConfig({ + source: { + entry: { + utils: path.join(__dirname, './fixtures/utils-lib/index.ts'), + }, + }, + id: 'utils-reactlynx', + output: { + distPath: { + root: path.join(fixtureDir, 'dist'), + }, + externals: { + '@lynx-js/react': ['ReactLynx', 'React'], + }, + minify: false, + }, + plugins: [pluginReactLynx()], + }) + + await build(rslibConfig) + + const decodedResult = await decodeTemplate( + path.join(fixtureDir, 'dist/utils-reactlynx.lynx.bundle'), + ) + expect(Object.keys(decodedResult['custom-sections']).sort()).toEqual([ + 'utils', + 'utils__main-thread', + ]) + expect(decodedResult['custom-sections']['utils']).toContain( + 'lynx[Symbol.for("__LYNX_EXTERNAL_GLOBAL__")].ReactLynx.React', + ) + expect(decodedResult['custom-sections']['utils__main-thread']).toContain( + 'lynx[Symbol.for("__LYNX_EXTERNAL_GLOBAL__")].ReactLynx.React', + ) + }) + it('should mount externals library to globalThis', async () => { + const fixtureDir = path.join(__dirname, './fixtures/utils-lib') + const rslibConfig = defineExternalBundleRslibConfig({ + source: { + entry: { + utils: path.join(__dirname, './fixtures/utils-lib/index.ts'), + }, + }, + id: 'utils-reactlynx', + output: { + distPath: { + root: path.join(fixtureDir, 'dist'), + }, + externals: { + '@lynx-js/react': ['ReactLynx', 'React'], + }, + minify: false, + globalObject: 'globalThis', + }, + plugins: [pluginReactLynx()], + }) + + await build(rslibConfig) + + const decodedResult = await decodeTemplate( + path.join(fixtureDir, 'dist/utils-reactlynx.lynx.bundle'), + ) + expect(Object.keys(decodedResult['custom-sections']).sort()).toEqual([ + 'utils', + 'utils__main-thread', + ]) + expect(decodedResult['custom-sections']['utils']).toContain( + 'lynx[Symbol.for("__LYNX_EXTERNAL_GLOBAL__")].ReactLynx.React', + ) + expect(decodedResult['custom-sections']['utils__main-thread']).toContain( + 'lynx[Symbol.for("__LYNX_EXTERNAL_GLOBAL__")].ReactLynx.React', + ) + }) +}) + describe('pluginReactLynx', () => { const fixtureDir = path.join(__dirname, './fixtures/utils-lib') const distRoot = path.join(fixtureDir, 'dist') diff --git a/packages/rspeedy/plugin-external-bundle/etc/external-bundle-rsbuild-plugin.api.md b/packages/rspeedy/plugin-external-bundle/etc/external-bundle-rsbuild-plugin.api.md index 78fd501a50..4fcc0d0144 100644 --- a/packages/rspeedy/plugin-external-bundle/etc/external-bundle-rsbuild-plugin.api.md +++ b/packages/rspeedy/plugin-external-bundle/etc/external-bundle-rsbuild-plugin.api.md @@ -11,6 +11,6 @@ import type { RsbuildPlugin } from '@rsbuild/core'; export function pluginExternalBundle(options: PluginExternalBundleOptions): RsbuildPlugin; // @public -export type PluginExternalBundleOptions = Pick; +export type PluginExternalBundleOptions = Pick; ``` diff --git a/packages/rspeedy/plugin-external-bundle/src/index.ts b/packages/rspeedy/plugin-external-bundle/src/index.ts index f26c216d90..21d37ef9e2 100644 --- a/packages/rspeedy/plugin-external-bundle/src/index.ts +++ b/packages/rspeedy/plugin-external-bundle/src/index.ts @@ -25,7 +25,7 @@ interface ExposedLayers { */ export type PluginExternalBundleOptions = Pick< ExternalsLoadingPluginOptions, - 'externals' + 'externals' | 'globalObject' > /** @@ -81,6 +81,7 @@ export function pluginExternalBundle( backgroundLayer: LAYERS.BACKGROUND, mainThreadLayer: LAYERS.MAIN_THREAD, externals: options.externals, + globalObject: options.globalObject, }), ) return config diff --git a/packages/rspeedy/plugin-external-bundle/test/index.test.ts b/packages/rspeedy/plugin-external-bundle/test/index.test.ts index 7e22b2f546..95038e00e8 100644 --- a/packages/rspeedy/plugin-external-bundle/test/index.test.ts +++ b/packages/rspeedy/plugin-external-bundle/test/index.test.ts @@ -190,4 +190,51 @@ describe('pluginExternalBundle', () => { }, }) }) + + test('should allow config globalObject', async () => { + const { pluginExternalBundle } = await import('../src/index.js') + + let capturedPlugins: unknown[] = [] + + const rsbuild = await createRsbuild({ + rsbuildConfig: { + source: { + entry: { + main: './fixtures/basic.tsx', + }, + }, + tools: { + rspack(config) { + capturedPlugins = config.plugins || [] + return config + }, + }, + plugins: [ + pluginStubLayers(), + pluginExternalBundle({ + externals: { + lodash: { + url: 'http://lodash.lynx.bundle', + background: { sectionPath: 'background' }, + mainThread: { sectionPath: 'mainThread' }, + }, + }, + globalObject: 'globalThis', + }), + ], + }, + }) + + await rsbuild.inspectConfig() + + const externalBundlePlugin = capturedPlugins.find( + (plugin) => plugin instanceof ExternalsLoadingPlugin, + ) + expect(externalBundlePlugin).toBeDefined() + expect(externalBundlePlugin).toMatchObject({ + options: { + globalObject: 'globalThis', + }, + }) + }) }) diff --git a/packages/webpack/externals-loading-webpack-plugin/etc/externals-loading-webpack-plugin.api.md b/packages/webpack/externals-loading-webpack-plugin/etc/externals-loading-webpack-plugin.api.md index f85c481407..952b9e52f6 100644 --- a/packages/webpack/externals-loading-webpack-plugin/etc/externals-loading-webpack-plugin.api.md +++ b/packages/webpack/externals-loading-webpack-plugin/etc/externals-loading-webpack-plugin.api.md @@ -17,6 +17,8 @@ export class ExternalsLoadingPlugin { export interface ExternalsLoadingPluginOptions { backgroundLayer: string; externals: Record; + // Warning: (tsdoc-undefined-tag) The TSDoc tag "@default" is not defined in this configuration + globalObject?: 'lynx' | 'globalThis' | undefined; mainThreadLayer: string; } diff --git a/packages/webpack/externals-loading-webpack-plugin/src/index.ts b/packages/webpack/externals-loading-webpack-plugin/src/index.ts index f393353f5d..3507882d6e 100644 --- a/packages/webpack/externals-loading-webpack-plugin/src/index.ts +++ b/packages/webpack/externals-loading-webpack-plugin/src/index.ts @@ -80,6 +80,16 @@ export interface ExternalsLoadingPluginOptions { string, ExternalValue >; + /** + * This option indicates what global object will be used to mount the library. + * + * In Lynx, the library will be mounted to `lynx[Symbol.for("__LYNX_EXTERNAL_GLOBAL__")]` by default. + * + * If you have enabled share js context and want to reuse the library by mounting to the global object, you can set this option to `'globalThis'`. + * + * @default 'lynx' + */ + globalObject?: 'lynx' | 'globalThis' | undefined; } /** @@ -211,8 +221,8 @@ export interface LayerOptions { sectionPath: string; } -function getLynxExternalGlobal() { - return `lynx[Symbol.for('__LYNX_EXTERNAL_GLOBAL__')]`; +function getLynxExternalGlobal(globalObject?: string) { + return `${globalObject ?? 'lynx'}[Symbol.for('__LYNX_EXTERNAL_GLOBAL__')]`; } /** @@ -301,7 +311,9 @@ export class ExternalsLoadingPlugin { if (externals.length === 0) { return ''; } - const runtimeGlobalsInit = `${getLynxExternalGlobal()} = {};`; + const runtimeGlobalsInit = `${ + getLynxExternalGlobal(externalsLoadingPluginOptions.globalObject) + } = {};`; const loadExternalFunc = ` function createLoadExternalAsync(handler, sectionPath) { return new Promise((resolve, reject) => { @@ -367,23 +379,24 @@ function createLoadExternalSync(handler, sectionPath, timeout) { `const handler${i} = lynx.fetchBundle(${JSON.stringify(url)}, {});`, ); + const mountVar = `${ + getLynxExternalGlobal( + externalsLoadingPluginOptions.globalObject, + ) + }[${JSON.stringify(libraryNameStr)}]`; if (async) { loadCode.push( - `${getLynxExternalGlobal()}[${ - JSON.stringify(libraryNameStr) - }] = createLoadExternalAsync(handler${i}, ${ + `${mountVar} = ${mountVar} === undefined ? createLoadExternalAsync(handler${i}, ${ JSON.stringify(layerOptions.sectionPath) - });`, + }) : ${mountVar};`, ); continue; } loadCode.push( - `${getLynxExternalGlobal()}[${ - JSON.stringify(libraryNameStr) - }] = createLoadExternalSync(handler${i}, ${ + `${mountVar} = ${mountVar} === undefined ? createLoadExternalSync(handler${i}, ${ JSON.stringify(layerOptions.sectionPath) - }, ${timeout});`, + }, ${timeout}) : ${mountVar};`, ); } @@ -403,7 +416,7 @@ function createLoadExternalSync(handler, sectionPath, timeout) { : (typeof compiler.options.externals === 'undefined' ? [] : [compiler.options.externals])), - this.#genExternalsConfig(), + this.#genExternalsConfig(externalsLoadingPluginOptions.globalObject), ]; }); @@ -438,7 +451,9 @@ function createLoadExternalSync(handler, sectionPath, timeout) { /** * If the external is async, use `promise` external type; otherwise, use `var` external type. */ - #genExternalsConfig(): ( + #genExternalsConfig( + globalObject: ExternalsLoadingPluginOptions['globalObject'], + ): ( data: ExternalItemFunctionData, callback: ( err?: Error, @@ -466,7 +481,7 @@ function createLoadExternalSync(handler, sectionPath, timeout) { return callback( undefined, [ - getLynxExternalGlobal(), + getLynxExternalGlobal(globalObject), ...(Array.isArray(libraryName) ? libraryName : [libraryName]), ], isAsync ? 'promise' : undefined, diff --git a/packages/webpack/externals-loading-webpack-plugin/test/cases/externals-loading/filter-duplicate-externals/index.js b/packages/webpack/externals-loading-webpack-plugin/test/cases/externals-loading/filter-duplicate-externals/index.js index dc21882695..f87aace7aa 100644 --- a/packages/webpack/externals-loading-webpack-plugin/test/cases/externals-loading/filter-duplicate-externals/index.js +++ b/packages/webpack/externals-loading-webpack-plugin/test/cases/externals-loading/filter-duplicate-externals/index.js @@ -19,13 +19,13 @@ it('should filter duplicate externals', async () => { expect( background.split( `lynx[Symbol.for('__LYNX_EXTERNAL_GLOBAL__')]["Foo"]` - + ' = createLoadExternalSync(', + + ' = ', ).length - 1, ).toBe(1); expect( mainThread.split( `lynx[Symbol.for('__LYNX_EXTERNAL_GLOBAL__')]["Foo"] ` - + '= createLoadExternalSync(', + + '= ', ).length - 1, ).toBe(1); }); diff --git a/packages/webpack/externals-loading-webpack-plugin/test/cases/externals-loading/globalObject-customize/index.js b/packages/webpack/externals-loading-webpack-plugin/test/cases/externals-loading/globalObject-customize/index.js new file mode 100644 index 0000000000..d97b3a9dfe --- /dev/null +++ b/packages/webpack/externals-loading-webpack-plugin/test/cases/externals-loading/globalObject-customize/index.js @@ -0,0 +1,29 @@ +import x from 'foo'; + +console.info(x); + +it('should mount to externals library to globalThis', async () => { + const fs = await import('node:fs'); + const path = await import('node:path'); + + const background = fs.readFileSync( + path.resolve(__dirname, 'main:background.js'), + 'utf-8', + ); + const mainThread = fs.readFileSync( + path.resolve(__dirname, 'main:main-thread.js'), + 'utf-8', + ); + expect( + background.split( + `globalThis[Symbol.for('__LYNX_EXTERNAL_GLOBAL__')]["Foo"]` + + ' = ', + ).length - 1, + ).toBe(1); + expect( + mainThread.split( + `globalThis[Symbol.for('__LYNX_EXTERNAL_GLOBAL__')]["Foo"] ` + + '= ', + ).length - 1, + ).toBe(1); +}); diff --git a/packages/webpack/externals-loading-webpack-plugin/test/cases/externals-loading/globalObject-customize/rspack.config.js b/packages/webpack/externals-loading-webpack-plugin/test/cases/externals-loading/globalObject-customize/rspack.config.js new file mode 100644 index 0000000000..75212b7b60 --- /dev/null +++ b/packages/webpack/externals-loading-webpack-plugin/test/cases/externals-loading/globalObject-customize/rspack.config.js @@ -0,0 +1,26 @@ +import { createConfig } from '../../../helpers/create-config.js'; + +/** @type {import('@rspack/core').Configuration} */ +export default { + context: __dirname, + ...createConfig( + { + backgroundLayer: 'background', + mainThreadLayer: 'main-thread', + externals: { + 'foo': { + libraryName: 'Foo', + url: 'foo', + async: false, + background: { + sectionPath: 'background', + }, + mainThread: { + sectionPath: 'mainThread', + }, + }, + }, + globalObject: 'globalThis', + }, + ), +}; diff --git a/packages/webpack/externals-loading-webpack-plugin/test/cases/externals-loading/globalObject-customize/test.config.cjs b/packages/webpack/externals-loading-webpack-plugin/test/cases/externals-loading/globalObject-customize/test.config.cjs new file mode 100644 index 0000000000..2fa53abe09 --- /dev/null +++ b/packages/webpack/externals-loading-webpack-plugin/test/cases/externals-loading/globalObject-customize/test.config.cjs @@ -0,0 +1,7 @@ +/** @type {import("@lynx-js/test-tools").TConfigCaseConfig} */ +module.exports = { + bundlePath: [ + 'main:main-thread.js', + 'main:background.js', + ], +}; From f38c9003d5e8942c77bd8bb421ac1fb2201cc1aa Mon Sep 17 00:00:00 2001 From: Yiming Li Date: Fri, 23 Jan 2026 17:57:38 +0800 Subject: [PATCH 2/2] fix: cr of rabbit --- .changeset/wet-fans-sleep.md | 2 +- .../test/external-bundle.test.ts | 11 +++++++---- .../externals-loading-webpack-plugin/src/index.ts | 8 +++++--- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.changeset/wet-fans-sleep.md b/.changeset/wet-fans-sleep.md index 9f9fa2e90c..ef863e04f0 100644 --- a/.changeset/wet-fans-sleep.md +++ b/.changeset/wet-fans-sleep.md @@ -4,4 +4,4 @@ "@lynx-js/external-bundle-rsbuild-plugin": patch --- -Add [`globalObject`](https://webpack.js.org/configuration/output/#outputglobalobject) config for external bundle loading, user can config it to `globalThis` for BTS external bundle sharing. +Add [`globalObject`](https://webpack.js.org/configuration/output/#outputglobalobject) config for external bundle loading, user can configure it to `globalThis` for BTS external bundle sharing. diff --git a/packages/rspeedy/lynx-bundle-rslib-config/test/external-bundle.test.ts b/packages/rspeedy/lynx-bundle-rslib-config/test/external-bundle.test.ts index ba18d87b27..e964b14d70 100644 --- a/packages/rspeedy/lynx-bundle-rslib-config/test/external-bundle.test.ts +++ b/packages/rspeedy/lynx-bundle-rslib-config/test/external-bundle.test.ts @@ -264,7 +264,7 @@ describe('mount externals library', () => { utils: path.join(__dirname, './fixtures/utils-lib/index.ts'), }, }, - id: 'utils-reactlynx', + id: 'utils-reactlynx-globalThis', output: { distPath: { root: path.join(fixtureDir, 'dist'), @@ -281,17 +281,20 @@ describe('mount externals library', () => { await build(rslibConfig) const decodedResult = await decodeTemplate( - path.join(fixtureDir, 'dist/utils-reactlynx.lynx.bundle'), + path.join( + fixtureDir, + 'dist/utils-reactlynx-globalThis.lynx.bundle', + ), ) expect(Object.keys(decodedResult['custom-sections']).sort()).toEqual([ 'utils', 'utils__main-thread', ]) expect(decodedResult['custom-sections']['utils']).toContain( - 'lynx[Symbol.for("__LYNX_EXTERNAL_GLOBAL__")].ReactLynx.React', + 'globalThis[Symbol.for("__LYNX_EXTERNAL_GLOBAL__")].ReactLynx.React', ) expect(decodedResult['custom-sections']['utils__main-thread']).toContain( - 'lynx[Symbol.for("__LYNX_EXTERNAL_GLOBAL__")].ReactLynx.React', + 'globalThis[Symbol.for("__LYNX_EXTERNAL_GLOBAL__")].ReactLynx.React', ) }) }) diff --git a/packages/webpack/externals-loading-webpack-plugin/src/index.ts b/packages/webpack/externals-loading-webpack-plugin/src/index.ts index 3507882d6e..782ec78ec7 100644 --- a/packages/webpack/externals-loading-webpack-plugin/src/index.ts +++ b/packages/webpack/externals-loading-webpack-plugin/src/index.ts @@ -311,9 +311,11 @@ export class ExternalsLoadingPlugin { if (externals.length === 0) { return ''; } - const runtimeGlobalsInit = `${ - getLynxExternalGlobal(externalsLoadingPluginOptions.globalObject) - } = {};`; + const lynxExternalGlobal = getLynxExternalGlobal( + externalsLoadingPluginOptions.globalObject, + ); + const runtimeGlobalsInit = + `${lynxExternalGlobal} = ${lynxExternalGlobal} === undefined ? {} : ${lynxExternalGlobal};`; const loadExternalFunc = ` function createLoadExternalAsync(handler, sectionPath) { return new Promise((resolve, reject) => {