Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
d460139
feat: allow with.mode attr in swc dynamic import plugin
upupming May 8, 2026
41fcbf2
feat(swc-dynamic-import): wrap import() with withLazyBundleMode for m…
upupming May 8, 2026
e0fb546
feat(react): add withLazyBundleMode runtime helper
upupming May 8, 2026
7c318ca
feat(react): add fetchBundle-based loadLazyBundle path
upupming May 8, 2026
f7a79ee
feat(template-plugin): emit FetchBundle-mode customSections for lazy …
upupming May 8, 2026
453d054
chore: move @lynx-js/types override from package.json to pnpm-workspa…
upupming May 8, 2026
6ea297b
feat(react-webpack-plugin): gate FetchBundle default on engineVersion…
upupming May 8, 2026
f91bf50
chore: regenerate api report + dedupe lockfile
upupming May 8, 2026
25edde6
fix(swc-plugin-dynamic-import): drop unused enumerate index
upupming May 8, 2026
e0275d3
chore: regenerate template-webpack-plugin api report
upupming May 8, 2026
4e8aeaa
chore: add empty changeset for in-flight FetchBundle work
upupming May 8, 2026
6bf0dc1
fix(web-core): add ^build to turbo build deps
upupming May 8, 2026
370fad7
chore(vendor): refresh @lynx-js/types tarball with optional loadScrip…
upupming May 11, 2026
f6d3891
fix(plugin-react): gate lazy-bundle build output on resolved fetcher
upupming May 11, 2026
a02fb76
fix(react-webpack-plugin): self-invoke main-thread wrapper for FetchB…
upupming May 11, 2026
2519670
chore: dev/build/preview commands for FetchBundle in lazy-bundle exam…
upupming May 11, 2026
754ab38
Merge remote-tracking branch 'origin/main' into feat/lazy-bundle-with…
upupming May 11, 2026
ad06fd9
chore(examples): split lazy bundles into Sync + Async demos
upupming May 11, 2026
6d1ca60
Merge remote-tracking branch 'origin/main' into feat/lazy-bundle-with…
upupming May 11, 2026
1a8cf3c
feat(react): coordinate MT prep on FetchBundle BG-triggered async load
upupming May 11, 2026
0c1d610
feat(react): throw on lazy bundle mode used with QueryComponent in dev
upupming May 11, 2026
4a12527
feat(template-plugin): default to bytecode for FetchBundle lazy main-…
upupming May 11, 2026
b1d9b44
chore(examples): branch lazy-bundle demos on __LAZY_BUNDLE_FETCHER__
upupming May 11, 2026
bc70e16
chore(examples): use bracket access for env vars in entry-url
upupming May 11, 2026
a99d88f
chore(changeset): convert lazy-bundle-fetch-bundle to a release entry
upupming May 11, 2026
036d48e
chore(changeset): bump lazy-bundle FetchBundle entry to minor
upupming May 11, 2026
ccdafb8
chore(changeset): satisfy peer ranges for FetchBundle minor bumps
upupming May 11, 2026
51eac2a
test(plugin-react): add lazyBundleFetcher to default options snapshot
upupming May 11, 2026
f1781d7
Merge branch 'main' into feat/lazy-bundle-with-fetchBundle
upupming May 11, 2026
bc0229e
chore: refresh lockfile after merging origin/main
upupming May 11, 2026
d7126e1
test(react): cover FetchBundle runtime path
upupming May 11, 2026
f54614e
test(react): cover remaining FetchBundle branches and unreachable
upupming May 11, 2026
5c266dc
test(template-plugin): cover FetchBundle output shape and bytecode ga…
upupming May 11, 2026
c464392
test(plugin-react): cover resolveLazyBundleFetcher decision matrix
upupming May 11, 2026
ffdbea3
test(react-webpack-plugin): cover FetchBundle MT wrapper + define inj…
upupming May 11, 2026
82cbb45
test(template-plugin): cover WebEncodePlugin lepusCode-undefined safety
upupming May 11, 2026
662fb77
fix(lazy-bundle): address review nits
upupming May 11, 2026
06d6697
refactor(lazy-bundle): merge MT loadScript+CSS into one try block
upupming May 11, 2026
4c4333d
chore(codecov): ignore examples/
upupming May 11, 2026
c126bbd
chore(codecov): ignore generated snapshot fixtures
upupming May 11, 2026
f6bc1e3
test(template-plugin): migrate FetchBundle shape tests to cases frame…
upupming May 12, 2026
d2e3c2e
Merge branch 'main' into feat/lazy-bundle-with-fetchBundle
upupming May 12, 2026
c75de42
chore: pnpm dedupe after merge
upupming May 12, 2026
db3c74c
chore(deps): bump @lynx-js/types to 3.10.2-alpha.0
upupming May 12, 2026
1828a25
fix(swc-dynamic-import): declare \`once_cell\` workspace dep
upupming May 12, 2026
00ed617
refactor(swc-dynamic-import): drop redundant atoms::once_cell import
upupming May 12, 2026
9ea2105
chore: pnpm dedupe after types bump
upupming May 12, 2026
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
14 changes: 14 additions & 0 deletions .changeset/lazy-bundle-fetch-bundle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@lynx-js/react": minor
"@lynx-js/react-rsbuild-plugin": minor
"@lynx-js/react-webpack-plugin": minor
"@lynx-js/template-webpack-plugin": minor
---

feat(lazy-bundle): add `lynx.fetchBundle`-based loader

Opt in by setting `engineVersion: '3.8'` (or higher) in `pluginReactLynx`.
Use `import('./X', { with: { mode: 'sync' | 'async' } })` to control whether
the first screen blocks on a sync fetch. The lazy bundle's main-thread
section is bytecoded by default (skipped in dev or when `DEBUG` includes
`rspeedy`).
1 change: 1 addition & 0 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion benchmark/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"@lynx-js/rspeedy": "workspace:*",
"@lynx-js/trace-processor": "^0.0.1",
"@lynx-js/type-element-api": "0.0.3",
"@lynx-js/types": "3.7.0",
"@lynx-js/types": "3.10.2-alpha.0",
"@types/react": "^18.3.28"
}
}
3 changes: 3 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,12 @@ coverage:
ignore:
- ".github/**"
- "codecov.yml"
- "examples/**"
- "packages/genui/**"
- "pnpm-lock.yaml"
- "rstest.config.ts"
- "**/__swc_snapshots__/**"
- "**/__snapshots__/**"

fixes:
- "/home/runner/_work/lynx-stack::"
Expand Down
2 changes: 1 addition & 1 deletion examples/gesture/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"@lynx-js/qrcode-rsbuild-plugin": "workspace:*",
"@lynx-js/react-rsbuild-plugin": "workspace:*",
"@lynx-js/rspeedy": "workspace:*",
"@lynx-js/types": "3.7.0",
"@lynx-js/types": "3.10.2-alpha.0",
"@types/react": "^18.3.28"
}
}
2 changes: 1 addition & 1 deletion examples/motion/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"@lynx-js/qrcode-rsbuild-plugin": "workspace:*",
"@lynx-js/react-rsbuild-plugin": "workspace:*",
"@lynx-js/rspeedy": "workspace:*",
"@lynx-js/types": "3.7.0",
"@lynx-js/types": "3.10.2-alpha.0",
"@types/react": "^18.3.28"
}
}
2 changes: 1 addition & 1 deletion examples/react-compiler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"@lynx-js/qrcode-rsbuild-plugin": "workspace:*",
"@lynx-js/react-rsbuild-plugin": "workspace:*",
"@lynx-js/rspeedy": "workspace:*",
"@lynx-js/types": "3.7.0",
"@lynx-js/types": "3.10.2-alpha.0",
"@rsbuild/plugin-babel": "1.1.0",
"@types/react": "^18.3.28",
"babel-plugin-react-compiler": "0.0.0-experimental-fe727a3-20250909"
Expand Down
2 changes: 1 addition & 1 deletion examples/react-element-template/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"@lynx-js/qrcode-rsbuild-plugin": "workspace:*",
"@lynx-js/react-rsbuild-plugin": "workspace:*",
"@lynx-js/rspeedy": "workspace:*",
"@lynx-js/types": "3.7.0",
"@lynx-js/types": "3.10.2-alpha.0",
"@types/react": "^18.3.28"
}
}
2 changes: 1 addition & 1 deletion examples/react-element/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"@lynx-js/qrcode-rsbuild-plugin": "workspace:*",
"@lynx-js/react-rsbuild-plugin": "workspace:*",
"@lynx-js/rspeedy": "workspace:*",
"@lynx-js/types": "3.7.0",
"@lynx-js/types": "3.10.2-alpha.0",
"@types/react": "^18.3.28"
}
}
2 changes: 1 addition & 1 deletion examples/react-externals/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"@lynx-js/qrcode-rsbuild-plugin": "workspace:*",
"@lynx-js/react-rsbuild-plugin": "workspace:*",
"@lynx-js/rspeedy": "workspace:*",
"@lynx-js/types": "3.7.0",
"@lynx-js/types": "3.10.2-alpha.0",
"@types/react": "^18.3.28",
"cross-env": "^7.0.3"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { detectLanHost, producerDevPort } from './demo-ports.js';

const projectRoot = path.dirname(fileURLToPath(import.meta.url));
const enableBundleAnalysis = !!process.env['RSPEEDY_BUNDLE_ANALYSIS'];
const enableFetchBundle = !!process.env['LAZY_BUNDLE_FETCHBUNDLE'];
const producerHost = detectLanHost();

export default defineConfig({
Expand All @@ -35,7 +36,9 @@ export default defineConfig({
},
},
plugins: [
pluginReactLynx(),
pluginReactLynx({
...(enableFetchBundle ? { engineVersion: '3.8' } : {}),
}),
pluginQRCode({
schema(url) {
return `${url}?fullscreen=true`;
Expand Down
4 changes: 4 additions & 0 deletions examples/react-lazy-bundle-standalone/lynx.config.producer.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ import { detectLanHost, producerDevPort } from './demo-ports.js';

const projectRoot = path.dirname(fileURLToPath(import.meta.url));
const enableBundleAnalysis = !!process.env['RSPEEDY_BUNDLE_ANALYSIS'];
const enableFetchBundle = !!process.env['LAZY_BUNDLE_FETCHBUNDLE'];
const producerPublicPath = `http://${detectLanHost()}:${producerDevPort}/`;

