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/wet-fans-sleep.md
Original file line number Diff line number Diff line change
@@ -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 configure it to `globalThis` for BTS external bundle sharing.
Comment thread
upupming marked this conversation as resolved.
1 change: 1 addition & 0 deletions examples/react-externals/lynx.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export default defineConfig({
async: true,
},
},
globalObject: 'globalThis',
}),
],
environments: {
Expand Down
1 change: 1 addition & 0 deletions examples/react-externals/rslib-comp-lib.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,6 @@ export default defineExternalBundleRslibConfig({
'preact': ['ReactLynx', 'Preact'],
},
minify: false,
globalObject: 'globalThis',
},
});
1 change: 1 addition & 0 deletions examples/react-externals/rslib-reactlynx.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ export default defineExternalBundleRslibConfig({
output: {
cleanDistPath: false,
minify: false,
globalObject: 'globalThis',
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,16 @@ export type LibOutputConfig = Required<LibConfig>['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 {
Expand All @@ -79,6 +89,7 @@ export interface ExternalBundleLibConfig extends LibConfig {

function transformExternals(
externals?: Externals,
globalObject?: string,
): Required<LibOutputConfig>['externals'] {
if (!externals) return {}

Expand All @@ -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')
}
Expand Down Expand Up @@ -177,7 +188,10 @@ export function defineExternalBundleRslibConfig(
...userLibConfig,
output: {
...userLibConfig.output,
externals: transformExternals(userLibConfig.output?.externals),
externals: transformExternals(
userLibConfig.output?.externals,
userLibConfig.output?.globalObject,
),
},
},
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,87 @@ 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-globalThis',
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-globalThis.lynx.bundle',
),
)
expect(Object.keys(decodedResult['custom-sections']).sort()).toEqual([
'utils',
'utils__main-thread',
])
expect(decodedResult['custom-sections']['utils']).toContain(
'globalThis[Symbol.for("__LYNX_EXTERNAL_GLOBAL__")].ReactLynx.React',
)
expect(decodedResult['custom-sections']['utils__main-thread']).toContain(
'globalThis[Symbol.for("__LYNX_EXTERNAL_GLOBAL__")].ReactLynx.React',
)
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})

describe('pluginReactLynx', () => {
const fixtureDir = path.join(__dirname, './fixtures/utils-lib')
const distRoot = path.join(fixtureDir, 'dist')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ import type { RsbuildPlugin } from '@rsbuild/core';
export function pluginExternalBundle(options: PluginExternalBundleOptions): RsbuildPlugin;

// @public
export type PluginExternalBundleOptions = Pick<ExternalsLoadingPluginOptions, 'externals'>;
export type PluginExternalBundleOptions = Pick<ExternalsLoadingPluginOptions, 'externals' | 'globalObject'>;

```
3 changes: 2 additions & 1 deletion packages/rspeedy/plugin-external-bundle/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ interface ExposedLayers {
*/
export type PluginExternalBundleOptions = Pick<
ExternalsLoadingPluginOptions,
'externals'
'externals' | 'globalObject'
>

/**
Expand Down Expand Up @@ -81,6 +81,7 @@ export function pluginExternalBundle(
backgroundLayer: LAYERS.BACKGROUND,
mainThreadLayer: LAYERS.MAIN_THREAD,
externals: options.externals,
globalObject: options.globalObject,
}),
)
return config
Expand Down
47 changes: 47 additions & 0 deletions packages/rspeedy/plugin-external-bundle/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export class ExternalsLoadingPlugin {
export interface ExternalsLoadingPluginOptions {
backgroundLayer: string;
externals: Record<string, ExternalValue>;
// Warning: (tsdoc-undefined-tag) The TSDoc tag "@default" is not defined in this configuration
globalObject?: 'lynx' | 'globalThis' | undefined;
mainThreadLayer: string;
}

Expand Down
45 changes: 31 additions & 14 deletions packages/webpack/externals-loading-webpack-plugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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__')]`;
}

/**
Expand Down Expand Up @@ -301,7 +311,11 @@ export class ExternalsLoadingPlugin {
if (externals.length === 0) {
return '';
}
const runtimeGlobalsInit = `${getLynxExternalGlobal()} = {};`;
const lynxExternalGlobal = getLynxExternalGlobal(
externalsLoadingPluginOptions.globalObject,
);
const runtimeGlobalsInit =
`${lynxExternalGlobal} = ${lynxExternalGlobal} === undefined ? {} : ${lynxExternalGlobal};`;
const loadExternalFunc = `
function createLoadExternalAsync(handler, sectionPath) {
return new Promise((resolve, reject) => {
Expand Down Expand Up @@ -367,23 +381,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};`,
);
}

Expand All @@ -403,7 +418,7 @@ function createLoadExternalSync(handler, sectionPath, timeout) {
: (typeof compiler.options.externals === 'undefined'
? []
: [compiler.options.externals])),
this.#genExternalsConfig(),
this.#genExternalsConfig(externalsLoadingPluginOptions.globalObject),
];
});

Expand Down Expand Up @@ -438,7 +453,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,
Expand Down Expand Up @@ -466,7 +483,7 @@ function createLoadExternalSync(handler, sectionPath, timeout) {
return callback(
undefined,
[
getLynxExternalGlobal(),
getLynxExternalGlobal(globalObject),
...(Array.isArray(libraryName) ? libraryName : [libraryName]),
],
isAsync ? 'promise' : undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Original file line number Diff line number Diff line change
@@ -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);
});
Loading
Loading