diff --git a/.changeset/four-rice-guess.md b/.changeset/four-rice-guess.md new file mode 100644 index 0000000000..10b64b6c92 --- /dev/null +++ b/.changeset/four-rice-guess.md @@ -0,0 +1,5 @@ +--- +"@lynx-js/react": patch +--- + +Auto define lynx.loadLazyBundle when using `import(/* relative path */)`. diff --git a/examples/react-lazy-bundle/lynx.config.js b/examples/react-lazy-bundle/lynx.config.js new file mode 100644 index 0000000000..ed483a4677 --- /dev/null +++ b/examples/react-lazy-bundle/lynx.config.js @@ -0,0 +1,25 @@ +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']; + +export default defineConfig({ + plugins: [ + pluginReactLynx(), + pluginQRCode({ + schema(url) { + // We use `?fullscreen=true` to open the page in LynxExplorer in full screen mode + return `${url}?fullscreen=true`; + }, + }), + ], + environments: { + web: {}, + lynx: { + performance: { + profile: enableBundleAnalysis, + }, + }, + }, +}); diff --git a/examples/react-lazy-bundle/package.json b/examples/react-lazy-bundle/package.json new file mode 100644 index 0000000000..a4ad705743 --- /dev/null +++ b/examples/react-lazy-bundle/package.json @@ -0,0 +1,21 @@ +{ + "name": "@lynx-js/example-react-lazy-bundle", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "rspeedy build", + "dev": "rspeedy dev" + }, + "dependencies": { + "@lynx-js/react": "workspace:*" + }, + "devDependencies": { + "@lynx-js/preact-devtools": "^5.0.1-cf9aef5", + "@lynx-js/qrcode-rsbuild-plugin": "workspace:*", + "@lynx-js/react-rsbuild-plugin": "workspace:*", + "@lynx-js/rspeedy": "workspace:*", + "@lynx-js/types": "3.4.11", + "@types/react": "^18.3.25" + } +} diff --git a/examples/react-lazy-bundle/src/App.css b/examples/react-lazy-bundle/src/App.css new file mode 100644 index 0000000000..fc28b87906 --- /dev/null +++ b/examples/react-lazy-bundle/src/App.css @@ -0,0 +1,34 @@ +:root { + background-color: #000; + --color-text: #fff; +} + +.Background { + position: fixed; + background: radial-gradient( + 71.43% 62.3% at 46.43% 36.43%, + rgba(18, 229, 229, 0) 15%, + rgba(239, 155, 255, 0.3) 56.35%, + #ff6448 100% + ); + box-shadow: 0px 12.93px 28.74px 0px #ffd28db2 inset; + border-radius: 50%; + width: 200vw; + height: 200vw; + top: -60vw; + left: -14.27vw; + transform: rotate(15.25deg); +} + +.App { + position: relative; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +text { + color: var(--color-text); +} diff --git a/examples/react-lazy-bundle/src/App.tsx b/examples/react-lazy-bundle/src/App.tsx new file mode 100644 index 0000000000..732b018af6 --- /dev/null +++ b/examples/react-lazy-bundle/src/App.tsx @@ -0,0 +1,30 @@ +import { useEffect } from '@lynx-js/react'; + +import './App.css'; + +export function App() { + useEffect(() => { + console.info('Hello, ReactLynx'); + void import('./utils/add.js').then((res) => { + console.info('dynamic import add', res.add(1, 2)); + }); + void import('./utils/dynamic.js').then((res) => { + console.info('dynamic import dynamic'); + void res.dynamicAdd(1, 2).then(res => { + console.info('dynamic add', res); + }); + }); + }, []); + + return ( + + + + + React + on Lynx + + + + ); +} diff --git a/examples/react-lazy-bundle/src/index.tsx b/examples/react-lazy-bundle/src/index.tsx new file mode 100644 index 0000000000..2166171b41 --- /dev/null +++ b/examples/react-lazy-bundle/src/index.tsx @@ -0,0 +1,17 @@ +import '@lynx-js/preact-devtools'; +import '@lynx-js/react/debug'; +import { root } from '@lynx-js/react'; + +import { App } from './App.jsx'; + +void import('./utils/minus.js').then((res) => { + console.info('dynamic import minus', res.minus(1, 2)); +}); + +root.render( + , +); + +if (import.meta.webpackHot) { + import.meta.webpackHot.accept(); +} diff --git a/examples/react-lazy-bundle/src/rspeedy-env.d.ts b/examples/react-lazy-bundle/src/rspeedy-env.d.ts new file mode 100644 index 0000000000..1c813a68b0 --- /dev/null +++ b/examples/react-lazy-bundle/src/rspeedy-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/react-lazy-bundle/src/utils/add.ts b/examples/react-lazy-bundle/src/utils/add.ts new file mode 100644 index 0000000000..3b399665dc --- /dev/null +++ b/examples/react-lazy-bundle/src/utils/add.ts @@ -0,0 +1,3 @@ +export function add(a: number, b: number) { + return a + b; +} diff --git a/examples/react-lazy-bundle/src/utils/dynamic.ts b/examples/react-lazy-bundle/src/utils/dynamic.ts new file mode 100644 index 0000000000..9444d62c9d --- /dev/null +++ b/examples/react-lazy-bundle/src/utils/dynamic.ts @@ -0,0 +1,4 @@ +export async function dynamicAdd(a: number, b: number) { + const { add } = await import('./add.js'); + return add(a, b); +} diff --git a/examples/react-lazy-bundle/src/utils/minus.ts b/examples/react-lazy-bundle/src/utils/minus.ts new file mode 100644 index 0000000000..6fc0dba75c --- /dev/null +++ b/examples/react-lazy-bundle/src/utils/minus.ts @@ -0,0 +1,3 @@ +export function minus(a: number, b: number) { + return a - b; +} diff --git a/examples/react-lazy-bundle/tsconfig.json b/examples/react-lazy-bundle/tsconfig.json new file mode 100644 index 0000000000..cd63e98e01 --- /dev/null +++ b/examples/react-lazy-bundle/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "@lynx-js/react", + "noEmit": true, + + "allowJs": true, + "checkJs": true, + "isolatedDeclarations": false, + }, + "include": ["src", "lynx.config.js", "test", "vitest.config.ts"], + "references": [ + { "path": "../../packages/react/tsconfig.json" }, + { "path": "../../packages/rspeedy/core/tsconfig.build.json" }, + { "path": "../../packages/rspeedy/plugin-qrcode/tsconfig.build.json" }, + { "path": "../../packages/rspeedy/plugin-react/tsconfig.build.json" }, + ], +} diff --git a/packages/react/runtime/src/index.ts b/packages/react/runtime/src/index.ts index 760fb6fbd0..cf11301fc2 100644 --- a/packages/react/runtime/src/index.ts +++ b/packages/react/runtime/src/index.ts @@ -15,8 +15,8 @@ import { createRef, forwardRef, isValidElement, + lazy, memo, - lazy as preactLazy, useSyncExternalStore, } from 'preact/compat'; @@ -32,18 +32,12 @@ import { useRef, useState, } from './hooks/react.js'; -import { loadLazyBundle } from './lynx/lazy-bundle.js'; import { Suspense } from './lynx/suspense.js'; export { Component, createContext } from 'preact'; export { PureComponent } from 'preact/compat'; export * from './hooks/react.js'; -const lazy: typeof import('preact/compat').lazy = /*#__PURE__*/ (() => { - lynx.loadLazyBundle = loadLazyBundle; - return preactLazy; -})(); - /** * @internal */ diff --git a/packages/react/runtime/vitest.config.ts b/packages/react/runtime/vitest.config.ts index 5b0ce2a1be..7aa9e7857a 100644 --- a/packages/react/runtime/vitest.config.ts +++ b/packages/react/runtime/vitest.config.ts @@ -36,6 +36,7 @@ function transformReactLynxPlugin(): Plugin { filename: 'test', target: 'MIXED', }, + dynamicImport: false, // snapshot: true, directiveDCE: false, defineDCE: false, diff --git a/packages/react/testing-library/src/vitest.config.js b/packages/react/testing-library/src/vitest.config.js index 65ef28273c..c4f1cb40aa 100644 --- a/packages/react/testing-library/src/vitest.config.js +++ b/packages/react/testing-library/src/vitest.config.js @@ -234,6 +234,11 @@ export const createVitestConfig = async (options) => { target: 'MIXED', }, engineVersion: options?.engineVersion ?? '', + dynamicImport: { + injectLazyBundle: false, + layer: 'test', + runtimePkg: `${runtimePkgName}/internal`, + }, // snapshot: true, directiveDCE: false, defineDCE: false, diff --git a/packages/react/transform/__test__/fixture.spec.js b/packages/react/transform/__test__/fixture.spec.js index 19a0ac251d..ad28cf61b7 100644 --- a/packages/react/transform/__test__/fixture.spec.js +++ b/packages/react/transform/__test__/fixture.spec.js @@ -913,6 +913,57 @@ export default class App extends Component { }); describe('dynamic import', () => { + it('lazy import', async () => { + const result = await transformReactLynx(`await import("https://www/a.js", { with: { type: "component" } });`, { + pluginName: '', + filename: '', + sourcemap: false, + parserConfig: { + tsx: true, + }, + cssScope: false, + jsx: false, + directiveDCE: false, + defineDCE: false, + shake: false, + compat: false, + worklet: false, + refresh: false, + }); + expect(result.code).toMatchInlineSnapshot(` + "import "@lynx-js/react/experimental/lazy/import"; + import { __dynamicImport } from "@lynx-js/react/internal"; + await __dynamicImport("https://www/a.js", { + with: { + type: "component" + } + }); + " + `); + }); + it('inline import', async () => { + const result = await transformReactLynx(`await import(/*webpackChunkName: "./index.js-test"*/"./index.js");`, { + pluginName: '', + filename: '', + sourcemap: false, + parserConfig: { + tsx: true, + }, + cssScope: false, + jsx: false, + directiveDCE: false, + defineDCE: false, + shake: false, + compat: false, + worklet: false, + refresh: false, + }); + expect(result.code).toMatchInlineSnapshot(` + "import 'data:text/javascript;charset=utf-8,import { loadLazyBundle } from "@lynx-js/react/internal";lynx.loadLazyBundle = loadLazyBundle;'; + await import(/*webpackChunkName: "./index.js-test"*/ /*webpackChunkName: "./index.js-"*/ "./index.js"); + " + `); + }); it('badcase', async () => { const result = await transformReactLynx( `\ diff --git a/packages/react/transform/crates/swc_plugin_dynamic_import/lib.rs b/packages/react/transform/crates/swc_plugin_dynamic_import/lib.rs index 23aee05a41..8a793d85f3 100644 --- a/packages/react/transform/crates/swc_plugin_dynamic_import/lib.rs +++ b/packages/react/transform/crates/swc_plugin_dynamic_import/lib.rs @@ -26,6 +26,8 @@ pub struct DynamicImportVisitorConfig { pub runtime_pkg: String, /// @internal pub layer: String, + /// @internal + pub inject_lazy_bundle: Option, } impl Default for DynamicImportVisitorConfig { @@ -33,6 +35,7 @@ impl Default for DynamicImportVisitorConfig { DynamicImportVisitorConfig { layer: "".into(), runtime_pkg: "@lynx-js/react/internal".into(), + inject_lazy_bundle: Some(true), } } } @@ -42,6 +45,7 @@ where C: Comments, { opts: DynamicImportVisitorConfig, + has_inner_lazy_bundle: bool, named_imports: HashSet, comments: Option, } @@ -63,6 +67,7 @@ where DynamicImportVisitor { opts, comments, + has_inner_lazy_bundle: false, named_imports: HashSet::new(), } } @@ -109,6 +114,21 @@ fn is_import_call_with_type(call_expr: &CallExpr) -> (bool, bool, Value) { } } +fn create_import_decl(name: &str) -> ModuleItem { + ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl { + span: DUMMY_SP, + phase: ImportPhase::Evaluation, + specifiers: vec![], + src: Box::new(Str { + span: DUMMY_SP, + raw: None, + value: name.into(), + }), + type_only: Default::default(), + with: Default::default(), + })) +} + impl VisitMut for DynamicImportVisitor where C: Comments, @@ -196,6 +216,7 @@ where text: format!("webpackChunkName: \"{}-{}\"", str_lit, self.opts.layer).into(), }, ); + self.has_inner_lazy_bundle = true; } else { let ident: Ident = "__dynamicImport".into(); *call_expr = CallExpr { @@ -244,20 +265,20 @@ where prepend_stmt( &mut n.body, - ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl { - span: DUMMY_SP, - phase: ImportPhase::Evaluation, - specifiers: vec![], - src: Box::new(Str { - span: DUMMY_SP, - raw: None, - value: format!("{}/experimental/lazy/import", self.opts.runtime_pkg) - .replace("/internal", "") - .into(), - }), - type_only: Default::default(), - with: Default::default(), - })), + create_import_decl( + &format!("{}/experimental/lazy/import", self.opts.runtime_pkg).replace("/internal", ""), + ), + ); + } + if match self.opts.inject_lazy_bundle { + Some(true) => true, + Some(false) => false, + None => true, + } && self.has_inner_lazy_bundle + { + prepend_stmt( + &mut n.body, + create_import_decl("data:text/javascript;charset=utf-8,import { loadLazyBundle } from \"@lynx-js/react/internal\";lynx.loadLazyBundle = loadLazyBundle;"), ); } } @@ -282,6 +303,7 @@ mod tests { |t| visit_mut_pass(DynamicImportVisitor::new( DynamicImportVisitorConfig { layer: "test".into(), + inject_lazy_bundle: Some(false), ..Default::default() }, Some(t.comments.clone()) @@ -318,15 +340,52 @@ mod tests { }, Some(t.comments.clone()) )), - should_not_import_lazy, + should_import_lazy_import, + r#" + (async function () { + await import("https://www/a.js", { with: { type: "component" } }); + })(); + "# + ); + + test!( + module, + Syntax::Es(EsSyntax { + jsx: true, + ..Default::default() + }), + |t| visit_mut_pass(DynamicImportVisitor::new( + DynamicImportVisitorConfig { + layer: "test".into(), + ..Default::default() + }, + Some(t.comments.clone()) + )), + should_import_lazy_bundle, r#" (async function () { await import("./index.js"); - await import(`./locales/${name}`); - await import("ftp://www/a.js"); + })(); + "# + ); - await import("./index.js", { with: { type: "component" } }); - await import("ftp://www/a.js", { with: { type: "component" } }); + test!( + module, + Syntax::Es(EsSyntax { + jsx: true, + ..Default::default() + }), + |t| visit_mut_pass(DynamicImportVisitor::new( + DynamicImportVisitorConfig { + layer: "test".into(), + ..Default::default() + }, + Some(t.comments.clone()) + )), + should_not_import_lazy, + r#" + (async function () { + await import(`./locales/${name}`); })(); "# ); diff --git a/packages/react/transform/crates/swc_plugin_dynamic_import/napi.rs b/packages/react/transform/crates/swc_plugin_dynamic_import/napi.rs index dc587b6784..7aabe2ab1c 100644 --- a/packages/react/transform/crates/swc_plugin_dynamic_import/napi.rs +++ b/packages/react/transform/crates/swc_plugin_dynamic_import/napi.rs @@ -15,6 +15,8 @@ pub struct DynamicImportVisitorConfig { pub runtime_pkg: String, /// @internal pub layer: String, + /// @internal + pub inject_lazy_bundle: Option, } impl Default for DynamicImportVisitorConfig { @@ -22,6 +24,7 @@ impl Default for DynamicImportVisitorConfig { DynamicImportVisitorConfig { layer: "".into(), runtime_pkg: "@lynx-js/react/internal".into(), + inject_lazy_bundle: Some(true), } } } @@ -31,6 +34,7 @@ impl From for CoreConfig { CoreConfig { layer: val.layer, runtime_pkg: val.runtime_pkg, + inject_lazy_bundle: val.inject_lazy_bundle, } } } @@ -40,6 +44,7 @@ impl From for DynamicImportVisitorConfig { DynamicImportVisitorConfig { layer: val.layer, runtime_pkg: val.runtime_pkg, + inject_lazy_bundle: val.inject_lazy_bundle, } } } diff --git a/packages/react/transform/crates/swc_plugin_dynamic_import/tests/__swc_snapshots__/lib.rs/should_import_lazy_bundle.js b/packages/react/transform/crates/swc_plugin_dynamic_import/tests/__swc_snapshots__/lib.rs/should_import_lazy_bundle.js new file mode 100644 index 0000000000..b1c03cd932 --- /dev/null +++ b/packages/react/transform/crates/swc_plugin_dynamic_import/tests/__swc_snapshots__/lib.rs/should_import_lazy_bundle.js @@ -0,0 +1,4 @@ +import 'data:text/javascript;charset=utf-8,import { loadLazyBundle } from "@lynx-js/react/internal";lynx.loadLazyBundle = loadLazyBundle;'; +(async function() { + await import(/*webpackChunkName: "./index.js-test"*/ "./index.js"); +})(); diff --git a/packages/react/transform/crates/swc_plugin_dynamic_import/tests/__swc_snapshots__/lib.rs/should_import_lazy_import.js b/packages/react/transform/crates/swc_plugin_dynamic_import/tests/__swc_snapshots__/lib.rs/should_import_lazy_import.js new file mode 100644 index 0000000000..f30d17d4b4 --- /dev/null +++ b/packages/react/transform/crates/swc_plugin_dynamic_import/tests/__swc_snapshots__/lib.rs/should_import_lazy_import.js @@ -0,0 +1,9 @@ +import "@lynx-js/react/experimental/lazy/import"; +import { __dynamicImport } from "@lynx-js/react/internal"; +(async function() { + await __dynamicImport("https://www/a.js", { + with: { + type: "component" + } + }); +})(); diff --git a/packages/react/transform/crates/swc_plugin_dynamic_import/tests/__swc_snapshots__/lib.rs/should_not_import_lazy.js b/packages/react/transform/crates/swc_plugin_dynamic_import/tests/__swc_snapshots__/lib.rs/should_not_import_lazy.js index 1a8b2eb787..8ee0db9ef7 100644 --- a/packages/react/transform/crates/swc_plugin_dynamic_import/tests/__swc_snapshots__/lib.rs/should_not_import_lazy.js +++ b/packages/react/transform/crates/swc_plugin_dynamic_import/tests/__swc_snapshots__/lib.rs/should_not_import_lazy.js @@ -1,15 +1,3 @@ (async function() { - await import(/*webpackChunkName: "./index.js-test"*/ "./index.js"); await import(`./locales/${name}`); - await import(/*webpackChunkName: "ftp://www/a.js-test"*/ "ftp://www/a.js"); - await import(/*webpackChunkName: "./index.js-test"*/ "./index.js", { - with: { - type: "component" - } - }); - await import(/*webpackChunkName: "ftp://www/a.js-test"*/ "ftp://www/a.js", { - with: { - type: "component" - } - }); })(); diff --git a/packages/react/transform/index.d.ts b/packages/react/transform/index.d.ts index ed99d6b654..e0ca50cae6 100644 --- a/packages/react/transform/index.d.ts +++ b/packages/react/transform/index.d.ts @@ -424,6 +424,8 @@ export interface DynamicImportVisitorConfig { runtimePkg: string /** @internal */ layer: string + /** @internal */ + injectLazyBundle?: boolean } /** * {@inheritdoc PluginReactLynxOptions.extractStr} diff --git a/packages/react/transform/swc-plugin-reactlynx/index.d.ts b/packages/react/transform/swc-plugin-reactlynx/index.d.ts index d23a785cd5..6f9369cc2c 100644 --- a/packages/react/transform/swc-plugin-reactlynx/index.d.ts +++ b/packages/react/transform/swc-plugin-reactlynx/index.d.ts @@ -168,6 +168,8 @@ export interface DynamicImportVisitorConfig { runtimePkg: string; /** @internal */ layer: string; + /** @internal */ + injectLazyBundle?: boolean; } export interface DirectiveDceVisitorConfig { diff --git a/packages/webpack/react-webpack-plugin/test/cases/code-splitting/lazy-imports/index.jsx b/packages/webpack/react-webpack-plugin/test/cases/code-splitting/lazy-imports/index.jsx index 3d2e63dfdb..f3c8f9ad29 100644 --- a/packages/webpack/react-webpack-plugin/test/cases/code-splitting/lazy-imports/index.jsx +++ b/packages/webpack/react-webpack-plugin/test/cases/code-splitting/lazy-imports/index.jsx @@ -25,6 +25,7 @@ it('should have experimental/lazy/import imported', async () => { } catch { // ignore error } + expect(lynx.loadLazyBundle).not.toBeUndefined(); if (__BACKGROUND__) { expect(lynx[sExportsReact]).not.toBeUndefined(); expect(lynx[sExportsReactLepus]).not.toBeUndefined(); diff --git a/packages/webpack/react-webpack-plugin/test/cases/code-splitting/load-lazy-bundle/foo2.js b/packages/webpack/react-webpack-plugin/test/cases/code-splitting/load-lazy-bundle/foo2.js new file mode 100644 index 0000000000..c374ea9d66 --- /dev/null +++ b/packages/webpack/react-webpack-plugin/test/cases/code-splitting/load-lazy-bundle/foo2.js @@ -0,0 +1,3 @@ +export function foo2() { + return 50; +} diff --git a/packages/webpack/react-webpack-plugin/test/cases/code-splitting/load-lazy-bundle/index.jsx b/packages/webpack/react-webpack-plugin/test/cases/code-splitting/load-lazy-bundle/index.jsx index 64605eabba..6484e29e63 100644 --- a/packages/webpack/react-webpack-plugin/test/cases/code-splitting/load-lazy-bundle/index.jsx +++ b/packages/webpack/react-webpack-plugin/test/cases/code-splitting/load-lazy-bundle/index.jsx @@ -1,9 +1,8 @@ /// +export {}; -import { lazy } from '@lynx-js/react'; - -lazy(() => import('./foo.js')); - -it('should have lynx.loadLazyBundle', () => { +it('should have lynx.loadLazyBundle', async () => { + await import('./foo.js'); + await import('./foo2.js'); expect(lynx.loadLazyBundle).not.toBeUndefined(); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 478f123e36..9d990b1930 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -242,6 +242,31 @@ importers: specifier: 0.0.0-experimental-0566679-20250709 version: 0.0.0-experimental-0566679-20250709 + examples/react-lazy-bundle: + dependencies: + '@lynx-js/react': + specifier: workspace:* + version: link:../../packages/react + devDependencies: + '@lynx-js/preact-devtools': + specifier: ^5.0.1-cf9aef5 + version: 5.0.1 + '@lynx-js/qrcode-rsbuild-plugin': + specifier: workspace:* + version: link:../../packages/rspeedy/plugin-qrcode + '@lynx-js/react-rsbuild-plugin': + specifier: workspace:* + version: link:../../packages/rspeedy/plugin-react + '@lynx-js/rspeedy': + specifier: workspace:* + version: link:../../packages/rspeedy/core + '@lynx-js/types': + specifier: 3.4.11 + version: 3.4.11 + '@types/react': + specifier: ^18.3.25 + version: 18.3.25 + examples/tailwindcss: dependencies: '@lynx-js/react':