diff --git a/.changeset/react-externals-setup-umd.md b/.changeset/react-externals-setup-umd.md new file mode 100644 index 0000000000..6c2d8602ab --- /dev/null +++ b/.changeset/react-externals-setup-umd.md @@ -0,0 +1,5 @@ +--- +'@lynx-js/react-umd': minor +--- + +Add standalone UMD build of the ReactLynx runtime. diff --git a/examples/react-externals/README.md b/examples/react-externals/README.md index eb50ece537..7d50ba1eb8 100644 --- a/examples/react-externals/README.md +++ b/examples/react-externals/README.md @@ -11,6 +11,7 @@ In this example, we show: ```bash pnpm build:reactlynx pnpm build:comp-lib -pnpx http-server -p 8080 dist -EXTERNAL_BUNDLE_PREFIX=http://${YOUR_IP_HERE}:8080 pnpm dev +pnpm dev ``` + +The dev server will automatically serve the ReactLynx runtime and the component library bundles. diff --git a/examples/react-externals/lynx.config.js b/examples/react-externals/lynx.config.ts similarity index 66% rename from examples/react-externals/lynx.config.js rename to examples/react-externals/lynx.config.ts index 2280877b0e..de67981bfc 100644 --- a/examples/react-externals/lynx.config.js +++ b/examples/react-externals/lynx.config.ts @@ -1,13 +1,91 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + import { pluginExternalBundle } from '@lynx-js/external-bundle-rsbuild-plugin'; import { pluginQRCode } from '@lynx-js/qrcode-rsbuild-plugin'; import { pluginReactLynx } from '@lynx-js/react-rsbuild-plugin'; import { defineConfig } from '@lynx-js/rspeedy'; +import type { RsbuildPlugin } from '@lynx-js/rspeedy'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +function getIPAddress() { + const interfaces = os.networkInterfaces(); + for (const devName in interfaces) { + const iface = interfaces[devName]; + if (iface) { + for (const alias of iface) { + if ( + alias.family === 'IPv4' && alias.address !== '127.0.0.1' + && !alias.internal + ) { + return alias.address; + } + } + } + } + return 'localhost'; +} const enableBundleAnalysis = !!process.env['RSPEEDY_BUNDLE_ANALYSIS']; -const EXTERNAL_BUNDLE_PREFIX = process.env['EXTERNAL_BUNDLE_PREFIX'] || ''; +const PORT = 8291; +const EXTERNAL_BUNDLE_PREFIX = process.env['EXTERNAL_BUNDLE_PREFIX'] + ?? `http://${getIPAddress()}:${PORT}`; + +const pluginServeExternals = (): RsbuildPlugin => ({ + name: 'serve-externals', + setup(api) { + api.modifyRsbuildConfig((config, { mergeRsbuildConfig }) => { + return mergeRsbuildConfig(config, { + dev: { + setupMiddlewares: [ + (middlewares) => { + middlewares.unshift(( + req: import('node:http').IncomingMessage, + res: import('node:http').ServerResponse, + next: () => void, + ) => { + if (req.url === '/react.lynx.bundle') { + const bundlePath = path.resolve( + __dirname, + '../../packages/react-umd/dist/react-dev.lynx.bundle', + ); + if (fs.existsSync(bundlePath)) { + res.setHeader('Content-Type', 'application/octet-stream'); + res.setHeader('Access-Control-Allow-Origin', '*'); + fs.createReadStream(bundlePath).pipe(res); + return; + } + } + if (req.url === '/comp-lib.lynx.bundle') { + const bundlePath = path.resolve( + __dirname, + './dist/comp-lib.lynx.bundle', + ); + if (fs.existsSync(bundlePath)) { + res.setHeader('Content-Type', 'application/octet-stream'); + res.setHeader('Access-Control-Allow-Origin', '*'); + fs.createReadStream(bundlePath).pipe(res); + return; + } + } + next(); + }); + return middlewares; + }, + ], + }, + }); + }); + }, +}); export default defineConfig({ plugins: [ + pluginServeExternals(), pluginReactLynx(), pluginQRCode({ schema(url) { @@ -120,6 +198,9 @@ export default defineConfig({ }, }, }, + server: { + port: PORT, + }, output: { filenameHash: 'contenthash:8', cleanDistPath: false, diff --git a/examples/react-externals/package.json b/examples/react-externals/package.json index 5db4c332e8..d2d9ad5418 100644 --- a/examples/react-externals/package.json +++ b/examples/react-externals/package.json @@ -7,8 +7,7 @@ "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", + "build:reactlynx": "pnpm --filter @lynx-js/react-umd build", "dev": "rspeedy dev" }, "dependencies": { diff --git a/examples/react-externals/tsconfig.json b/examples/react-externals/tsconfig.json index 8f6d4de332..822ced26a4 100644 --- a/examples/react-externals/tsconfig.json +++ b/examples/react-externals/tsconfig.json @@ -9,7 +9,7 @@ "checkJs": true, "isolatedDeclarations": false, }, - "include": ["src", "external-bundle", "lynx.config.js", "rslib-comp-lib.config.ts", "rslib-reactlynx.config.ts"], + "include": ["src", "external-bundle", "lynx.config.ts", "rslib-comp-lib.config.ts", "rslib-reactlynx.config.ts"], "references": [ { "path": "../../packages/react/tsconfig.json" }, { "path": "../../packages/rspeedy/core/tsconfig.build.json" }, diff --git a/packages/react-umd/README.md b/packages/react-umd/README.md new file mode 100644 index 0000000000..c30eb9c9b5 --- /dev/null +++ b/packages/react-umd/README.md @@ -0,0 +1,97 @@ +# @lynx-js/react-umd + +This package provides a standalone, Universal Module Definition (UMD) build of the `ReactLynx` runtime. It is designed to be used as an **external bundle**, allowing multiple Lynx components or applications to load a single React runtime instance over the network, which improves load time, caches efficiency, and reduces memory usage. + +## Purpose + +When building Lynx applications, the ReactLynx runtime is typically bundled directly into the application instance. However, for advanced use cases like micro-frontends or dynamically loading remote components, it's highly beneficial to expose ReactLynx as an external dependency. `react-umd` pre-bundles ReactLynx so that it can be loaded on-demand and shared across different parts of your Lynx app. + +## Building + +To build the development and production bundles locally: + +```bash +pnpm build +``` + +The script will automatically execute: + +- `pnpm build:development` (sets `NODE_ENV=development`) +- `pnpm build:production` (sets `NODE_ENV=production`) + +This generates the following artifacts in the `dist/` directory: + +- `react-dev.lynx.bundle` +- `react-prod.lynx.bundle` + +## Usage as an External Bundle + +For a full working example of how to serve and consume this external bundle, see the [`react-externals` example](../../examples/react-externals/README.md) in this repository. + +Typically, you will use `@lynx-js/external-bundle-rsbuild-plugin` to map `@lynx-js/react` and its internal modules directly to the URL where this UMD bundle is served in your Lynx config file (eg. `lynx.config.ts`). + +### 1. Consuming in a Custom Component Bundle (`rslib.config.ts`) + +If you are building your own external UI component library that relies on React, you should map the React imports to the global variable exposed by this UMD bundle. + +```ts +// rslib-comp-lib.config.ts +export default defineExternalBundleRslibConfig({ + output: { + externals: { + '@lynx-js/react': ['ReactLynx', 'React'], + '@lynx-js/react/internal': ['ReactLynx', 'ReactInternal'], + '@lynx-js/react/jsx-dev-runtime': ['ReactLynx', 'ReactJSXDevRuntime'], + '@lynx-js/react/jsx-runtime': ['ReactLynx', 'ReactJSXRuntime'], + '@lynx-js/react/lepus/jsx-dev-runtime': [ + 'ReactLynx', + 'ReactJSXLepusDevRuntime', + ], + '@lynx-js/react/lepus/jsx-runtime': ['ReactLynx', 'ReactJSXLepusRuntime'], + '@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'], + }, + }, +}); +``` + +### 2. Resolving the Bundle in a Lynx App (`lynx.config.ts`) + +In the host application, configure the Rsbuild plugin to load the React UMD bundle into the correct engine layers (Background Thread and Main Thread) when the app starts. + +```ts +// lynx.config.ts +import { pluginExternalBundle } from '@lynx-js/external-bundle-rsbuild-plugin'; + +export default defineConfig({ + plugins: [ + pluginExternalBundle({ + externals: { + '@lynx-js/react': { + libraryName: ['ReactLynx', 'React'], + url: `http:///react.lynx.bundle`, + background: { sectionPath: 'ReactLynx' }, + mainThread: { sectionPath: 'ReactLynx__main-thread' }, + async: false, + }, + // ... include similar configurations for other @lynx-js/react/* subpaths + }, + }), + ], +}); +``` + +> Note: Ensure you map both the `background` and `mainThread` configurations properly so that React successfully attaches across the threaded architecture. diff --git a/packages/react-umd/package.json b/packages/react-umd/package.json new file mode 100644 index 0000000000..778924129d --- /dev/null +++ b/packages/react-umd/package.json @@ -0,0 +1,44 @@ +{ + "name": "@lynx-js/react-umd", + "version": "0.0.1-alpha-1", + "description": "UMD build for ReactLynx", + "keywords": [ + "ReactLynx", + "rspeedy", + "externals", + "external bundle", + "umd" + ], + "repository": { + "type": "git", + "url": "https://github.com/lynx-family/lynx-stack.git", + "directory": "packages/react-umd" + }, + "license": "Apache-2.0", + "author": { + "name": "Yiming Li", + "email": "yimingli.cs@gmail.com" + }, + "type": "module", + "exports": { + "./dev": "./dist/react-dev.lynx.bundle", + "./prod": "./dist/react-prod.lynx.bundle" + }, + "files": [ + "dist", + "CHANGELOG.md", + "README.md" + ], + "scripts": { + "build": "rimraf dist && pnpm build:development && pnpm build:production", + "build:development": "cross-env NODE_ENV=development rslib build", + "build:production": "rslib build" + }, + "devDependencies": { + "@lynx-js/lynx-bundle-rslib-config": "workspace:*", + "@lynx-js/react": "workspace:*", + "@lynx-js/react-rsbuild-plugin": "workspace:*", + "cross-env": "^7.0.3", + "rimraf": "^6.1.3" + } +} diff --git a/examples/react-externals/rslib-reactlynx.config.ts b/packages/react-umd/rslib.config.ts similarity index 74% rename from examples/react-externals/rslib-reactlynx.config.ts rename to packages/react-umd/rslib.config.ts index 1524fa329d..0723848c2c 100644 --- a/examples/react-externals/rslib-reactlynx.config.ts +++ b/packages/react-umd/rslib.config.ts @@ -2,10 +2,10 @@ import { defineExternalBundleRslibConfig } from '@lynx-js/lynx-bundle-rslib-conf import { pluginReactLynx } from '@lynx-js/react-rsbuild-plugin'; export default defineExternalBundleRslibConfig({ - id: 'react', + id: process.env.NODE_ENV === 'development' ? 'react-dev' : 'react-prod', source: { entry: { - 'ReactLynx': './external-bundle/ReactLynx.ts', + 'ReactLynx': './src/index.ts', }, }, plugins: [ @@ -13,6 +13,5 @@ export default defineExternalBundleRslibConfig({ ], output: { cleanDistPath: false, - globalObject: 'globalThis', }, }); diff --git a/examples/react-externals/external-bundle/ReactLynx.ts b/packages/react-umd/src/index.ts similarity index 82% rename from examples/react-externals/external-bundle/ReactLynx.ts rename to packages/react-umd/src/index.ts index d32260d9a7..cef18399a5 100644 --- a/examples/react-externals/external-bundle/ReactLynx.ts +++ b/packages/react-umd/src/index.ts @@ -1,3 +1,7 @@ +// 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. + export * as React from '@lynx-js/react'; export * as ReactInternal from '@lynx-js/react/internal'; export * as ReactJSXDevRuntime from '@lynx-js/react/jsx-dev-runtime'; diff --git a/packages/react-umd/tsconfig.json b/packages/react-umd/tsconfig.json new file mode 100644 index 0000000000..5ab2d1c9ca --- /dev/null +++ b/packages/react-umd/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "composite": true, + }, + "include": [ + "src", + ], +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dd91c603d5..d12953d3e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -563,6 +563,24 @@ importers: specifier: ^18.3.28 version: 18.3.28 + packages/react-umd: + devDependencies: + '@lynx-js/lynx-bundle-rslib-config': + specifier: workspace:* + version: link:../rspeedy/lynx-bundle-rslib-config + '@lynx-js/react': + specifier: workspace:* + version: link:../react + '@lynx-js/react-rsbuild-plugin': + specifier: workspace:* + version: link:../rspeedy/plugin-react + cross-env: + specifier: ^7.0.3 + version: 7.0.3 + rimraf: + specifier: ^6.1.3 + version: 6.1.3 + packages/react/refresh: devDependencies: '@lynx-js/react': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3ee906f1e2..c981f61513 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -14,6 +14,7 @@ packages: - packages/react/transform/swc-plugin-reactlynx - packages/react/transform/swc-plugin-reactlynx-compat - packages/react/worklet-runtime + - packages/react-umd - packages/rspeedy/* - packages/tailwind-preset - packages/testing-library/*