diff --git a/examples/react-externals/README.md b/examples/react-externals/README.md
new file mode 100644
index 0000000000..064e5e5c8c
--- /dev/null
+++ b/examples/react-externals/README.md
@@ -0,0 +1,16 @@
+# @lynx-js/example-react-externals
+
+In this example, we show:
+
+- Use `@lynx-js/lynx-bundle-rslib-config` to bundle ReactLynx runtime to a separate Lynx bundle.
+- Use `@lynx-js/lynx-bundle-rslib-config` to bundle a simple ReactLynx component library to a separate Lynx bundle.
+- Use `@lynx-js/externals-loading-webpack-plugin` to load ReactLynx runtime (sync) and component bundle (async).
+
+## Usage
+
+```bash
+pnpm build:reactlynx
+pnpm build:comp-lib
+pnpx http-server -p 8080 dist
+EXTERNAL_BUNDLE_PREFIX=http://${YOUR_IP_HERE}$:8080 pnpm dev
+```
diff --git a/examples/react-externals/external-bundle/CompLib.tsx b/examples/react-externals/external-bundle/CompLib.tsx
new file mode 100644
index 0000000000..3c7068933a
--- /dev/null
+++ b/examples/react-externals/external-bundle/CompLib.tsx
@@ -0,0 +1 @@
+export { App } from '../src/App.js';
diff --git a/examples/react-externals/external-bundle/ReactLynx.ts b/examples/react-externals/external-bundle/ReactLynx.ts
new file mode 100644
index 0000000000..401d2ca086
--- /dev/null
+++ b/examples/react-externals/external-bundle/ReactLynx.ts
@@ -0,0 +1,10 @@
+export * as React from '@lynx-js/react';
+export * as ReactInternal from '@lynx-js/react/internal';
+export * as ReactLazyImport from '@lynx-js/react/experimental/lazy/import';
+export * as ReactLegacyRuntime from '@lynx-js/react/legacy-react-runtime';
+export * as ReactComponents from '@lynx-js/react/runtime-components';
+export * as ReactWorkletRuntime from '@lynx-js/react/worklet-runtime/bindings';
+export * as ReactDebug from '@lynx-js/react/debug';
+// @ts-expect-error preact is aliased by rspeedy
+// eslint-disable-next-line import/no-unresolved
+export * as Preact from 'preact';
diff --git a/examples/react-externals/lynx.config.js b/examples/react-externals/lynx.config.js
new file mode 100644
index 0000000000..98efc148f1
--- /dev/null
+++ b/examples/react-externals/lynx.config.js
@@ -0,0 +1,108 @@
+import { ExternalsLoadingPlugin } from '@lynx-js/externals-loading-webpack-plugin';
+import { pluginQRCode } from '@lynx-js/qrcode-rsbuild-plugin';
+import { LAYERS, pluginReactLynx } from '@lynx-js/react-rsbuild-plugin';
+import { defineConfig } from '@lynx-js/rspeedy';
+
+const enableBundleAnalysis = !!process.env['RSPEEDY_BUNDLE_ANALYSIS'];
+const EXTERNAL_BUNDLE_PREFIX = process.env['EXTERNAL_BUNDLE_PREFIX'] || '';
+
+export default defineConfig({
+ tools: {
+ rspack: {
+ plugins: [
+ new ExternalsLoadingPlugin({
+ mainThreadChunks: ['main__main-thread'],
+ backgroundChunks: ['main'],
+ mainThreadLayer: LAYERS.MAIN_THREAD,
+ backgroundLayer: LAYERS.BACKGROUND,
+ externals: {
+ '@lynx-js/react': {
+ libraryName: ['ReactLynx', 'React'],
+ url: `${EXTERNAL_BUNDLE_PREFIX}/react.lynx.bundle`,
+ background: { sectionPath: 'ReactLynx' },
+ mainThread: { sectionPath: 'ReactLynx__main-thread' },
+ async: false,
+ },
+ '@lynx-js/react/internal': {
+ libraryName: ['ReactLynx', 'ReactInternal'],
+ url: `${EXTERNAL_BUNDLE_PREFIX}/react.lynx.bundle`,
+ background: { sectionPath: 'ReactLynx' },
+ mainThread: { sectionPath: 'ReactLynx__main-thread' },
+ async: false,
+ },
+ '@lynx-js/react/experimental/lazy/import': {
+ libraryName: ['ReactLynx', 'ReactLazyImport'],
+ url: `${EXTERNAL_BUNDLE_PREFIX}/react.lynx.bundle`,
+ background: { sectionPath: 'ReactLynx' },
+ mainThread: { sectionPath: 'ReactLynx__main-thread' },
+ async: false,
+ },
+ '@lynx-js/react/legacy-react-runtime': {
+ libraryName: ['ReactLynx', 'ReactLegacyRuntime'],
+ url: `${EXTERNAL_BUNDLE_PREFIX}/react.lynx.bundle`,
+ background: { sectionPath: 'ReactLynx' },
+ mainThread: { sectionPath: 'ReactLynx__main-thread' },
+ async: false,
+ },
+ '@lynx-js/react/runtime-components': {
+ libraryName: ['ReactLynx', 'ReactComponents'],
+ url: `${EXTERNAL_BUNDLE_PREFIX}/react.lynx.bundle`,
+ background: { sectionPath: 'ReactLynx' },
+ mainThread: { sectionPath: 'ReactLynx__main-thread' },
+ async: false,
+ },
+ '@lynx-js/react/worklet-runtime/bindings': {
+ libraryName: ['ReactLynx', 'ReactWorkletRuntime'],
+ url: `${EXTERNAL_BUNDLE_PREFIX}/react.lynx.bundle`,
+ background: { sectionPath: 'ReactLynx' },
+ mainThread: { sectionPath: 'ReactLynx__main-thread' },
+ async: false,
+ },
+ '@lynx-js/react/debug': {
+ libraryName: ['ReactLynx', 'ReactDebug'],
+ url: `${EXTERNAL_BUNDLE_PREFIX}/react.lynx.bundle`,
+ background: { sectionPath: 'ReactLynx' },
+ mainThread: { sectionPath: 'ReactLynx__main-thread' },
+ async: false,
+ },
+ preact: {
+ libraryName: ['ReactLynx', 'Preact'],
+ url: `${EXTERNAL_BUNDLE_PREFIX}/react.lynx.bundle`,
+ background: { sectionPath: 'ReactLynx' },
+ mainThread: { sectionPath: 'ReactLynx__main-thread' },
+ async: false,
+ },
+ './App.js': {
+ libraryName: 'CompLib',
+ url: `${EXTERNAL_BUNDLE_PREFIX}/comp-lib.lynx.bundle`,
+ background: { sectionPath: 'CompLib' },
+ mainThread: { sectionPath: 'CompLib__main-thread' },
+ async: false,
+ },
+ },
+ }),
+ ],
+ },
+ },
+ plugins: [
+ pluginReactLynx(),
+ pluginQRCode({
+ schema(url) {
+ // We use `?fullscreen=true` to open the page in LynxExplorer in full screen mode
+ return `${url}?fullscreen=true`;
+ },
+ }),
+ ],
+ environments: {
+ web: {},
+ lynx: {
+ performance: {
+ profile: enableBundleAnalysis,
+ },
+ },
+ },
+ output: {
+ filenameHash: 'contenthash:8',
+ cleanDistPath: false,
+ },
+});
diff --git a/examples/react-externals/package.json b/examples/react-externals/package.json
new file mode 100644
index 0000000000..ccf2f8e918
--- /dev/null
+++ b/examples/react-externals/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "@lynx-js/example-react-externals",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "build": "rspeedy build",
+ "build:comp-lib": "rslib build --config rslib-comp-lib.config.ts",
+ "build:reactlynx": "rslib build --config rslib-reactlynx.config.ts",
+ "dev": "rspeedy dev",
+ "test:type": "vitest --typecheck.only"
+ },
+ "dependencies": {
+ "@lynx-js/react": "workspace:*"
+ },
+ "devDependencies": {
+ "@lynx-js/externals-loading-webpack-plugin": "workspace:*",
+ "@lynx-js/lynx-bundle-rslib-config": "workspace:*",
+ "@lynx-js/preact-devtools": "^5.0.1-cf9aef5",
+ "@lynx-js/qrcode-rsbuild-plugin": "workspace:*",
+ "@lynx-js/react-rsbuild-plugin": "workspace:*",
+ "@lynx-js/react-webpack-plugin": "workspace:*",
+ "@lynx-js/rspeedy": "workspace:*",
+ "@lynx-js/types": "3.4.11",
+ "@types/react": "^18.3.25"
+ }
+}
diff --git a/examples/react-externals/rslib-comp-lib.config.ts b/examples/react-externals/rslib-comp-lib.config.ts
new file mode 100644
index 0000000000..a9afe92215
--- /dev/null
+++ b/examples/react-externals/rslib-comp-lib.config.ts
@@ -0,0 +1,81 @@
+import { createRequire } from 'node:module';
+import path from 'node:path';
+
+import {
+ LAYERS,
+ defineExternalBundleRslibConfig,
+} from '@lynx-js/lynx-bundle-rslib-config';
+import { ReactWebpackPlugin } from '@lynx-js/react-webpack-plugin';
+
+const require = createRequire(import.meta.url);
+const reactLynxDir = path.dirname(
+ require.resolve('@lynx-js/react/package.json'),
+);
+const preactDir = path.dirname(
+ require.resolve('preact/package.json', { paths: [reactLynxDir] }),
+);
+
+export default defineExternalBundleRslibConfig({
+ id: 'comp-lib',
+ tools: {
+ rspack: {
+ module: {
+ rules: [
+ {
+ test: /\.(?:js|jsx|mjs|cjs|ts|tsx|mts|cts)$/,
+ issuerLayer: LAYERS.BACKGROUND,
+ loader: ReactWebpackPlugin.loaders.BACKGROUND,
+ },
+ {
+ test: /\.(?:js|jsx|mjs|cjs|ts|tsx|mts|cts)$/,
+ issuerLayer: LAYERS.MAIN_THREAD,
+ loader: ReactWebpackPlugin.loaders.MAIN_THREAD,
+ },
+ ],
+ },
+ },
+ },
+ source: {
+ entry: {
+ 'CompLib': './external-bundle/CompLib.tsx',
+ },
+ define: {
+ '__DEV__': 'false',
+ 'process.env.NODE_ENV': '"production"',
+ '__FIRST_SCREEN_SYNC_TIMING__': '"immediately"',
+ '__ENABLE_SSR__': 'false',
+ '__PROFILE__': 'false',
+ '__EXTRACT_STR__': 'false',
+ },
+ },
+ resolve: {
+ alias: {
+ 'react': preactDir,
+ 'preact': preactDir,
+ },
+ },
+ output: {
+ cleanDistPath: false,
+ dataUriLimit: Number.POSITIVE_INFINITY,
+ externals: {
+ '@lynx-js/react': ['ReactLynx', 'React'],
+ '@lynx-js/react/internal': ['ReactLynx', 'ReactInternal'],
+ '@lynx-js/react/experimental/lazy/import': [
+ 'ReactLynx',
+ 'ReactLazyImport',
+ ],
+ '@lynx-js/react/legacy-react-runtime': [
+ 'ReactLynx',
+ 'ReactLegacyRuntime',
+ ],
+ '@lynx-js/react/runtime-components': ['ReactLynx', 'ReactComponents'],
+ '@lynx-js/react/worklet-runtime/bindings': [
+ 'ReactLynx',
+ 'ReactWorkletRuntime',
+ ],
+ '@lynx-js/react/debug': ['ReactLynx', 'ReactDebug'],
+ 'preact': ['ReactLynx', 'Preact'],
+ },
+ minify: false,
+ },
+});
diff --git a/examples/react-externals/rslib-reactlynx.config.ts b/examples/react-externals/rslib-reactlynx.config.ts
new file mode 100644
index 0000000000..c16f9ce09c
--- /dev/null
+++ b/examples/react-externals/rslib-reactlynx.config.ts
@@ -0,0 +1,59 @@
+import { createRequire } from 'node:module';
+import path from 'node:path';
+
+import {
+ LAYERS,
+ defineExternalBundleRslibConfig,
+} from '@lynx-js/lynx-bundle-rslib-config';
+import { ReactWebpackPlugin } from '@lynx-js/react-webpack-plugin';
+
+const require = createRequire(import.meta.url);
+const reactLynxDir = path.dirname(
+ require.resolve('@lynx-js/react/package.json'),
+);
+const preactDir = path.dirname(
+ require.resolve('preact/package.json', { paths: [reactLynxDir] }),
+);
+
+export default defineExternalBundleRslibConfig({
+ id: 'react',
+ tools: {
+ rspack: {
+ module: {
+ rules: [
+ {
+ issuerLayer: LAYERS.BACKGROUND,
+ loader: ReactWebpackPlugin.loaders.BACKGROUND,
+ },
+ {
+ issuerLayer: LAYERS.MAIN_THREAD,
+ loader: ReactWebpackPlugin.loaders.MAIN_THREAD,
+ },
+ ],
+ },
+ },
+ },
+ source: {
+ entry: {
+ 'ReactLynx': './external-bundle/ReactLynx.ts',
+ },
+ define: {
+ '__DEV__': 'false',
+ 'process.env.NODE_ENV': '"production"',
+ '__FIRST_SCREEN_SYNC_TIMING__': '"immediately"',
+ '__ENABLE_SSR__': 'false',
+ '__PROFILE__': 'false',
+ '__EXTRACT_STR__': 'false',
+ },
+ },
+ resolve: {
+ alias: {
+ 'react': preactDir,
+ 'preact': preactDir,
+ },
+ },
+ output: {
+ cleanDistPath: false,
+ minify: false,
+ },
+});
diff --git a/examples/react-externals/src/App.css b/examples/react-externals/src/App.css
new file mode 100644
index 0000000000..650e423282
--- /dev/null
+++ b/examples/react-externals/src/App.css
@@ -0,0 +1,119 @@
+:root {
+ background-color: #000;
+ --color-text: #fff;
+}
+
+.Background {
+ position: fixed;
+ background: radial-gradient(
+ 71.43% 62.3% at 46.43% 36.43%,
+ rgba(18, 229, 229, 0) 15%,
+ rgba(239, 155, 255, 0.3) 56.35%,
+ #ff6448 100%
+ );
+ box-shadow: 0px 12.93px 28.74px 0px #ffd28db2 inset;
+ border-radius: 50%;
+ width: 200vw;
+ height: 200vw;
+ top: -60vw;
+ left: -14.27vw;
+ transform: rotate(15.25deg);
+}
+
+.App {
+ position: relative;
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+}
+
+text {
+ color: var(--color-text);
+}
+
+.Banner {
+ flex: 5;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ z-index: 100;
+}
+
+.Logo {
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 8px;
+}
+
+.Logo--react {
+ width: 100px;
+ height: 100px;
+ animation: Logo--spin infinite 20s linear;
+}
+
+.Logo--lynx {
+ width: 100px;
+ height: 100px;
+ animation: Logo--shake infinite 0.5s ease;
+}
+
+@keyframes Logo--spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes Logo--shake {
+ 0% {
+ transform: scale(1);
+ }
+ 50% {
+ transform: scale(0.9);
+ }
+ 100% {
+ transform: scale(1);
+ }
+}
+
+.Content {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+}
+
+.Arrow {
+ width: 24px;
+ height: 24px;
+}
+
+.Title {
+ font-size: 36px;
+ font-weight: 700;
+}
+
+.Subtitle {
+ font-style: italic;
+ font-size: 22px;
+ font-weight: 600;
+ margin-bottom: 8px;
+}
+
+.Description {
+ font-size: 20px;
+ color: rgba(255, 255, 255, 0.85);
+ margin: 15rpx;
+}
+
+.Hint {
+ font-size: 12px;
+ margin: 5px;
+ color: rgba(255, 255, 255, 0.65);
+}
diff --git a/examples/react-externals/src/App.tsx b/examples/react-externals/src/App.tsx
new file mode 100644
index 0000000000..5da30edede
--- /dev/null
+++ b/examples/react-externals/src/App.tsx
@@ -0,0 +1,52 @@
+import { useCallback, useEffect, useState } from '@lynx-js/react';
+
+import './App.css';
+import arrow from './assets/arrow.png';
+import lynxLogo from './assets/lynx-logo.png';
+import reactLynxLogo from './assets/react-logo.png';
+
+export function App() {
+ const [alterLogo, setAlterLogo] = useState(false);
+
+ useEffect(() => {
+ console.info('Hello, ReactLynx');
+ }, []);
+
+ const onTap = useCallback(() => {
+ 'background-only';
+ setAlterLogo(prevAlterLogo => !prevAlterLogo);
+ }, []);
+
+ return (
+
+
+
+
+
+ {alterLogo
+ ?
+ : }
+
+ React
+ on Lynx
+
+
+
+ Tap the logo and have fun!
+
+ Edit
+ {' src/App.tsx '}
+
+ to see updates!
+
+
+
+
+
+ );
+}
diff --git a/examples/react-externals/src/assets/arrow.png b/examples/react-externals/src/assets/arrow.png
new file mode 100644
index 0000000000..435c8ad4d1
Binary files /dev/null and b/examples/react-externals/src/assets/arrow.png differ
diff --git a/examples/react-externals/src/assets/lynx-logo.png b/examples/react-externals/src/assets/lynx-logo.png
new file mode 100644
index 0000000000..fe44bf0e3c
Binary files /dev/null and b/examples/react-externals/src/assets/lynx-logo.png differ
diff --git a/examples/react-externals/src/assets/react-logo.png b/examples/react-externals/src/assets/react-logo.png
new file mode 100644
index 0000000000..4ad12a6b55
Binary files /dev/null and b/examples/react-externals/src/assets/react-logo.png differ
diff --git a/examples/react-externals/src/index.tsx b/examples/react-externals/src/index.tsx
new file mode 100644
index 0000000000..d0d893b024
--- /dev/null
+++ b/examples/react-externals/src/index.tsx
@@ -0,0 +1,17 @@
+import '@lynx-js/react/debug';
+import { root } from '@lynx-js/react';
+
+import { App } from './App.js';
+
+// We have to manually import the css now
+// TODO: load css from external bundle
+// when it is supported in Lynx engine
+import './App.css';
+
+root.render(
+ ,
+);
+
+if (import.meta.webpackHot) {
+ import.meta.webpackHot.accept();
+}
diff --git a/examples/react-externals/src/rspeedy-env.d.ts b/examples/react-externals/src/rspeedy-env.d.ts
new file mode 100644
index 0000000000..1c813a68b0
--- /dev/null
+++ b/examples/react-externals/src/rspeedy-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/examples/react-externals/tsconfig.json b/examples/react-externals/tsconfig.json
new file mode 100644
index 0000000000..8f6d4de332
--- /dev/null
+++ b/examples/react-externals/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "jsxImportSource": "@lynx-js/react",
+ "noEmit": true,
+
+ "allowJs": true,
+ "checkJs": true,
+ "isolatedDeclarations": false,
+ },
+ "include": ["src", "external-bundle", "lynx.config.js", "rslib-comp-lib.config.ts", "rslib-reactlynx.config.ts"],
+ "references": [
+ { "path": "../../packages/react/tsconfig.json" },
+ { "path": "../../packages/rspeedy/core/tsconfig.build.json" },
+ { "path": "../../packages/rspeedy/plugin-qrcode/tsconfig.build.json" },
+ { "path": "../../packages/rspeedy/plugin-react/tsconfig.build.json" },
+ ],
+}
diff --git a/packages/rspeedy/lynx-bundle-rslib-config/src/externalBundleRslibConfig.ts b/packages/rspeedy/lynx-bundle-rslib-config/src/externalBundleRslibConfig.ts
index 81dd438f1e..67d1c3a02e 100644
--- a/packages/rspeedy/lynx-bundle-rslib-config/src/externalBundleRslibConfig.ts
+++ b/packages/rspeedy/lynx-bundle-rslib-config/src/externalBundleRslibConfig.ts
@@ -75,6 +75,45 @@ export const defaultExternalBundleLibConfig: LibConfig = {
},
}
+type Externals = Record
+
+type LibOutputConfig = Required['output']
+
+interface OutputConfig extends LibOutputConfig {
+ externals?: Externals
+}
+
+interface ExternalBundleLibConfig extends LibConfig {
+ output?: OutputConfig
+}
+
+function transformExternals(
+ externals?: Externals,
+): Required['externals'] {
+ if (!externals) return {}
+
+ return function({ request, contextInfo }, callback) {
+ if (!request) return callback()
+ const libraryName = externals[request]
+ if (!libraryName) return callback()
+
+ if (contextInfo?.issuerLayer === LAYERS.MAIN_THREAD) {
+ callback(undefined, [
+ 'globalThis',
+ 'lynx_ex',
+ ...(Array.isArray(libraryName) ? libraryName : [libraryName]),
+ ], 'var')
+ } else {
+ callback(undefined, [
+ 'lynxCoreInject',
+ 'tt',
+ 'lynx_ex',
+ ...(Array.isArray(libraryName) ? libraryName : [libraryName]),
+ ], 'var')
+ }
+ }
+}
+
/**
* Get the rslib config for building Lynx external bundles.
*
@@ -146,7 +185,7 @@ export const defaultExternalBundleLibConfig: LibConfig = {
* Then you can use `lynx.loadScript('utils', { bundleName: 'utils-lib-bundle-url' })` in background thread and `lynx.loadScript('utils__main-thread', { bundleName: 'utils-lib-bundle-url' })` in main-thread.
*/
export function defineExternalBundleRslibConfig(
- userLibConfig: LibConfig,
+ userLibConfig: ExternalBundleLibConfig,
encodeOptions: EncodeOptions = {},
): RslibConfig {
return {
@@ -154,7 +193,13 @@ export function defineExternalBundleRslibConfig(
// eslint-disable-next-line import/namespace
rsbuild.mergeRsbuildConfig(
defaultExternalBundleLibConfig,
- userLibConfig,
+ {
+ ...userLibConfig,
+ output: {
+ ...userLibConfig.output,
+ externals: transformExternals(userLibConfig.output?.externals),
+ },
+ },
),
],
plugins: [
diff --git a/packages/webpack/externals-loading-webpack-plugin/src/index.ts b/packages/webpack/externals-loading-webpack-plugin/src/index.ts
index 3926da1903..9f4d635bf7 100644
--- a/packages/webpack/externals-loading-webpack-plugin/src/index.ts
+++ b/packages/webpack/externals-loading-webpack-plugin/src/index.ts
@@ -144,11 +144,32 @@ export interface ExternalsLoadingPluginOptions {
* }
* ```
*
+ * You can pass an array to specify subpath of the external. Same as https://webpack.js.org/configuration/externals/#string-1. For example:
+ *
+ * ```js
+ * ExternalsLoadingPlugin({
+ * externals: {
+ * preact: {
+ * libraryName: ['ReactLynx', 'Preact'],
+ * url: '……',
+ * },
+ * }
+ * })
+ * ```
+ *
+ * Will generate the following webpack externals config:
+ *
+ * ```js
+ * externals: {
+ * preact: '__webpack_require__.lynx_ex.ReactLynx.Preact',
+ * }
+ * ```
+ *
* @defaultValue `undefined`
*
* @example `Lodash`
*/
- libraryName?: string;
+ libraryName?: string | string[];
/**
* Whether the source should be loaded asynchronously or not.
@@ -193,6 +214,13 @@ export interface LayerOptions {
sectionPath: string;
}
+function getLynxExternalGlobal(layer: 'background' | 'mainThread') {
+ // We do not use `globalThis` in BTS to avoid issues when sharing js context is enabled
+ return `${
+ layer === 'background' ? 'lynxCoreInject.tt.lynx_ex' : 'globalThis.lynx_ex'
+ }`;
+}
+
/**
* The webpack plugin to load lynx external bundles.
*
@@ -228,10 +256,6 @@ export interface LayerOptions {
* @public
*/
export class ExternalsLoadingPlugin {
- static RuntimeGlobals = {
- lynxExternals: '__webpack_require__.lynx_ex',
- };
-
constructor(private options: ExternalsLoadingPluginOptions) {}
apply(compiler: Compiler): void {
@@ -279,7 +303,7 @@ export class ExternalsLoadingPlugin {
const syncLoadCode: string[] = [];
// filter duplicate externals by libraryName or package name to avoid loading the same external multiple times. We keep the last one.
const externalsMap = new Map<
- string,
+ string | string[],
ExternalsLoadingPluginOptions['externals'][string]
>();
for (
@@ -294,8 +318,7 @@ export class ExternalsLoadingPlugin {
if (externals.length === 0) {
return '';
}
- const runtimeGlobalsInit =
- `${ExternalsLoadingPlugin.RuntimeGlobals.lynxExternals} = {};`;
+ const runtimeGlobalsInit = `${getLynxExternalGlobal(layer)} = {};`;
const loadExternalFunc = `
function createLoadExternalAsync(handler, sectionPath) {
return new Promise((resolve, reject) => {
@@ -328,6 +351,8 @@ function createLoadExternalSync(handler, sectionPath, timeout) {
}
`;
+ const hasUrlLibraryNamePairInjected = new Set();
+
for (let i = 0; i < externals.length; i++) {
const [pkgName, external] = externals[i]!;
const {
@@ -343,13 +368,26 @@ function createLoadExternalSync(handler, sectionPath, timeout) {
if (!layerOptions?.sectionPath) {
continue;
}
+
+ const libraryNameWithDefault = libraryName ?? pkgName;
+ const libraryNameStr = Array.isArray(libraryNameWithDefault)
+ ? libraryNameWithDefault[0]
+ : libraryNameWithDefault;
+
+ const hash = `${url}-${libraryNameStr}`;
+ if (hasUrlLibraryNamePairInjected.has(hash)) {
+ continue;
+ }
+ hasUrlLibraryNamePairInjected.add(hash);
+
fetchCode.push(
`const handler${i} = lynx.fetchBundle(${JSON.stringify(url)}, {});`,
);
+
if (async) {
asyncLoadCode.push(
- `${ExternalsLoadingPlugin.RuntimeGlobals.lynxExternals}[${
- JSON.stringify(libraryName ?? pkgName)
+ `${getLynxExternalGlobal(layer)}[${
+ JSON.stringify(libraryNameStr)
}] = createLoadExternalAsync(handler${i}, ${
JSON.stringify(layerOptions.sectionPath)
});`,
@@ -358,8 +396,8 @@ function createLoadExternalSync(handler, sectionPath, timeout) {
}
syncLoadCode.push(
- `${ExternalsLoadingPlugin.RuntimeGlobals.lynxExternals}[${
- JSON.stringify(libraryName ?? pkgName)
+ `${getLynxExternalGlobal(layer)}[${
+ JSON.stringify(libraryNameStr)
}] = createLoadExternalSync(handler${i}, ${
JSON.stringify(layerOptions.sectionPath)
}, ${timeout});`,
@@ -428,13 +466,14 @@ function createLoadExternalSync(handler, sectionPath, timeout) {
&& externals[request]?.[currentLayer]
) {
const isAsync = externals[request]?.async ?? true;
+ const libraryName = externals[request]?.libraryName ?? request;
return callback(
undefined,
- `${
- isAsync ? 'promise ' : ''
- }${ExternalsLoadingPlugin.RuntimeGlobals.lynxExternals}[${
- JSON.stringify(externals[request]?.libraryName ?? request)
- }]`,
+ [
+ getLynxExternalGlobal(currentLayer),
+ ...(Array.isArray(libraryName) ? libraryName : [libraryName]),
+ ],
+ isAsync ? 'promise' : undefined,
);
}
// Continue without externalizing the import
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2b29c7b398..4d3343fdc4 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -242,6 +242,40 @@ importers:
specifier: 0.0.0-experimental-0566679-20250709
version: 0.0.0-experimental-0566679-20250709
+ examples/react-externals:
+ dependencies:
+ '@lynx-js/react':
+ specifier: workspace:*
+ version: link:../../packages/react
+ devDependencies:
+ '@lynx-js/externals-loading-webpack-plugin':
+ specifier: workspace:*
+ version: link:../../packages/webpack/externals-loading-webpack-plugin
+ '@lynx-js/lynx-bundle-rslib-config':
+ specifier: workspace:*
+ version: link:../../packages/rspeedy/lynx-bundle-rslib-config
+ '@lynx-js/preact-devtools':
+ specifier: ^5.0.1-cf9aef5
+ version: 5.0.1
+ '@lynx-js/qrcode-rsbuild-plugin':
+ specifier: workspace:*
+ version: link:../../packages/rspeedy/plugin-qrcode
+ '@lynx-js/react-rsbuild-plugin':
+ specifier: workspace:*
+ version: link:../../packages/rspeedy/plugin-react
+ '@lynx-js/react-webpack-plugin':
+ specifier: workspace:*
+ version: link:../../packages/webpack/react-webpack-plugin
+ '@lynx-js/rspeedy':
+ specifier: workspace:*
+ version: link:../../packages/rspeedy/core
+ '@lynx-js/types':
+ specifier: 3.4.11
+ version: 3.4.11
+ '@types/react':
+ specifier: ^18.3.25
+ version: 18.3.25
+
examples/react-lazy-bundle:
dependencies:
'@lynx-js/react':