From fd56e8dfc9b43b1deb043b018ff78cf0d4041773 Mon Sep 17 00:00:00 2001 From: Aditya-PS-05 Date: Mon, 7 Jul 2025 11:11:08 +0530 Subject: [PATCH] feat(template-webpack-plugin): apply inlineScripts to WebEncodePlugin --- packages/rspeedy/plugin-react/src/entry.ts | 10 ++- .../etc/template-webpack-plugin.api.md | 15 ++++ .../src/WebEncodePlugin.ts | 79 ++++++++++++++++++- .../template-webpack-plugin/src/index.ts | 1 + .../cases/web-inline-scripts/external/foo.js | 3 + .../web-inline-scripts/external/index.js | 39 +++++++++ .../external/rspack.config.js | 32 ++++++++ .../cases/web-inline-scripts/inline/foo.js | 3 + .../cases/web-inline-scripts/inline/index.js | 32 ++++++++ .../inline/rspack.config.js | 32 ++++++++ 10 files changed, 243 insertions(+), 3 deletions(-) create mode 100644 packages/webpack/template-webpack-plugin/test/cases/web-inline-scripts/external/foo.js create mode 100644 packages/webpack/template-webpack-plugin/test/cases/web-inline-scripts/external/index.js create mode 100644 packages/webpack/template-webpack-plugin/test/cases/web-inline-scripts/external/rspack.config.js create mode 100644 packages/webpack/template-webpack-plugin/test/cases/web-inline-scripts/inline/foo.js create mode 100644 packages/webpack/template-webpack-plugin/test/cases/web-inline-scripts/inline/index.js create mode 100644 packages/webpack/template-webpack-plugin/test/cases/web-inline-scripts/inline/rspack.config.js diff --git a/packages/rspeedy/plugin-react/src/entry.ts b/packages/rspeedy/plugin-react/src/entry.ts index 44556ee6a5..5126f92d71 100644 --- a/packages/rspeedy/plugin-react/src/entry.ts +++ b/packages/rspeedy/plugin-react/src/entry.ts @@ -241,9 +241,17 @@ export function applyEntry( } if (isWeb) { + let inlineScripts + if (experimental_isLazyBundle) { + // TODO: support inlineScripts in lazyBundle + inlineScripts = true + } else { + inlineScripts = environment.config.output?.inlineScripts ?? true + } + chain .plugin(PLUGIN_NAME_WEB) - .use(WebEncodePlugin, []) + .use(WebEncodePlugin, [{ inlineScripts }]) .end() } diff --git a/packages/webpack/template-webpack-plugin/etc/template-webpack-plugin.api.md b/packages/webpack/template-webpack-plugin/etc/template-webpack-plugin.api.md index 8c4b0d7a7a..49e96b67c2 100644 --- a/packages/webpack/template-webpack-plugin/etc/template-webpack-plugin.api.md +++ b/packages/webpack/template-webpack-plugin/etc/template-webpack-plugin.api.md @@ -171,10 +171,13 @@ export interface TemplateHooks { // // @public (undocumented) export class WebEncodePlugin { + constructor(options?: WebEncodePluginOptions); // (undocumented) apply(compiler: Compiler): void; // (undocumented) static BEFORE_ENCODE_HOOK_STAGE: number; + // (undocumented) + static defaultOptions: Readonly>; deleteDebuggingAssets(compilation: Compilation, assets: ({ name: string; } | undefined)[]): void; @@ -182,6 +185,18 @@ export class WebEncodePlugin { static ENCODE_HOOK_STAGE: number; // (undocumented) static name: string; + // (undocumented) + protected options: Required; +} + +// @public +export interface WebEncodePluginOptions { + // (undocumented) + encodeBinary?: string; + // Warning: (ae-forgotten-export) The symbol "InlineChunkConfig_2" needs to be exported by the entry point index.d.ts + // + // (undocumented) + inlineScripts?: InlineChunkConfig_2 | undefined; } // Warnings were encountered during analysis: diff --git a/packages/webpack/template-webpack-plugin/src/WebEncodePlugin.ts b/packages/webpack/template-webpack-plugin/src/WebEncodePlugin.ts index 00061e6e61..de30da1fed 100644 --- a/packages/webpack/template-webpack-plugin/src/WebEncodePlugin.ts +++ b/packages/webpack/template-webpack-plugin/src/WebEncodePlugin.ts @@ -11,11 +11,42 @@ import { } from './LynxTemplatePlugin.js'; import { genStyleInfo } from './web/genStyleInfo.js'; +// https://github.com/web-infra-dev/rsbuild/blob/main/packages/core/src/types/config.ts#L1029 +type InlineChunkTestFunction = (params: { + size: number; + name: string; +}) => boolean; +type InlineChunkTest = RegExp | InlineChunkTestFunction; +type InlineChunkConfig = boolean | InlineChunkTest | { + enable?: boolean | 'auto'; + test: InlineChunkTest; +}; + +/** + * The options for WebEncodePluginOptions + * + * @public + */ +export interface WebEncodePluginOptions { + encodeBinary?: string; + inlineScripts?: InlineChunkConfig | undefined; +} + export class WebEncodePlugin { static name = 'WebEncodePlugin'; static BEFORE_ENCODE_HOOK_STAGE = 100; static ENCODE_HOOK_STAGE = 100; + static defaultOptions: Readonly> = Object + .freeze>({ + encodeBinary: 'napi', + inlineScripts: true, + }); + + constructor(options: WebEncodePluginOptions = {}) { + this.options = { ...WebEncodePlugin.defaultOptions, ...options }; + } + apply(compiler: Compiler): void { const isDev = process.env['NODE_ENV'] === 'development' || compiler.options.mode === 'development'; @@ -50,13 +81,33 @@ export class WebEncodePlugin { }, (encodeOptions) => { const { encodeData } = encodeOptions; const { cssMap } = encodeData.css; + const { manifest } = encodeData; const styleInfo = genStyleInfo(cssMap); - const [name, content] = last(Object.entries(encodeData.manifest))!; + const [, content] = last(Object.entries(manifest))!; + + // Determine which assets should be inlined vs external + const inlinedAssetNames = new Set(); + const externalAssetNames = new Set(); + + Object.keys(manifest).forEach(manifestName => { + const assert = compilation.getAsset(manifestName); + const shouldInline = this.#shouldInlineScript( + manifestName, + assert!.source.size(), + ); + + if (shouldInline) { + inlinedAssetNames.add(manifestName); + } else { + externalAssetNames.add(manifestName); + } + }); if (!isDebug() && !isDev && !isRsdoctor()) { [ - { name }, + // Only add inlined assets to the deletion list + ...Array.from(inlinedAssetNames).map(name => ({ name })), encodeData.lepusCode.root, ...encodeData.lepusCode.chunks, ...encodeData.css.chunks, @@ -119,6 +170,30 @@ export class WebEncodePlugin { return compilation.deleteAsset(name); } } + + #shouldInlineScript(name: string, size: number): boolean { + const inlineConfig = this.options.inlineScripts; + + if (inlineConfig instanceof RegExp) { + return inlineConfig.test(name); + } + + if (typeof inlineConfig === 'function') { + return inlineConfig({ size, name }); + } + + if (typeof inlineConfig === 'object') { + if (inlineConfig.enable === false) return false; + if (inlineConfig.test instanceof RegExp) { + return inlineConfig.test.test(name); + } + return inlineConfig.test({ size, name }); + } + + return inlineConfig !== false; + } + + protected options: Required; } function last(array: T[]): T | undefined { diff --git a/packages/webpack/template-webpack-plugin/src/index.ts b/packages/webpack/template-webpack-plugin/src/index.ts index b93e092caf..ccd6515dd3 100644 --- a/packages/webpack/template-webpack-plugin/src/index.ts +++ b/packages/webpack/template-webpack-plugin/src/index.ts @@ -17,5 +17,6 @@ export type { export { LynxEncodePlugin } from './LynxEncodePlugin.js'; export type { LynxEncodePluginOptions } from './LynxEncodePlugin.js'; export { WebEncodePlugin } from './WebEncodePlugin.js'; +export type { WebEncodePluginOptions } from './WebEncodePlugin.js'; export * as CSSPlugins from './css/plugins/index.js'; export * as CSS from './css/index.js'; diff --git a/packages/webpack/template-webpack-plugin/test/cases/web-inline-scripts/external/foo.js b/packages/webpack/template-webpack-plugin/test/cases/web-inline-scripts/external/foo.js new file mode 100644 index 0000000000..5775bf0191 --- /dev/null +++ b/packages/webpack/template-webpack-plugin/test/cases/web-inline-scripts/external/foo.js @@ -0,0 +1,3 @@ +export function foo() { + return 42; +} diff --git a/packages/webpack/template-webpack-plugin/test/cases/web-inline-scripts/external/index.js b/packages/webpack/template-webpack-plugin/test/cases/web-inline-scripts/external/index.js new file mode 100644 index 0000000000..c309d37001 --- /dev/null +++ b/packages/webpack/template-webpack-plugin/test/cases/web-inline-scripts/external/index.js @@ -0,0 +1,39 @@ +/* +// 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 { existsSync } from 'node:fs'; +import { readFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; + +it('should have correct chunk content', async () => { + const { foo } = await import( + /* webpackChunkName: 'foo:background' */ + './foo.js' + ); + expect(foo()).toBe(42); +}); + +it('should generate correct web template with external scripts', async () => { + const outputPath = resolve(__dirname, 'main.bundle'); + expect(existsSync(outputPath)).toBeTruthy(); + + const content = await readFile(outputPath, 'utf-8'); + const bundleData = JSON.parse(content); + + expect(bundleData).toHaveProperty('manifest'); + expect(bundleData.manifest).toHaveProperty('/app-service.js'); + + // Check that external script file exists (this means scripts were not inlined) + const externalScriptPath = resolve( + __dirname, + 'foo:background.rspack.bundle.js', + ); + expect(existsSync(externalScriptPath)).toBeTruthy(); + + const externalContent = await readFile(externalScriptPath, 'utf-8'); + expect(externalContent).toContain('function foo()'); +}); diff --git a/packages/webpack/template-webpack-plugin/test/cases/web-inline-scripts/external/rspack.config.js b/packages/webpack/template-webpack-plugin/test/cases/web-inline-scripts/external/rspack.config.js new file mode 100644 index 0000000000..9eb822ec85 --- /dev/null +++ b/packages/webpack/template-webpack-plugin/test/cases/web-inline-scripts/external/rspack.config.js @@ -0,0 +1,32 @@ +import { LynxTemplatePlugin, WebEncodePlugin } from '../../../../src'; + +/** @type {import('@rspack/core').Configuration} */ +export default { + devtool: false, + mode: 'development', + plugins: [ + new WebEncodePlugin({ + inlineScripts: false, + }), + new LynxTemplatePlugin({ + ...LynxTemplatePlugin.defaultOptions, + intermediate: '.rspeedy/main', + }), + /** + * @param {import('@rspack/core').Compiler} compiler - Rspack Compiler + */ + (compiler) => { + compiler.hooks.thisCompilation.tap('test', (compilation) => { + const hooks = LynxTemplatePlugin.getLynxTemplatePluginHooks( + compilation, + ); + hooks.asyncChunkName.tap( + 'test', + chunkName => + chunkName + .replace(':background', ''), + ); + }); + }, + ], +}; diff --git a/packages/webpack/template-webpack-plugin/test/cases/web-inline-scripts/inline/foo.js b/packages/webpack/template-webpack-plugin/test/cases/web-inline-scripts/inline/foo.js new file mode 100644 index 0000000000..5775bf0191 --- /dev/null +++ b/packages/webpack/template-webpack-plugin/test/cases/web-inline-scripts/inline/foo.js @@ -0,0 +1,3 @@ +export function foo() { + return 42; +} diff --git a/packages/webpack/template-webpack-plugin/test/cases/web-inline-scripts/inline/index.js b/packages/webpack/template-webpack-plugin/test/cases/web-inline-scripts/inline/index.js new file mode 100644 index 0000000000..51623831d7 --- /dev/null +++ b/packages/webpack/template-webpack-plugin/test/cases/web-inline-scripts/inline/index.js @@ -0,0 +1,32 @@ +/* +// 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 { existsSync } from 'node:fs'; +import { readFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; + +it('should have correct chunk content', async () => { + const { foo } = await import( + /* webpackChunkName: 'foo:background' */ + './foo.js' + ); + expect(foo()).toBe(42); +}); + +it('should generate correct web template with inlined scripts', async () => { + const outputPath = resolve(__dirname, 'main.bundle'); + expect(existsSync(outputPath)).toBeTruthy(); + + const content = await readFile(outputPath, 'utf-8'); + const bundleData = JSON.parse(content); + + expect(bundleData).toHaveProperty('manifest'); + expect(bundleData.manifest).toHaveProperty('/app-service.js'); + + // When inlineScripts is true, the script content should be inlined + expect(bundleData.manifest['/app-service.js']).toContain('function foo()'); +}); diff --git a/packages/webpack/template-webpack-plugin/test/cases/web-inline-scripts/inline/rspack.config.js b/packages/webpack/template-webpack-plugin/test/cases/web-inline-scripts/inline/rspack.config.js new file mode 100644 index 0000000000..b0a9f28abe --- /dev/null +++ b/packages/webpack/template-webpack-plugin/test/cases/web-inline-scripts/inline/rspack.config.js @@ -0,0 +1,32 @@ +import { LynxTemplatePlugin, WebEncodePlugin } from '../../../../src'; + +/** @type {import('@rspack/core').Configuration} */ +export default { + devtool: false, + mode: 'development', + plugins: [ + new WebEncodePlugin({ + inlineScripts: true, + }), + new LynxTemplatePlugin({ + ...LynxTemplatePlugin.defaultOptions, + intermediate: '.rspeedy/main', + }), + /** + * @param {import('@rspack/core').Compiler} compiler - Rspack Compiler + */ + (compiler) => { + compiler.hooks.thisCompilation.tap('test', (compilation) => { + const hooks = LynxTemplatePlugin.getLynxTemplatePluginHooks( + compilation, + ); + hooks.asyncChunkName.tap( + 'test', + chunkName => + chunkName + .replace(':background', ''), + ); + }); + }, + ], +};