export default defineConfig({
source: {
entry: {
LazyComponent: './src/LazyComponent.tsx',
LazyComponentSync: './src/LazyComponentSync.tsx',
LazyComponentAsync: './src/LazyComponentAsync.tsx',
add: './src/utils/add.ts',
minus: './src/utils/minus.ts',
dynamic: './src/utils/dynamic.ts',
Expand All @@ -35,6 +38,7 @@ export default defineConfig({
plugins: [
pluginReactLynx({
experimental_isLazyBundle: true,
...(enableFetchBundle ? { engineVersion: '3.8' } : {}),
}),
],
environments: {
Expand Down
8 changes: 6 additions & 2 deletions examples/react-lazy-bundle-standalone/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@
"scripts": {
"build": "pnpm run --parallel \"/^build:(producer|consumer)$/\"",
"build:consumer": "rspeedy build --config lynx.config.consumer.js",
"build:fetchbundle": "cross-env LAZY_BUNDLE_FETCHBUNDLE=1 pnpm run build",
"build:producer": "rspeedy build --config lynx.config.producer.js",
"dev": "node scripts/serve.mjs dev",
"dev:consumer": "rspeedy dev --config lynx.config.consumer.js",
"dev:fetchbundle": "cross-env LAZY_BUNDLE_FETCHBUNDLE=1 node scripts/serve.mjs dev",
"dev:producer": "rspeedy dev --config lynx.config.producer.js",
"preview": "node scripts/serve.mjs preview",
"preview:consumer": "rspeedy preview --config lynx.config.consumer.js",
"preview:fetchbundle": "cross-env LAZY_BUNDLE_FETCHBUNDLE=1 node scripts/serve.mjs preview",
"preview:producer": "rspeedy preview --config lynx.config.producer.js"
},
"dependencies": {
Expand All @@ -22,7 +25,8 @@
"@lynx-js/qrcode-rsbuild-plugin": "workspace:*",
"@lynx-js/react-rsbuild-plugin": "workspace:*",
"@lynx-js/rspeedy": "workspace:*",
"@lynx-js/types": "3.7.0",
"@types/react": "^18.3.28"
"@lynx-js/types": "3.10.2-alpha.0",
"@types/react": "^18.3.28",
"cross-env": "^7.0.3"
}
}
45 changes: 35 additions & 10 deletions examples/react-lazy-bundle-standalone/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,40 @@ import { createProducerBundleUrl } from './entry-url.js';

import './App.css';

const LazyComponent = lazy(() =>
import(createProducerBundleUrl('LazyComponent.lynx.bundle'), {
with: {
type: 'component',
},
})
);
let LazyComponentDemo: () => JSX.Element;
if (__LAZY_BUNDLE_FETCHER__ === 'FetchBundle') {
const LazyComponentSync = lazy(() =>
import(createProducerBundleUrl('LazyComponentSync.lynx.bundle'), {
with: { type: 'component', mode: 'sync' },
})
);
const LazyComponentAsync = lazy(() =>
import(createProducerBundleUrl('LazyComponentAsync.lynx.bundle'), {
with: { type: 'component', mode: 'async' },
})
);
LazyComponentDemo = () => (
<>
<Suspense fallback={<text>Loading sync...</text>}>
<LazyComponentSync />
</Suspense>
<Suspense fallback={<text>Loading async...</text>}>
<LazyComponentAsync />
</Suspense>
</>
);
} else {
const LazyComponent = lazy(() =>
import(createProducerBundleUrl('LazyComponent.lynx.bundle'), {
with: { type: 'component' },
})
);
LazyComponentDemo = () => (
<Suspense fallback={<text>Loading...</text>}>
<LazyComponent />
</Suspense>
);
}

export function App() {
useEffect(() => {
Expand Down Expand Up @@ -43,9 +70,7 @@ export function App() {
<text className='Subtitle'>on Lynx</text>
</view>
<view className='Suspense'>
<Suspense fallback={<text>Loading...</text>}>
<LazyComponent />
</Suspense>
<LazyComponentDemo />
</view>
</view>
</view>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.LazyComponentAsync {
font-weight: 700;
color: cyan;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import './LazyComponentAsync.css';

export default function LazyComponentAsync() {
return (
<view>
<text className='LazyComponentAsync'>LazyComponentAsync</text>
</view>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.LazyComponentSync {
font-weight: 700;
color: yellow;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import './LazyComponentSync.css';

export default function LazyComponentSync() {
return (
<view>
<text className='LazyComponentSync'>LazyComponentSync</text>
</view>
);
}
6 changes: 4 additions & 2 deletions examples/react-lazy-bundle-standalone/src/entry-url.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export function createProducerBundleUrl(bundleFileName: string): string {
if (process.env.NODE_ENV === 'production') {
return `http://${process.env.LYNX_STANDALONE_PRODUCER_HOST}:${process.env.LYNX_STANDALONE_PRODUCER_PORT}/${bundleFileName}`;
if (process.env['NODE_ENV'] === 'production') {
return `http://${process.env['LYNX_STANDALONE_PRODUCER_HOST']}:${
process.env['LYNX_STANDALONE_PRODUCER_PORT']
}/${bundleFileName}`;
}
return `${__webpack_public_path__}producer/${bundleFileName}`;
}
28 changes: 27 additions & 1 deletion examples/react-lazy-bundle/lynx.config.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,38 @@
import os from 'node:os';

import { pluginQRCode } from '@lynx-js/qrcode-rsbuild-plugin';
import { pluginReactLynx } from '@lynx-js/react-rsbuild-plugin';
import { defineConfig } from '@lynx-js/rspeedy';

const enableBundleAnalysis = !!process.env['RSPEEDY_BUNDLE_ANALYSIS'];
const enableFetchBundle = !!process.env['LAZY_BUNDLE_FETCHBUNDLE'];

function detectLanHost() {
for (const ifaces of Object.values(os.networkInterfaces())) {
for (const iface of ifaces ?? []) {
if (iface.family === 'IPv4' && !iface.internal) {
return iface.address;
}
}
}
throw new Error('No external IPv4 interface found for lazy bundle host.');
}

const port = Number(process.env['LYNX_LAZY_BUNDLE_PORT'] ?? '54173');
const assetPrefix = `http://${detectLanHost()}:${port}/`;
Comment thread
upupming marked this conversation as resolved.

export default defineConfig({
output: {
assetPrefix,
},
server: {
port,
strictPort: true,
},
plugins: [
pluginReactLynx(),
pluginReactLynx({
...(enableFetchBundle ? { engineVersion: '3.8' } : {}),
}),
pluginQRCode({
schema(url) {
// We use `?fullscreen=true` to open the page in LynxExplorer in full screen mode
Expand Down
11 changes: 8 additions & 3 deletions examples/react-lazy-bundle/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
"type": "module",
"scripts": {
"build": "rspeedy build",
"dev": "rspeedy dev"
"build:fetchbundle": "cross-env LAZY_BUNDLE_FETCHBUNDLE=1 rspeedy build",
"dev": "rspeedy dev",
"dev:fetchbundle": "cross-env LAZY_BUNDLE_FETCHBUNDLE=1 rspeedy dev",
"preview": "rspeedy preview",
"preview:fetchbundle": "cross-env LAZY_BUNDLE_FETCHBUNDLE=1 rspeedy preview"
},
"dependencies": {
"@lynx-js/react": "workspace:*"
Expand All @@ -15,7 +19,8 @@
"@lynx-js/qrcode-rsbuild-plugin": "workspace:*",
"@lynx-js/react-rsbuild-plugin": "workspace:*",
"@lynx-js/rspeedy": "workspace:*",
"@lynx-js/types": "3.7.0",
"@types/react": "^18.3.28"
"@lynx-js/types": "3.10.2-alpha.0",
"@types/react": "^18.3.28",
"cross-env": "^7.0.3"
}
}
31 changes: 27 additions & 4 deletions examples/react-lazy-bundle/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,32 @@ import { Suspense, lazy, useEffect } from '@lynx-js/react';

import './App.css';

const LazyComponent = lazy(() => import('./LazyComponent.js'));
let LazyComponentDemo: () => JSX.Element;
if (__LAZY_BUNDLE_FETCHER__ === 'FetchBundle') {
const LazyComponentSync = lazy(() =>
import('./LazyComponentSync.js', { with: { mode: 'sync' } })
);
const LazyComponentAsync = lazy(() =>
import('./LazyComponentAsync.js', { with: { mode: 'async' } })
);
LazyComponentDemo = () => (
<>
<Suspense fallback={<text>Loading sync...</text>}>
<LazyComponentSync />
</Suspense>
<Suspense fallback={<text>Loading async...</text>}>
<LazyComponentAsync />
</Suspense>
</>
);
} else {
const LazyComponent = lazy(() => import('./LazyComponent.js'));
LazyComponentDemo = () => (
<Suspense fallback={<text>Loading...</text>}>
<LazyComponent />
</Suspense>
);
}

export function App() {
useEffect(() => {
Expand All @@ -27,9 +52,7 @@ export function App() {
<text className='Subtitle'>on Lynx</text>
</view>
<view className='Suspense'>
<Suspense fallback={<text>Loading...</text>}>
<LazyComponent />
</Suspense>
<LazyComponentDemo />
</view>
</view>
</view>
Expand Down
4 changes: 4 additions & 0 deletions examples/react-lazy-bundle/src/LazyComponentAsync.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.LazyComponentAsync {
font-weight: 700;
color: cyan;
}
9 changes: 9 additions & 0 deletions examples/react-lazy-bundle/src/LazyComponentAsync.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import './LazyComponentAsync.css';

export default function LazyComponentAsync() {
return (
<view>
<text className='LazyComponentAsync'>LazyComponentAsync</text>
</view>
);
}
4 changes: 4 additions & 0 deletions examples/react-lazy-bundle/src/LazyComponentSync.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.LazyComponentSync {
font-weight: 700;
color: yellow;
}
Loading
Loading