diff --git a/.changeset/plugin-externals-rsbuild.md b/.changeset/plugin-externals-rsbuild.md
new file mode 100644
index 0000000000..ec1f8d2065
--- /dev/null
+++ b/.changeset/plugin-externals-rsbuild.md
@@ -0,0 +1,26 @@
+---
+'@lynx-js/external-bundle-rsbuild-plugin': patch
+---
+
+Introduce `@lynx-js/external-bundle-rsbuild-plugin`.
+
+```ts
+// lynx.config.ts
+import { pluginExternalBundle } from '@lynx-js/external-bundle-rsbuild-plugin';
+import { pluginReactLynx } from '@lynx-js/react-rsbuild-plugin';
+
+export default {
+ plugins: [
+ pluginReactLynx(),
+ pluginExternalBundle({
+ externals: {
+ lodash: {
+ url: 'http://lodash.lynx.bundle',
+ background: { sectionPath: 'background' },
+ mainThread: { sectionPath: 'mainThread' },
+ },
+ },
+ }),
+ ],
+};
+```
diff --git a/.changeset/sixty-emus-call.md b/.changeset/sixty-emus-call.md
new file mode 100644
index 0000000000..2a8302b06d
--- /dev/null
+++ b/.changeset/sixty-emus-call.md
@@ -0,0 +1,5 @@
+---
+"@lynx-js/react-rsbuild-plugin": patch
+---
+
+expose LAYERS via `api.expose` for other rsbuild plugins.
diff --git a/examples/react-externals/lynx.config.js b/examples/react-externals/lynx.config.js
index e0323b53d5..bcaea076e1 100644
--- a/examples/react-externals/lynx.config.js
+++ b/examples/react-externals/lynx.config.js
@@ -1,87 +1,12 @@
-import { ExternalsLoadingPlugin } from '@lynx-js/externals-loading-webpack-plugin';
+import { pluginExternalBundle } from '@lynx-js/external-bundle-rsbuild-plugin';
import { pluginQRCode } from '@lynx-js/qrcode-rsbuild-plugin';
-import { LAYERS, pluginReactLynx } from '@lynx-js/react-rsbuild-plugin';
+import { 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({
- 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: true,
- },
- },
- }),
- ],
- },
- },
plugins: [
pluginReactLynx(),
pluginQRCode({
@@ -90,6 +15,73 @@ export default defineConfig({
return `${url}?fullscreen=true`;
},
}),
+ pluginExternalBundle({
+ 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: true,
+ },
+ },
+ }),
],
environments: {
web: {},
diff --git a/examples/react-externals/package.json b/examples/react-externals/package.json
index f04b9d960f..e79f237887 100644
--- a/examples/react-externals/package.json
+++ b/examples/react-externals/package.json
@@ -13,7 +13,7 @@
"@lynx-js/react": "workspace:*"
},
"devDependencies": {
- "@lynx-js/externals-loading-webpack-plugin": "workspace:*",
+ "@lynx-js/external-bundle-rsbuild-plugin": "workspace:*",
"@lynx-js/lynx-bundle-rslib-config": "workspace:*",
"@lynx-js/preact-devtools": "^5.0.1-cf9aef5",
"@lynx-js/qrcode-rsbuild-plugin": "workspace:*",
diff --git a/packages/rspeedy/lynx-bundle-rslib-config/src/index.ts b/packages/rspeedy/lynx-bundle-rslib-config/src/index.ts
index 7da318e5b3..1c714fdc89 100644
--- a/packages/rspeedy/lynx-bundle-rslib-config/src/index.ts
+++ b/packages/rspeedy/lynx-bundle-rslib-config/src/index.ts
@@ -6,44 +6,6 @@
* @packageDocumentation
*
* `@lynx-js/lynx-bundle-rslib-config` is the package that provides the configurations for bundling Lynx bundle with {@link https://rslib.rs/ | Rslib}.
- *
- * 1. Install the package:
- *
- * ```bash
- * pnpm add @lynx-js/lynx-bundle-rslib-config @rslib/core -D
- * ```
- *
- * 2. Add the following code to `rslib.config.ts`:
- *
- * ```ts
- * import { defineExternalBundleRslibConfig } from '@lynx-js/lynx-bundle-rslib-config'
- *
- * export default defineExternalBundleRslibConfig({
- * id: 'my-utils',
- * source: {
- * entry: {
- * utils: './src/utils.ts'
- * }
- * }
- * })
- * ```
- *
- * 3. Run the command `pnpm rslib build` and you will get the `my-utils.lynx.bundle` in the `dist` directory. You can upload the bundle to CDN or server.
- *
- * 4. Finally, you can fetch and load the external bundle through:
- *
- * ```js
- * const bundleUrl = 'http://cdn.com/my-utils.lynx.bundle'
- * lynx.fetchBundle(bundleUrl).wait(3) // timeout is 3s
- *
- * if (__BACKGROUND__) {
- * const utils = lynx.loadScript('utils', { bundleName: bundleUrl })
- * } else {
- * const utils = lynx.loadScript('utils__main-thread', { bundleName: bundleUrl })
- * }
- * ```
- *
- * For more detail, please refer to the {@link defineExternalBundleRslibConfig}.
*/
export {
defineExternalBundleRslibConfig,
diff --git a/packages/rspeedy/plugin-external-bundle/CHANGELOG.md b/packages/rspeedy/plugin-external-bundle/CHANGELOG.md
new file mode 100644
index 0000000000..13ef778430
--- /dev/null
+++ b/packages/rspeedy/plugin-external-bundle/CHANGELOG.md
@@ -0,0 +1 @@
+# @lynx-js/external-bundle-rsbuild-plugin
diff --git a/packages/rspeedy/plugin-external-bundle/README.md b/packages/rspeedy/plugin-external-bundle/README.md
new file mode 100644
index 0000000000..b7e00dd7f0
--- /dev/null
+++ b/packages/rspeedy/plugin-external-bundle/README.md
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Getting Started
+
+```bash
+npm install -D @lynx-js/external-bundle-rsbuild-plugin
+```
+
+## Usage
+
+```ts
+// lynx.config.ts
+import { pluginExternalBundle } from '@lynx-js/external-bundle-rsbuild-plugin'
+import { pluginReactLynx } from '@lynx-js/react-rsbuild-plugin'
+
+export default {
+ plugins: [
+ pluginReactLynx(),
+ pluginExternalBundle({
+ externals: {
+ lodash: {
+ url: 'http://lodash.lynx.bundle',
+ background: { sectionPath: 'background' },
+ mainThread: { sectionPath: 'mainThread' },
+ },
+ },
+ }),
+ ],
+}
+```
+
+## Documentation
+
+Visit [Lynx Website](https://lynxjs.org/api/rspeedy/external-bundle-rsbuild-plugin) to view the full documentation.
+
+## Contributing
+
+Contributions to Rspeedy are welcome and highly appreciated. However, before you jump right into it, we would like you to review our [Contribution Guidelines](/CONTRIBUTING.md) to make sure you have a smooth experience contributing to this project.
+
+## License
+
+Rspeedy is Apache-2.0 licensed.
diff --git a/packages/rspeedy/plugin-external-bundle/api-extractor.json b/packages/rspeedy/plugin-external-bundle/api-extractor.json
new file mode 100644
index 0000000000..65b0c69ca6
--- /dev/null
+++ b/packages/rspeedy/plugin-external-bundle/api-extractor.json
@@ -0,0 +1,6 @@
+/**
+ * Config file for API Extractor. For more info, please visit: https://api-extractor.com
+ */
+{
+ "extends": "../../../api-extractor.json",
+}
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
new file mode 100644
index 0000000000..78fd501a50
--- /dev/null
+++ b/packages/rspeedy/plugin-external-bundle/etc/external-bundle-rsbuild-plugin.api.md
@@ -0,0 +1,16 @@
+## API Report File for "@lynx-js/external-bundle-rsbuild-plugin"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+
+import type { ExternalsLoadingPluginOptions } from '@lynx-js/externals-loading-webpack-plugin';
+import type { RsbuildPlugin } from '@rsbuild/core';
+
+// @public
+export function pluginExternalBundle(options: PluginExternalBundleOptions): RsbuildPlugin;
+
+// @public
+export type PluginExternalBundleOptions = Pick;
+
+```
diff --git a/packages/rspeedy/plugin-external-bundle/package.json b/packages/rspeedy/plugin-external-bundle/package.json
new file mode 100644
index 0000000000..1f3dce4e2e
--- /dev/null
+++ b/packages/rspeedy/plugin-external-bundle/package.json
@@ -0,0 +1,50 @@
+{
+ "name": "@lynx-js/external-bundle-rsbuild-plugin",
+ "version": "0.0.0",
+ "description": "A rsbuild plugin for loading lynx external bundles.",
+ "keywords": [
+ "rsbuild",
+ "Lynx",
+ "externals",
+ "bundle"
+ ],
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/lynx-family/lynx-stack.git",
+ "directory": "packages/rspeedy/plugin-external-bundle"
+ },
+ "license": "Apache-2.0",
+ "author": {
+ "name": "Hengchang Lu",
+ "email": "luhengchang228@gmail.com"
+ },
+ "type": "module",
+ "exports": {
+ ".": {
+ "types": "./lib/index.d.ts",
+ "default": "./lib/index.js"
+ },
+ "./package.json": "./package.json"
+ },
+ "types": "./lib/index.d.ts",
+ "files": [
+ "lib",
+ "!lib/**/*.js.map",
+ "CHANGELOG.md",
+ "README.md"
+ ],
+ "scripts": {
+ "api-extractor": "api-extractor run --verbose",
+ "test": "pnpm -w run test --project rspeedy/externals"
+ },
+ "dependencies": {
+ "@lynx-js/externals-loading-webpack-plugin": "workspace:*"
+ },
+ "devDependencies": {
+ "@microsoft/api-extractor": "catalog:",
+ "@rsbuild/core": "catalog:rsbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+}
diff --git a/packages/rspeedy/plugin-external-bundle/src/index.ts b/packages/rspeedy/plugin-external-bundle/src/index.ts
new file mode 100644
index 0000000000..f26c216d90
--- /dev/null
+++ b/packages/rspeedy/plugin-external-bundle/src/index.ts
@@ -0,0 +1,90 @@
+// Copyright 2024 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.
+
+/**
+ * @packageDocumentation
+ *
+ * A rsbuild plugin for loading external bundles using externals-loading-webpack-plugin.
+ */
+
+import type { RsbuildPlugin } from '@rsbuild/core'
+
+import type { ExternalsLoadingPluginOptions } from '@lynx-js/externals-loading-webpack-plugin'
+import { ExternalsLoadingPlugin } from '@lynx-js/externals-loading-webpack-plugin'
+
+interface ExposedLayers {
+ readonly BACKGROUND: string
+ readonly MAIN_THREAD: string
+}
+
+/**
+ * Options for the external-bundle-rsbuild-plugin.
+ *
+ * @public
+ */
+export type PluginExternalBundleOptions = Pick<
+ ExternalsLoadingPluginOptions,
+ 'externals'
+>
+
+/**
+ * Create a rsbuild plugin for loading external bundles.
+ *
+ * This plugin wraps the externals-loading-webpack-plugin and automatically
+ * retrieves layer names from the react-rsbuild-plugin via api.useExposed.
+ *
+ * @example
+ * ```ts
+ * // lynx.config.ts
+ * import { pluginExternalBundle } from '@lynx-js/external-bundle-rsbuild-plugin'
+ * import { pluginReactLynx } from '@lynx-js/react-rsbuild-plugin'
+ *
+ * export default {
+ * plugins: [
+ * pluginReactLynx(),
+ * pluginExternalBundle({
+ * externals: {
+ * lodash: {
+ * url: 'http://lodash.lynx.bundle',
+ * background: { sectionPath: 'background' },
+ * mainThread: { sectionPath: 'mainThread' },
+ * },
+ * },
+ * }),
+ * ],
+ * }
+ * ```
+ *
+ * @public
+ */
+export function pluginExternalBundle(
+ options: PluginExternalBundleOptions,
+): RsbuildPlugin {
+ return {
+ name: 'lynx:external-bundle',
+ setup(api) {
+ api.modifyRspackConfig((config) => {
+ // Get layer names from react-rsbuild-plugin
+ const LAYERS = api.useExposed(
+ Symbol.for('LAYERS'),
+ )
+
+ if (!LAYERS) {
+ throw new Error(
+ 'external-bundle-rsbuild-plugin requires exposed `LAYERS`.',
+ )
+ }
+ config.plugins = config.plugins || []
+ config.plugins.push(
+ new ExternalsLoadingPlugin({
+ backgroundLayer: LAYERS.BACKGROUND,
+ mainThreadLayer: LAYERS.MAIN_THREAD,
+ externals: options.externals,
+ }),
+ )
+ return config
+ })
+ },
+ }
+}
diff --git a/packages/rspeedy/plugin-external-bundle/test/index.test.ts b/packages/rspeedy/plugin-external-bundle/test/index.test.ts
new file mode 100644
index 0000000000..7e22b2f546
--- /dev/null
+++ b/packages/rspeedy/plugin-external-bundle/test/index.test.ts
@@ -0,0 +1,193 @@
+// Copyright 2024 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 { createRsbuild } from '@rsbuild/core'
+import { describe, expect, test } from 'vitest'
+
+import { ExternalsLoadingPlugin } from '@lynx-js/externals-loading-webpack-plugin'
+
+import { pluginStubLayers } from './stub-layers.plugin.js'
+
+describe('pluginExternalBundle', () => {
+ test('should register ExternalsLoadingPlugin with correct options', async () => {
+ const { pluginExternalBundle } = await import('../src/index.js')
+
+ const externalsConfig = {
+ lodash: {
+ url: 'http://lodash.lynx.bundle',
+ background: { sectionPath: 'background' },
+ mainThread: { sectionPath: 'mainThread' },
+ },
+ react: {
+ url: 'http://react.lynx.bundle',
+ background: { sectionPath: 'react-background' },
+ mainThread: { sectionPath: 'react-main' },
+ },
+ }
+
+ let capturedPlugins: unknown[] = []
+
+ const rsbuild = await createRsbuild({
+ rsbuildConfig: {
+ source: {
+ entry: {
+ main: './fixtures/basic.tsx',
+ },
+ },
+ tools: {
+ rspack(config) {
+ // Capture plugins for verification
+ capturedPlugins = config.plugins || []
+ return config
+ },
+ },
+ plugins: [
+ pluginStubLayers(),
+ pluginExternalBundle({
+ externals: externalsConfig,
+ }),
+ ],
+ },
+ })
+
+ // Trigger the config to be resolved
+ await rsbuild.inspectConfig()
+
+ // Verify that ExternalsLoadingPlugin is registered
+ const externalBundlePlugin = capturedPlugins.find(
+ (plugin) => plugin instanceof ExternalsLoadingPlugin,
+ )
+
+ expect(externalBundlePlugin).toBeDefined()
+
+ // Verify plugin options
+ expect(externalBundlePlugin).toMatchObject({
+ options: {
+ backgroundLayer: 'BACKGROUND_LAYER',
+ mainThreadLayer: 'MAIN_THREAD_LAYER',
+ externals: externalsConfig,
+ },
+ })
+ })
+
+ test('should throw error if LAYERS is not exposed', async () => {
+ const { pluginExternalBundle } = await import('../src/index.js')
+
+ const rsbuild = await createRsbuild({
+ rsbuildConfig: {
+ source: {
+ entry: {
+ main: './fixtures/basic.tsx',
+ },
+ },
+ plugins: [
+ // Not including pluginStubLayers to test error case
+ pluginExternalBundle({
+ externals: {
+ lodash: {
+ url: 'http://lodash.lynx.bundle',
+ background: { sectionPath: 'background' },
+ mainThread: { sectionPath: 'mainThread' },
+ },
+ },
+ }),
+ ],
+ },
+ })
+
+ // The error should be thrown during config inspection/build
+ await expect(rsbuild.inspectConfig()).rejects.toThrow(
+ 'external-bundle-rsbuild-plugin requires exposed `LAYERS`.',
+ )
+ })
+
+ test('should work with empty externals config', 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: {} })],
+ },
+ })
+
+ await rsbuild.inspectConfig()
+
+ const externalBundlePlugin = capturedPlugins.find(
+ (plugin) => plugin instanceof ExternalsLoadingPlugin,
+ )
+
+ expect(externalBundlePlugin).toBeDefined()
+ expect(externalBundlePlugin).toMatchObject({
+ options: {
+ externals: {},
+ },
+ })
+ })
+
+ test('should correctly pass layer names from LAYERS', async () => {
+ const { pluginExternalBundle } = await import('../src/index.js')
+
+ const customLayers = {
+ BACKGROUND: 'CUSTOM_BACKGROUND',
+ MAIN_THREAD: 'CUSTOM_MAIN',
+ }
+
+ let capturedPlugins: unknown[] = []
+
+ const rsbuild = await createRsbuild({
+ rsbuildConfig: {
+ source: {
+ entry: {
+ main: './fixtures/basic.tsx',
+ },
+ },
+ tools: {
+ rspack(config) {
+ capturedPlugins = config.plugins || []
+ return config
+ },
+ },
+ plugins: [
+ pluginStubLayers(customLayers),
+ pluginExternalBundle({
+ externals: {
+ lodash: {
+ url: 'http://lodash.lynx.bundle',
+ background: { sectionPath: 'background' },
+ mainThread: { sectionPath: 'mainThread' },
+ },
+ },
+ }),
+ ],
+ },
+ })
+
+ await rsbuild.inspectConfig()
+
+ const externalBundlePlugin = capturedPlugins.find(
+ (plugin) => plugin instanceof ExternalsLoadingPlugin,
+ )
+
+ expect(externalBundlePlugin).toBeDefined()
+ expect(externalBundlePlugin).toMatchObject({
+ options: {
+ backgroundLayer: 'CUSTOM_BACKGROUND',
+ mainThreadLayer: 'CUSTOM_MAIN',
+ },
+ })
+ })
+})
diff --git a/packages/rspeedy/plugin-external-bundle/test/stub-layers.plugin.ts b/packages/rspeedy/plugin-external-bundle/test/stub-layers.plugin.ts
new file mode 100644
index 0000000000..79b0e16f42
--- /dev/null
+++ b/packages/rspeedy/plugin-external-bundle/test/stub-layers.plugin.ts
@@ -0,0 +1,24 @@
+// Copyright 2024 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 type { RsbuildPlugin } from '@rsbuild/core'
+
+interface ExposedLayers {
+ readonly BACKGROUND: string
+ readonly MAIN_THREAD: string
+}
+
+/**
+ * Stub plugin that exposes LAYERS for testing
+ */
+export const pluginStubLayers = (
+ layers: ExposedLayers = {
+ BACKGROUND: 'BACKGROUND_LAYER',
+ MAIN_THREAD: 'MAIN_THREAD_LAYER',
+ },
+): RsbuildPlugin => ({
+ name: 'lynx:stub-layers',
+ setup(api) {
+ api.expose(Symbol.for('LAYERS'), layers)
+ },
+})
diff --git a/packages/rspeedy/plugin-external-bundle/tsconfig.build.json b/packages/rspeedy/plugin-external-bundle/tsconfig.build.json
new file mode 100644
index 0000000000..b75bf5dd1c
--- /dev/null
+++ b/packages/rspeedy/plugin-external-bundle/tsconfig.build.json
@@ -0,0 +1,12 @@
+{
+ "extends": "../../../tsconfig.json",
+ "compilerOptions": {
+ "composite": true,
+ "outDir": "./lib",
+ "rootDir": "./src",
+ },
+ "references": [
+ { "path": "../core/tsconfig.build.json" },
+ ],
+ "include": ["src"],
+}
diff --git a/packages/rspeedy/plugin-external-bundle/tsconfig.json b/packages/rspeedy/plugin-external-bundle/tsconfig.json
new file mode 100644
index 0000000000..110e5a0ccb
--- /dev/null
+++ b/packages/rspeedy/plugin-external-bundle/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "extends": ["./tsconfig.build.json"],
+ "compilerOptions": {
+ "rootDir": ".",
+ "noEmit": true,
+ },
+ "include": ["src", "test", "vitest.config.ts"],
+ "references": [
+ { "path": "../core/tsconfig.build.json" },
+ ],
+}
diff --git a/packages/rspeedy/plugin-external-bundle/vitest.config.ts b/packages/rspeedy/plugin-external-bundle/vitest.config.ts
new file mode 100644
index 0000000000..8221393e89
--- /dev/null
+++ b/packages/rspeedy/plugin-external-bundle/vitest.config.ts
@@ -0,0 +1,10 @@
+import { defineProject } from 'vitest/config'
+import type { UserWorkspaceConfig } from 'vitest/config'
+
+const config: UserWorkspaceConfig = defineProject({
+ test: {
+ name: 'rspeedy/externals',
+ },
+})
+
+export default config
diff --git a/packages/rspeedy/plugin-react/src/pluginReactLynx.ts b/packages/rspeedy/plugin-react/src/pluginReactLynx.ts
index 4f4c9fd023..b4ae22a386 100644
--- a/packages/rspeedy/plugin-react/src/pluginReactLynx.ts
+++ b/packages/rspeedy/plugin-react/src/pluginReactLynx.ts
@@ -368,6 +368,8 @@ export function pluginReactLynx(
applyLazy(api)
}
+ api.expose(Symbol.for('LAYERS'), LAYERS)
+
const rspeedyAPIs = api.useExposed(
Symbol.for('rspeedy.api'),
)!
diff --git a/packages/rspeedy/tsconfig.json b/packages/rspeedy/tsconfig.json
index 014b25cb83..7595fb6cbd 100644
--- a/packages/rspeedy/tsconfig.json
+++ b/packages/rspeedy/tsconfig.json
@@ -10,6 +10,7 @@
{ "path": "./plugin-react/tsconfig.build.json" },
{ "path": "./plugin-react-alias/tsconfig.build.json" },
{ "path": "./lynx-bundle-rslib-config/tsconfig.build.json" },
+ { "path": "./plugin-external-bundle/tsconfig.build.json" },
/** packages-end */
],
"include": [],
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6c53119eb7..fbf1554a3f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -248,9 +248,9 @@ importers:
specifier: workspace:*
version: link:../../packages/react
devDependencies:
- '@lynx-js/externals-loading-webpack-plugin':
+ '@lynx-js/external-bundle-rsbuild-plugin':
specifier: workspace:*
- version: link:../../packages/webpack/externals-loading-webpack-plugin
+ version: link:../../packages/rspeedy/plugin-external-bundle
'@lynx-js/lynx-bundle-rslib-config':
specifier: workspace:*
version: link:../../packages/rspeedy/lynx-bundle-rslib-config
@@ -656,6 +656,19 @@ importers:
specifier: ^5.102.0
version: 5.102.0
+ packages/rspeedy/plugin-external-bundle:
+ dependencies:
+ '@lynx-js/externals-loading-webpack-plugin':
+ specifier: workspace:*
+ version: link:../../webpack/externals-loading-webpack-plugin
+ devDependencies:
+ '@microsoft/api-extractor':
+ specifier: 'catalog:'
+ version: 7.55.2(@types/node@24.6.1)
+ '@rsbuild/core':
+ specifier: catalog:rsbuild
+ version: 1.6.13
+
packages/rspeedy/plugin-qrcode:
devDependencies:
'@clack/prompts':
@@ -1518,6 +1531,9 @@ importers:
'@lynx-js/css-extract-webpack-plugin':
specifier: workspace:*
version: link:../packages/webpack/css-extract-webpack-plugin
+ '@lynx-js/external-bundle-rsbuild-plugin':
+ specifier: workspace:*
+ version: link:../packages/rspeedy/plugin-external-bundle
'@lynx-js/lynx-bundle-rslib-config':
specifier: workspace:*
version: link:../packages/rspeedy/lynx-bundle-rslib-config
diff --git a/website/package.json b/website/package.json
index 760ba97165..4a47632f76 100644
--- a/website/package.json
+++ b/website/package.json
@@ -21,6 +21,7 @@
"devDependencies": {
"@lynx-js/chunk-loading-webpack-plugin": "workspace:*",
"@lynx-js/css-extract-webpack-plugin": "workspace:*",
+ "@lynx-js/external-bundle-rsbuild-plugin": "workspace:*",
"@lynx-js/lynx-bundle-rslib-config": "workspace:*",
"@lynx-js/qrcode-rsbuild-plugin": "workspace:*",
"@lynx-js/react": "workspace:*",
diff --git a/website/rspress.config.ts b/website/rspress.config.ts
index 1f0c5ad27d..c07cc2b1ca 100644
--- a/website/rspress.config.ts
+++ b/website/rspress.config.ts
@@ -64,6 +64,9 @@ const SIDEBARS = {
'PluginQRCodeOptions',
],
}),
+ createAPI({
+ name: 'external-bundle-rsbuild-plugin',
+ }),
createAPI({
name: 'lynx-bundle-rslib-config',
}),
@@ -204,6 +207,10 @@ const SIDEBARS_ZH = {
'PluginQRCodeOptions',
],
}),
+ createAPI({
+ base: 'zh/api',
+ name: 'external-bundle-rsbuild-plugin',
+ }),
createAPI({
base: 'zh/api',
name: 'lynx-bundle-rslib-config',