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
5 changes: 5 additions & 0 deletions .changeset/react-externals-setup-umd.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lynx-js/react-umd': minor
---

Add standalone UMD build of the ReactLynx runtime.
5 changes: 3 additions & 2 deletions examples/react-externals/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Original file line number Diff line number Diff line change
@@ -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;
},
],
},
Comment thread
upupming marked this conversation as resolved.
});
});
},
});

export default defineConfig({
plugins: [
pluginServeExternals(),
pluginReactLynx(),
pluginQRCode({
schema(url) {
Expand Down Expand Up @@ -120,6 +198,9 @@ export default defineConfig({
},
},
},
server: {
port: PORT,
},
output: {
filenameHash: 'contenthash:8',
cleanDistPath: false,
Expand Down
3 changes: 1 addition & 2 deletions examples/react-externals/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion examples/react-externals/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down
97 changes: 97 additions & 0 deletions packages/react-umd/README.md
Original file line number Diff line number Diff line change
@@ -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://<your-server>/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.
44 changes: 44 additions & 0 deletions packages/react-umd/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "@lynx-js/react-umd",
"version": "0.0.1-alpha-1",
Comment thread
upupming marked this conversation as resolved.
"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"
}
Comment thread
colinaaa marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,16 @@ 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: [
pluginReactLynx(),
],
output: {
cleanDistPath: false,
globalObject: 'globalThis',
},
});
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
9 changes: 9 additions & 0 deletions packages/react-umd/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
},
"include": [
"src",
],
}
18 changes: 18 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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/*
Expand Down
Loading