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/better-crews-wash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lynx-js/react-webpack-plugin": patch
---

feat: add `experimental_isLazyBundle` option, it will disable snapshot HMR for standalone lazy bundle
5 changes: 5 additions & 0 deletions .changeset/short-crabs-lead.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lynx-js/react-rsbuild-plugin": patch
---

fix: resolve page crash on development mode when enabling `experimental_isLazyBundle: true`
5 changes: 5 additions & 0 deletions .changeset/vast-trains-wave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lynx-js/runtime-wrapper-webpack-plugin": patch
---

feat: add `experimental_isLazyBundle` option, it will disable lynxChunkEntries for standalone lazy bundle
39 changes: 31 additions & 8 deletions packages/rspeedy/plugin-react/src/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,12 @@ export function applyEntry(
// For non-Lynx environment, the entry is not deleted.
// So we do not put it in the intermediate.
: '',
getBackgroundFilename(entryName, environment.config, isProd),
getBackgroundFilename(
entryName,
environment.config,
isProd,
experimental_isLazyBundle,
),
)
const backgroundEntry = entryName

Expand All @@ -116,8 +121,10 @@ export function applyEntry(
})
.when(isDev && !isWeb, entry => {
const require = createRequire(import.meta.url)
// use prepend to make sure it does not affect the exports
// from the entry
entry
.add({
.prepend({
layer: LAYERS.MAIN_THREAD,
import: require.resolve(
'@lynx-js/css-extract-webpack-plugin/runtime/hotModuleReplacement.lepus.cjs',
Expand All @@ -131,19 +138,23 @@ export function applyEntry(
import: imports,
filename: backgroundName,
})
// in standalone lazy bundle mode, we do not add
// other entries to avoid wrongly exporting from other entries
.when(isDev && !isWeb, entry => {
// use prepend to make sure it does not affect the exports
// from the entry
entry
// This is aliased in `@lynx-js/rspeedy`
.add({
.prepend({
layer: LAYERS.BACKGROUND,
import: '@rspack/core/hot/dev-server',
})
.add({
.prepend({
layer: LAYERS.BACKGROUND,
import: '@lynx-js/webpack-dev-transport/client',
})
// This is aliased in `./refresh.ts`
.add({
.prepend({
layer: LAYERS.BACKGROUND,
import: '@lynx-js/react/refresh',
})
Expand Down Expand Up @@ -207,6 +218,7 @@ export function applyEntry(
targetSdkVersion,
// Inject runtime wrapper for all `.js` but not `main-thread.js` and `main-thread.[hash].js`.
test: /^(?!.*main-thread(?:\.[A-Fa-f0-9]*)?\.js$).*\.js$/,
experimental_isLazyBundle,
}])
.end()
.plugin(`${LynxEncodePlugin.name}`)
Expand Down Expand Up @@ -282,6 +294,7 @@ function getBackgroundFilename(
entryName: string,
config: NormalizedEnvironmentConfig,
isProd: boolean,
experimental_isLazyBundle: boolean,
): string {
const { filename } = config.output

Expand All @@ -290,18 +303,28 @@ function getBackgroundFilename(
.replaceAll('[name]', entryName)
.replaceAll('.js', '/background.js')
} else {
return `${entryName}/background${getHash(config, isProd)}.js`
return `${entryName}/background${
getHash(config, isProd, experimental_isLazyBundle)
}.js`
}
}

function getHash(config: NormalizedEnvironmentConfig, isProd: boolean): string {
function getHash(
config: NormalizedEnvironmentConfig,
isProd: boolean,
experimental_isLazyBundle: boolean,
): string {
if (typeof config.output?.filenameHash === 'string') {
return config.output.filenameHash
? `.[${config.output.filenameHash}]`
: EMPTY_HASH
} else if (config.output?.filenameHash === false) {
return EMPTY_HASH
} else if (isProd) {
} else if (isProd || experimental_isLazyBundle) {
// In standalone lazy bundle mode, due to an internal bug of `lynx.requireModule`,
// it will cache module with same path (eg. `/.rspeedy/main/background.js`)
// even they have different entryName (eg. `__Card__` and `http://[ip]:[port]/main/template.js`)
// we need add hash (`/.rspeedy/main/background.[hash].js`) to avoid module conflict with the lazy bundle consumer.
return DEFAULT_FILENAME_HASH
} else {
return EMPTY_HASH
Expand Down
81 changes: 73 additions & 8 deletions packages/rspeedy/plugin-react/test/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -991,18 +991,18 @@ describe('Config', () => {
"main": {
"filename": ".rspeedy/main/background.js",
"import": [
"./fixtures/basic.tsx",
"@rspack/core/hot/dev-server",
"@lynx-js/webpack-dev-transport/client",
"@lynx-js/react/refresh",
"@lynx-js/webpack-dev-transport/client",
"@rspack/core/hot/dev-server",
"./fixtures/basic.tsx",
],
"layer": "react:background",
},
"main__main-thread": {
"filename": ".rspeedy/main/main-thread.js",
"import": [
"./fixtures/basic.tsx",
"<WORKSPACE>/packages/webpack/css-extract-webpack-plugin/runtime/hotModuleReplacement.lepus.cjs",
"./fixtures/basic.tsx",
],
"layer": "react:main-thread",
},
Expand Down Expand Up @@ -1041,18 +1041,18 @@ describe('Config', () => {
"main": {
"filename": ".rspeedy/main/background.[contenthash].js",
"import": [
"./fixtures/basic.tsx",
"@rspack/core/hot/dev-server",
"@lynx-js/webpack-dev-transport/client",
"@lynx-js/react/refresh",
"@lynx-js/webpack-dev-transport/client",
"@rspack/core/hot/dev-server",
"./fixtures/basic.tsx",
],
"layer": "react:background",
},
"main__main-thread": {
"filename": ".rspeedy/main/main-thread.js",
"import": [
"./fixtures/basic.tsx",
"<WORKSPACE>/packages/webpack/css-extract-webpack-plugin/runtime/hotModuleReplacement.lepus.cjs",
"./fixtures/basic.tsx",
],
"layer": "react:main-thread",
},
Expand Down Expand Up @@ -1550,6 +1550,71 @@ describe('Config', () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
expect(templatePlugin?.options.targetSdkVersion).toBe('3.4')
})
;['development', 'production'].forEach(mode => {
test(`lazyBundle on ${mode} mode`, async () => {
vi.stubEnv('NODE_ENV', mode)

const { pluginReactLynx } = await import('../src/pluginReactLynx.js')

const rsbuild = await createRspeedy({
rspeedyConfig: {
plugins: [
pluginReactLynx({
experimental_isLazyBundle: true,
}),
pluginStubRspeedyAPI(),
],
},
})

const [config] = await rsbuild.initConfigs()
if (mode === 'development') {
expect(config?.entry).toMatchInlineSnapshot(`
{
"main": {
"filename": ".rspeedy/main/background.[contenthash:8].js",
"import": [
"@lynx-js/react/refresh",
"@lynx-js/webpack-dev-transport/client",
"@rspack/core/hot/dev-server",
"./src/index.js",
],
"layer": "react:background",
},
"main__main-thread": {
"filename": ".rspeedy/main/main-thread.js",
"import": [
"<WORKSPACE>/packages/webpack/css-extract-webpack-plugin/runtime/hotModuleReplacement.lepus.cjs",
"./src/index.js",
],
"layer": "react:main-thread",
},
}
`)
} else {
expect(config?.entry).toMatchInlineSnapshot(`
{
"main": {
"filename": ".rspeedy/main/background.[contenthash:8].js",
"import": [
"./src/index.js",
],
"layer": "react:background",
},
"main__main-thread": {
"filename": ".rspeedy/main/main-thread.js",
"import": [
"./src/index.js",
],
"layer": "react:main-thread",
},
}
`)
}

vi.unstubAllEnvs()
})
})
})

describe('MPA Config', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function LazyBundleComp() {
return (
<view>
<text>Hello from lazy bundle!</text>
</view>
)
}
110 changes: 109 additions & 1 deletion packages/rspeedy/plugin-react/test/lazy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
// 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 type { Rspack } from '@rsbuild/core'
import { describe, expect, test, vi } from 'vitest'

import { createRspeedy } from '@lynx-js/rspeedy'

import { pluginStubRspeedyAPI } from './stub-rspeedy-api.plugin.js'

Expand Down Expand Up @@ -65,4 +68,109 @@ describe('Lazy', () => {

expect(config?.output?.library).toHaveProperty('type', 'commonjs')
})
;['development', 'production'].forEach(mode => {
test(`exports should have the component exported on ${mode} mode`, async () => {
vi.stubEnv('NODE_ENV', mode)

const { pluginReactLynx } = await import('../src/pluginReactLynx.js')
let backgroundJSContent = ''

const rsbuild = await createRspeedy({
rspeedyConfig: {
source: {
entry: {
main: new URL(
'./fixtures/standalone-lazy-bundle/index.tsx',
import.meta.url,
)
.pathname,
},
},
output: {
distPath: {
root: new URL(
'./dist/standalone-lazy-bundle',
import.meta.url,
).pathname,
},
},
plugins: [
pluginReactLynx({
experimental_isLazyBundle: true,
}),
],
tools: {
rspack: {
plugins: [
{
name: 'extractBackgroundJSContent',
apply(compiler) {
compiler.hooks.compilation.tap(
'extractBackgroundJSContent',
(compilation) => {
compilation.hooks.processAssets.tap(
'extractBackgroundJSContent',
(assets) => {
for (const key in assets) {
if (/background.*?\.js$/.test(key)) {
backgroundJSContent = assets[key]!.source()
.toString()!
}
}
},
)
},
)
},
} as Rspack.RspackPluginInstance,
],
},
},
},
})

await rsbuild.build()

const handler = {
get: function() {
return new Proxy(() => infiniteNestedObject, handler)
},
}
const infiniteNestedObject = new Proxy(
() => infiniteNestedObject,
handler,
)

// biome-ignore lint/suspicious/noExplicitAny: cache of modules
const mod: Record<string, any> = {}
// biome-ignore lint/suspicious/noExplicitAny: used to collect exports from lazy bundle
const exports: Record<string, any> = {}
// @ts-expect-error tt is used in eval of backgroundJSContent
// biome-ignore lint/correctness/noUnusedVariables: tt is used in eval of backgroundJSContent
const tt = {
define: (key: string, func: () => void) => {
mod[key] = func
},
require: (key: string) => {
// biome-ignore lint/suspicious/noExplicitAny: args passed to tt.define of lazy bundle
const args: any[] = Array(18).fill(0).map(() => infiniteNestedObject)
args[2] = exports
args[10] = console
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call
return mod[key](
...args,
)
},
}
eval(backgroundJSContent)

expect(exports).toHaveProperty(
'default',
)
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
expect(exports['default'].name).toBe('LazyBundleComp')

vi.unstubAllEnvs()
})
})
})
12 changes: 6 additions & 6 deletions packages/webpack/react-webpack-plugin/src/loaders/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,12 @@ function getCommonOptions(
snapshot: {
// TODO: config
preserveJsx: false,
target: 'MIXED',
// In standalone lazy bundle mode, we do not support HMR now.
target: this.hot && !isDynamicComponent
// Using `MIX` when HMR is enabled.
// This allows serializing the updated runtime code to Lepus using `Function.prototype.toString`.
? 'MIXED'
: 'JS',
runtimePkg: RUNTIME_PKG,
filename,
isDynamicComponent: isDynamicComponent ?? false,
Expand Down Expand Up @@ -277,11 +282,6 @@ export function getBackgroundTransformOptions(
snapshot: {
...commonOptions.snapshot,
jsxImportSource: JSX_IMPORT_SOURCE.BACKGROUND,
target: this.hot
// Using `MIX` when HMR is enabled.
// This allows serializing the updated runtime code to Lepus using `Function.prototype.toString`.
? 'MIXED'
: 'JS',
},
defineDCE: {
define: {
Expand Down
Loading
Loading