diff --git a/.changeset/afraid-meals-attend.md b/.changeset/afraid-meals-attend.md new file mode 100644 index 0000000000..44ffb80400 --- /dev/null +++ b/.changeset/afraid-meals-attend.md @@ -0,0 +1,21 @@ +--- +"@lynx-js/template-webpack-plugin": patch +"@lynx-js/react-rsbuild-plugin": patch +"@lynx-js/rspeedy": patch +--- + +Support `output.inlineScripts`, which controls whether to inline scripts into Lynx bundle (`.lynx.bundle`). + +Only background thread scripts can remain non-inlined, whereas the main thread script is always inlined. + +example: + +```js +import { defineConfig } from '@lynx-js/rspeedy'; + +export default defineConfig({ + output: { + inlineScripts: false, + }, +}); +``` diff --git a/packages/rspeedy/core/etc/rspeedy.api.md b/packages/rspeedy/core/etc/rspeedy.api.md index 4a93450508..03bcbaf0bd 100644 --- a/packages/rspeedy/core/etc/rspeedy.api.md +++ b/packages/rspeedy/core/etc/rspeedy.api.md @@ -232,6 +232,7 @@ export interface Output { distPath?: DistPath | undefined; filename?: string | Filename | undefined; filenameHash?: boolean | string | undefined; + inlineScripts?: boolean | undefined; legalComments?: 'none' | 'inline' | 'linked' | undefined; minify?: Minify | boolean | undefined; sourceMap?: boolean | SourceMap | undefined; diff --git a/packages/rspeedy/core/src/config/defaults.ts b/packages/rspeedy/core/src/config/defaults.ts index e387826ea1..1c1ef870b6 100644 --- a/packages/rspeedy/core/src/config/defaults.ts +++ b/packages/rspeedy/core/src/config/defaults.ts @@ -14,6 +14,9 @@ export function applyDefaultRspeedyConfig(config: Config): Config { // since some plugin(e.g.: `@lynx-js/qrcode-rsbuild-plugin`) will read // from the `output.filename.bundle` field. filename: getFilename(config.output?.filename), + + // inlineScripts should be enabled by default + inlineScripts: true, }, tools: { diff --git a/packages/rspeedy/core/src/config/output/index.ts b/packages/rspeedy/core/src/config/output/index.ts index 6b9b6e794e..82d8f66d2c 100644 --- a/packages/rspeedy/core/src/config/output/index.ts +++ b/packages/rspeedy/core/src/config/output/index.ts @@ -299,6 +299,32 @@ export interface Output { */ filenameHash?: boolean | string | undefined + /** + * The {@link Output.inlineScripts} option controls whether to inline scripts into Lynx bundle (`.lynx.bundle`). + * + * @remarks + * + * If no value is provided, the default value would be `true`. + * + * This is different with {@link https://rsbuild.dev/config/output/inline-scripts | output.inlineScripts } since we normally want to inline scripts in Lynx bundle (`.lynx.bundle`). + * + * Only background thread scripts can remain non-inlined, whereas the main thread script is always inlined. + * + * @example + * + * Disable inlining background thread scripts. + * ```js + * import { defineConfig } from '@lynx-js/rspeedy' + * + * export default defineConfig({ + * output: { + * inlineScripts: false, + * }, + * }) + * ``` + */ + inlineScripts?: boolean | undefined + /** * The {@link Output.legalComments} controls how to handle the legal comment. * diff --git a/packages/rspeedy/core/test/config/output.test-d.ts b/packages/rspeedy/core/test/config/output.test-d.ts index 32c5839bd6..3db9b06a53 100644 --- a/packages/rspeedy/core/test/config/output.test-d.ts +++ b/packages/rspeedy/core/test/config/output.test-d.ts @@ -175,6 +175,15 @@ describe('Config - Output', () => { }) }) + test('output.inlineScripts', () => { + assertType({ + inlineScripts: true, + }) + assertType({ + inlineScripts: false, + }) + }) + test('output.legalComments', () => { assertType({ legalComments: 'inline', diff --git a/packages/rspeedy/core/test/config/validate.test.ts b/packages/rspeedy/core/test/config/validate.test.ts index c53122907c..160ab75a1d 100644 --- a/packages/rspeedy/core/test/config/validate.test.ts +++ b/packages/rspeedy/core/test/config/validate.test.ts @@ -833,6 +833,8 @@ describe('Config Validation', () => { { distPath: { image: 'image' } }, { distPath: { font: 'font' } }, { distPath: { svg: 'svg' } }, + { inlineScripts: true }, + { inlineScripts: false }, { legalComments: 'inline' }, { legalComments: 'none' }, { legalComments: 'linked' }, @@ -1097,6 +1099,16 @@ describe('Config Validation', () => { ] `) + expect(() => validate({ output: { inlineScripts: null } })) + .toThrowErrorMatchingInlineSnapshot(` + [Error: Invalid configuration. + + Invalid config on \`$input.output.inlineScripts\`. + - Expect to be (boolean | undefined) + - Got: null + ] + `) + expect(() => validate({ output: { legalComments: [null] } })) .toThrowErrorMatchingInlineSnapshot(` [Error: Invalid configuration. diff --git a/packages/rspeedy/plugin-react/src/entry.ts b/packages/rspeedy/plugin-react/src/entry.ts index 12ea3a2ab7..bdda96ed15 100644 --- a/packages/rspeedy/plugin-react/src/entry.ts +++ b/packages/rspeedy/plugin-react/src/entry.ts @@ -198,6 +198,11 @@ export function applyEntry( }) if (isLynx) { + const inlineScripts = + typeof environment.config.output?.inlineScripts === 'boolean' + ? environment.config.output.inlineScripts + : true + chain .plugin(PLUGIN_NAME_RUNTIME_WRAPPER) .use(RuntimeWrapperWebpackPlugin, [{ @@ -222,7 +227,7 @@ export function applyEntry( }]) .end() .plugin(`${LynxEncodePlugin.name}`) - .use(LynxEncodePlugin, [{}]) + .use(LynxEncodePlugin, [{ 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 00a885e3b9..67edd3ffed 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 @@ -78,6 +78,10 @@ export class LynxEncodePlugin { // @public export interface LynxEncodePluginOptions { + // (undocumented) + encodeBinary?: string; + // (undocumented) + inlineScripts?: boolean; } // @public diff --git a/packages/webpack/template-webpack-plugin/src/LynxEncodePlugin.ts b/packages/webpack/template-webpack-plugin/src/LynxEncodePlugin.ts index 952894a2f0..5d5fb0fb4b 100644 --- a/packages/webpack/template-webpack-plugin/src/LynxEncodePlugin.ts +++ b/packages/webpack/template-webpack-plugin/src/LynxEncodePlugin.ts @@ -14,8 +14,10 @@ import type { CSS } from './index.js'; * * @public */ -// biome-ignore lint/suspicious/noEmptyInterface: As expected. -export interface LynxEncodePluginOptions {} +export interface LynxEncodePluginOptions { + encodeBinary?: string; + inlineScripts?: boolean; +} /** * LynxEncodePlugin @@ -92,6 +94,7 @@ export class LynxEncodePlugin { static defaultOptions: Readonly> = Object .freeze>({ encodeBinary: 'napi', + inlineScripts: true, }); /** * The entry point of a webpack plugin. @@ -146,6 +149,19 @@ export class LynxEncodePluginImpl { const { encodeData } = args; const { manifest } = encodeData; + let publicPath = '/'; + if (!this.options.inlineScripts) { + if (typeof compilation?.outputOptions.publicPath === 'function') { + compilation.errors.push( + new compiler.webpack.WebpackError( + '`publicPath` as a function is not supported yet.', + ), + ); + } else { + publicPath = compilation?.outputOptions.publicPath ?? '/'; + } + } + if (!isDebug() && !isDev && !isRsdoctor()) { [ encodeData.lepusCode.root, @@ -178,18 +194,20 @@ export class LynxEncodePluginImpl { Object.keys(manifest) .map((name) => `module.exports=lynx.requireModule('${ - this.#formatJSName(name) + this.#formatJSName(name, publicPath) }',globDynamicComponentEntry?globDynamicComponentEntry:'__Card__')` ) .join(','), this.#appServiceFooter(), ].join(''), - ...(Object.fromEntries( - Object.entries(manifest).map(([name, source]) => [ - this.#formatJSName(name), - source, - ]), - )), + ...(this.options.inlineScripts + ? Object.fromEntries( + Object.entries(manifest).map(([name, source]) => [ + this.#formatJSName(name, publicPath), + source, + ]), + ) + : {}), }; return args; @@ -230,8 +248,8 @@ export class LynxEncodePluginImpl { return amdFooter + loadScriptFooter; } - #formatJSName(name: string): string { - return `/${name}`; + #formatJSName(name: string, publicPath: string): string { + return publicPath + name; } protected options: Required; diff --git a/packages/webpack/template-webpack-plugin/test/cases/inline-scripts/external/foo.js b/packages/webpack/template-webpack-plugin/test/cases/inline-scripts/external/foo.js new file mode 100644 index 0000000000..5775bf0191 --- /dev/null +++ b/packages/webpack/template-webpack-plugin/test/cases/inline-scripts/external/foo.js @@ -0,0 +1,3 @@ +export function foo() { + return 42; +} diff --git a/packages/webpack/template-webpack-plugin/test/cases/inline-scripts/external/index.js b/packages/webpack/template-webpack-plugin/test/cases/inline-scripts/external/index.js new file mode 100644 index 0000000000..f077b8f24d --- /dev/null +++ b/packages/webpack/template-webpack-plugin/test/cases/inline-scripts/external/index.js @@ -0,0 +1,46 @@ +/* +// 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:main-thread' */ + './foo.js' + ); + expect(foo()).toBe(42); + + const fooBackground = await import( + /* webpackChunkName: 'foo:background' */ + './foo.js' + ); + expect(fooBackground.foo()).toBe(42); +}); + +it('manifest only contains /app-service.js', async () => { + const tasmJSONPath = resolve(__dirname, '.rspeedy/async/foo/tasm.json'); + expect(existsSync(tasmJSONPath)).toBeTruthy(); + + const content = await readFile(tasmJSONPath, 'utf-8'); + const { sourceContent, manifest } = JSON.parse(content); + const output = resolve(__dirname, 'foo:background.rspack.bundle.js'); + expect(existsSync(output)); + + const outputContent = await readFile(output, 'utf-8'); + expect(outputContent).toContain(['function', 'foo()'].join(' ')); + + expect(sourceContent).toHaveProperty('appType', 'DynamicComponent'); + + expect(manifest).not.toHaveProperty('/foo:background.rspack.bundle.js'); + expect(manifest).toHaveProperty('/app-service.js'); + + expect(manifest['/app-service.js']).toContain( + `lynx.requireModule('/foo:background.rspack.bundle.js',globDynamicComponentEntry?globDynamicComponentEntry:'__Card__')`, + ); +}); diff --git a/packages/webpack/template-webpack-plugin/test/cases/inline-scripts/external/rspack.config.js b/packages/webpack/template-webpack-plugin/test/cases/inline-scripts/external/rspack.config.js new file mode 100644 index 0000000000..f889279c21 --- /dev/null +++ b/packages/webpack/template-webpack-plugin/test/cases/inline-scripts/external/rspack.config.js @@ -0,0 +1,33 @@ +import { LynxEncodePlugin, LynxTemplatePlugin } from '../../../../src'; + +/** @type {import('@rspack/core').Configuration} */ +export default { + devtool: false, + mode: 'development', + plugins: [ + new LynxEncodePlugin({ + 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(':main-thread', '') + .replace(':background', ''), + ); + }); + }, + ], +}; diff --git a/packages/webpack/template-webpack-plugin/test/cases/inline-scripts/inline/foo.js b/packages/webpack/template-webpack-plugin/test/cases/inline-scripts/inline/foo.js new file mode 100644 index 0000000000..5775bf0191 --- /dev/null +++ b/packages/webpack/template-webpack-plugin/test/cases/inline-scripts/inline/foo.js @@ -0,0 +1,3 @@ +export function foo() { + return 42; +} diff --git a/packages/webpack/template-webpack-plugin/test/cases/inline-scripts/inline/index.js b/packages/webpack/template-webpack-plugin/test/cases/inline-scripts/inline/index.js new file mode 100644 index 0000000000..168236f1d2 --- /dev/null +++ b/packages/webpack/template-webpack-plugin/test/cases/inline-scripts/inline/index.js @@ -0,0 +1,37 @@ +/* +// 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:main-thread' */ + './foo.js' + ); + expect(foo()).toBe(42); + + const fooBackground = await import( + /* webpackChunkName: 'foo:background' */ + './foo.js' + ); + expect(fooBackground.foo()).toBe(42); +}); + +it('should generate correct foo template', async () => { + const tasmJSONPath = resolve(__dirname, '.rspeedy/async/foo/tasm.json'); + expect(existsSync(tasmJSONPath)).toBeTruthy(); + + const content = await readFile(tasmJSONPath, 'utf-8'); + const { sourceContent, manifest } = JSON.parse(content); + expect(sourceContent).toHaveProperty('appType', 'DynamicComponent'); + expect(manifest).toHaveProperty( + '/foo:background.rspack.bundle.js', + expect.stringContaining('function foo()'), + ); +}); diff --git a/packages/webpack/template-webpack-plugin/test/cases/inline-scripts/inline/rspack.config.js b/packages/webpack/template-webpack-plugin/test/cases/inline-scripts/inline/rspack.config.js new file mode 100644 index 0000000000..93b6e8b86e --- /dev/null +++ b/packages/webpack/template-webpack-plugin/test/cases/inline-scripts/inline/rspack.config.js @@ -0,0 +1,33 @@ +import { LynxEncodePlugin, LynxTemplatePlugin } from '../../../../src'; + +/** @type {import('@rspack/core').Configuration} */ +export default { + devtool: false, + mode: 'development', + plugins: [ + new LynxEncodePlugin({ + 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(':main-thread', '') + .replace(':background', ''), + ); + }); + }, + ], +};