diff --git a/.changeset/fifty-rats-tickle.md b/.changeset/fifty-rats-tickle.md new file mode 100644 index 0000000000..0057675a01 --- /dev/null +++ b/.changeset/fifty-rats-tickle.md @@ -0,0 +1,5 @@ +--- +"@lynx-js/react-rsbuild-plugin": patch +--- + +Avoid IIFE in `main-thread.js` to resolve memory leak when using ``. diff --git a/.changeset/loose-zoos-say.md b/.changeset/loose-zoos-say.md new file mode 100644 index 0000000000..ec8a635ac9 --- /dev/null +++ b/.changeset/loose-zoos-say.md @@ -0,0 +1,5 @@ +--- +"@lynx-js/runtime-wrapper-webpack-plugin": patch +--- + +Wrap with IIFE when `output.iife: false` to avoid naming conflict. diff --git a/packages/rspeedy/plugin-react/src/pluginReactLynx.ts b/packages/rspeedy/plugin-react/src/pluginReactLynx.ts index 857074f6d5..2c6e8e3aeb 100644 --- a/packages/rspeedy/plugin-react/src/pluginReactLynx.ts +++ b/packages/rspeedy/plugin-react/src/pluginReactLynx.ts @@ -400,6 +400,14 @@ export function pluginReactLynx( }) } + // This is used to avoid the IIFE in main-thread.js, which would cause memory leak. + // TODO: remove this when required Rspeedy version bumped to ^0.10.0 + config = mergeRsbuildConfig({ + tools: { + rspack: { output: { iife: false } }, + }, + }, config) + return config }) diff --git a/packages/rspeedy/plugin-react/test/config.test.ts b/packages/rspeedy/plugin-react/test/config.test.ts index 5f2863440f..56f93cb863 100644 --- a/packages/rspeedy/plugin-react/test/config.test.ts +++ b/packages/rspeedy/plugin-react/test/config.test.ts @@ -1318,6 +1318,66 @@ describe('Config', () => { }) }) + describe('Output IIFE', () => { + test('defaults', async () => { + const { pluginReactLynx } = await import('../src/pluginReactLynx.js') + const rspeedy = await createRspeedy({ + rspeedyConfig: { + plugins: [ + pluginReactLynx(), + ], + }, + }) + + const [config] = await rspeedy.initConfigs() + + expect(config?.output?.iife).toBe(false) + }) + + test('with output.iife: false', async () => { + const { pluginReactLynx } = await import('../src/pluginReactLynx.js') + const rspeedy = await createRspeedy({ + rspeedyConfig: { + tools: { + rspack: { + output: { + iife: false, + }, + }, + }, + plugins: [ + pluginReactLynx(), + ], + }, + }) + + const [config] = await rspeedy.initConfigs() + + expect(config?.output?.iife).toBe(false) + }) + test('with output.iife: true', async () => { + const { pluginReactLynx } = await import('../src/pluginReactLynx.js') + const rspeedy = await createRspeedy({ + rspeedyConfig: { + tools: { + rspack: { + output: { + iife: true, + }, + }, + }, + plugins: [ + pluginReactLynx(), + ], + }, + }) + + const [config] = await rspeedy.initConfigs() + + expect(config?.output?.iife).toBe(true) + }) + }) + describe('Bundle Splitting', () => { test('default', async () => { const { pluginReactLynx } = await import('../src/pluginReactLynx.js') diff --git a/packages/webpack/runtime-wrapper-webpack-plugin/src/RuntimeWrapperWebpackPlugin.ts b/packages/webpack/runtime-wrapper-webpack-plugin/src/RuntimeWrapperWebpackPlugin.ts index af495a7bfe..86d4a75703 100644 --- a/packages/webpack/runtime-wrapper-webpack-plugin/src/RuntimeWrapperWebpackPlugin.ts +++ b/packages/webpack/runtime-wrapper-webpack-plugin/src/RuntimeWrapperWebpackPlugin.ts @@ -151,6 +151,7 @@ class RuntimeWrapperWebpackPluginImpl { if (typeof options.injectVars === 'function') { injectStr = options.injectVars(defaultInjectVars).join(','); } + const iife = compiler.options.output.iife ?? true; // banner new BannerPlugin({ @@ -168,6 +169,7 @@ class RuntimeWrapperWebpackPluginImpl { overrideRuntimePromise: true, moduleId: '[name].js', targetSdkVersion, + iife, }) // In standalone lazy bundle mode, the lazy bundle will // also has chunk.id "main", it will be conflict with the @@ -189,7 +191,7 @@ class RuntimeWrapperWebpackPluginImpl { const footer = this.#getBannerType(filename) === 'script' ? loadScriptFooter : loadBundleFooter; - return amdFooter('[name].js') + footer; + return amdFooter('[name].js', iife) + footer; }, }).apply(compiler); } @@ -253,12 +255,18 @@ const amdBanner = ({ moduleId, overrideRuntimePromise, targetSdkVersion, + iife, }: { injectStr: string; moduleId: string; overrideRuntimePromise: boolean; targetSdkVersion: string; + iife: boolean; }) => { + const iifeWrapper = iife ? '' : ` +// This needs to be wrapped in an IIFE because it needs to be isolated against Lynx injected variables. +(() => {`; + return ( ` tt.define("${moduleId}", function(require, module, exports, ${injectStr}) { @@ -270,11 +278,13 @@ ${overrideRuntimePromise ? `var Promise = lynx.Promise;` : ''} fetch = fetch || lynx.fetch; requestAnimationFrame = requestAnimationFrame || lynx.requestAnimationFrame; cancelAnimationFrame = cancelAnimationFrame || lynx.cancelAnimationFrame; +${iifeWrapper} ` ); }; -const amdFooter = (moduleId: string) => ` +const amdFooter = (moduleId: string, iife: boolean) => ` +${iife ? '' : '})();'} }); return tt.require("${moduleId}");`; diff --git a/packages/webpack/runtime-wrapper-webpack-plugin/test/cases/variables/iife-false/fetch.js b/packages/webpack/runtime-wrapper-webpack-plugin/test/cases/variables/iife-false/fetch.js new file mode 100644 index 0000000000..ee623f1b61 --- /dev/null +++ b/packages/webpack/runtime-wrapper-webpack-plugin/test/cases/variables/iife-false/fetch.js @@ -0,0 +1,3 @@ +export const fetch = function fetch() { + return 42; +}; diff --git a/packages/webpack/runtime-wrapper-webpack-plugin/test/cases/variables/iife-false/index.js b/packages/webpack/runtime-wrapper-webpack-plugin/test/cases/variables/iife-false/index.js new file mode 100644 index 0000000000..204f3ab1c1 --- /dev/null +++ b/packages/webpack/runtime-wrapper-webpack-plugin/test/cases/variables/iife-false/index.js @@ -0,0 +1,15 @@ +// Copyright 2025 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. + +import { fetch as myFetch } from './fetch.js'; + +expect(myFetch).not.toBe(lynx.fetch); + +expect(myFetch()).toBe(42); + +const fetch = () => { + return 42; +}; + +expect(fetch()).toBe(42); diff --git a/packages/webpack/runtime-wrapper-webpack-plugin/test/cases/variables/iife-false/rspack.config.js b/packages/webpack/runtime-wrapper-webpack-plugin/test/cases/variables/iife-false/rspack.config.js new file mode 100644 index 0000000000..a3db18f129 --- /dev/null +++ b/packages/webpack/runtime-wrapper-webpack-plugin/test/cases/variables/iife-false/rspack.config.js @@ -0,0 +1,8 @@ +/* +// Copyright 2024 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. +*/ +import config from './webpack.config.js'; + +export default config; diff --git a/packages/webpack/runtime-wrapper-webpack-plugin/test/cases/variables/iife-false/webpack.config.js b/packages/webpack/runtime-wrapper-webpack-plugin/test/cases/variables/iife-false/webpack.config.js new file mode 100644 index 0000000000..e70066f4a8 --- /dev/null +++ b/packages/webpack/runtime-wrapper-webpack-plugin/test/cases/variables/iife-false/webpack.config.js @@ -0,0 +1,20 @@ +/* +// Copyright 2024 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. +*/ +import { RuntimeWrapperWebpackPlugin } from '../../../../src'; + +/** @type {import('webpack').Configuration} */ +export default { + plugins: [ + new RuntimeWrapperWebpackPlugin(), + ], + output: { + iife: false, + }, + optimization: { + avoidEntryIife: true, + concatenateModules: true, + }, +}; diff --git a/packages/webpack/runtime-wrapper-webpack-plugin/test/cases/variables/iife-true/fetch.js b/packages/webpack/runtime-wrapper-webpack-plugin/test/cases/variables/iife-true/fetch.js new file mode 100644 index 0000000000..ee623f1b61 --- /dev/null +++ b/packages/webpack/runtime-wrapper-webpack-plugin/test/cases/variables/iife-true/fetch.js @@ -0,0 +1,3 @@ +export const fetch = function fetch() { + return 42; +}; diff --git a/packages/webpack/runtime-wrapper-webpack-plugin/test/cases/variables/iife-true/index.js b/packages/webpack/runtime-wrapper-webpack-plugin/test/cases/variables/iife-true/index.js new file mode 100644 index 0000000000..204f3ab1c1 --- /dev/null +++ b/packages/webpack/runtime-wrapper-webpack-plugin/test/cases/variables/iife-true/index.js @@ -0,0 +1,15 @@ +// Copyright 2025 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. + +import { fetch as myFetch } from './fetch.js'; + +expect(myFetch).not.toBe(lynx.fetch); + +expect(myFetch()).toBe(42); + +const fetch = () => { + return 42; +}; + +expect(fetch()).toBe(42); diff --git a/packages/webpack/runtime-wrapper-webpack-plugin/test/cases/variables/iife-true/rspack.config.js b/packages/webpack/runtime-wrapper-webpack-plugin/test/cases/variables/iife-true/rspack.config.js new file mode 100644 index 0000000000..a3db18f129 --- /dev/null +++ b/packages/webpack/runtime-wrapper-webpack-plugin/test/cases/variables/iife-true/rspack.config.js @@ -0,0 +1,8 @@ +/* +// Copyright 2024 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. +*/ +import config from './webpack.config.js'; + +export default config; diff --git a/packages/webpack/runtime-wrapper-webpack-plugin/test/cases/variables/iife-true/webpack.config.js b/packages/webpack/runtime-wrapper-webpack-plugin/test/cases/variables/iife-true/webpack.config.js new file mode 100644 index 0000000000..b3c91ae3b2 --- /dev/null +++ b/packages/webpack/runtime-wrapper-webpack-plugin/test/cases/variables/iife-true/webpack.config.js @@ -0,0 +1,20 @@ +/* +// Copyright 2024 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. +*/ +import { RuntimeWrapperWebpackPlugin } from '../../../../src'; + +/** @type {import('webpack').Configuration} */ +export default { + plugins: [ + new RuntimeWrapperWebpackPlugin(), + ], + output: { + iife: true, + }, + optimization: { + avoidEntryIife: true, + concatenateModules: true, + }, +};