diff --git a/.changeset/flat-hats-flow.md b/.changeset/flat-hats-flow.md new file mode 100644 index 0000000000..ab2b2c9bb8 --- /dev/null +++ b/.changeset/flat-hats-flow.md @@ -0,0 +1,5 @@ +--- +"@lynx-js/chunk-loading-webpack-plugin": patch +--- + +Add `StartupChunkDependenciesRuntimeModule` to fix `RuntimeGlobals.ensureChunkHandler` not found when using chunk splitting diff --git a/packages/webpack/chunk-loading-webpack-plugin/src/ChunkLoadingWebpackPlugin.ts b/packages/webpack/chunk-loading-webpack-plugin/src/ChunkLoadingWebpackPlugin.ts index 9c164e4d2e..ad6723db3e 100644 --- a/packages/webpack/chunk-loading-webpack-plugin/src/ChunkLoadingWebpackPlugin.ts +++ b/packages/webpack/chunk-loading-webpack-plugin/src/ChunkLoadingWebpackPlugin.ts @@ -8,11 +8,13 @@ import { RuntimeGlobals as LynxRuntimeGlobals } from '@lynx-js/webpack-runtime-g import { createChunkLoadingRuntimeModule } from './ChunkLoadingRuntimeModule.js'; import { createCssChunkLoadingRuntimeModule } from './CssChunkLoadingRuntimeModule.js'; +import { StartupChunkDependenciesPlugin } from './StartupChunkDependenciesPlugin.js'; import type { ChunkLoadingWebpackPluginOptions } from './index.js'; export class ChunkLoadingWebpackPluginImpl { name = 'ChunkLoadingWebpackPlugin'; + _asyncChunkLoading = true; static chunkLoadingValue = 'lynx'; @@ -34,6 +36,11 @@ export class ChunkLoadingWebpackPluginImpl { return; } + new StartupChunkDependenciesPlugin({ + chunkLoading: ChunkLoadingWebpackPluginImpl.chunkLoadingValue, + asyncChunkLoading: this._asyncChunkLoading, + }).apply(compiler); + // javascript chunk loading compiler.hooks.thisCompilation.tap(this.name, (compilation) => { const ChunkLoadingRuntimeModule = createChunkLoadingRuntimeModule( diff --git a/packages/webpack/chunk-loading-webpack-plugin/src/StartupChunkDependenciesPlugin.ts b/packages/webpack/chunk-loading-webpack-plugin/src/StartupChunkDependenciesPlugin.ts new file mode 100644 index 0000000000..57537b4312 --- /dev/null +++ b/packages/webpack/chunk-loading-webpack-plugin/src/StartupChunkDependenciesPlugin.ts @@ -0,0 +1,98 @@ +// 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 type { Chunk, Compiler } from 'webpack'; + +import { createStartupChunkDependenciesRuntimeModule } from './StartupChunkDependenciesRuntimeModule.js'; +import { createStartupEntrypointRuntimeModule } from './StartupEntrypointRuntimeModule.js'; + +/** + * The options for StartupChunkDependenciesPlugin + */ +interface StartupChunkDependenciesPluginOptions { + /** + * Specifies the chunk loading method + * @defaultValue 'lynx' + * @remarks Currently only 'lynx' is supported + */ + chunkLoading: string; + + /** + * Whether to enable async chunk loading + * @defaultValue true + * @remarks Currently only async loading mode is supported + */ + asyncChunkLoading: boolean; +} + +const PLUGIN_NAME = 'StartupChunkDependenciesPlugin'; + +export class StartupChunkDependenciesPlugin { + chunkLoading: string; + asyncChunkLoading: boolean; + + constructor( + public options: StartupChunkDependenciesPluginOptions, + ) { + this.chunkLoading = options.chunkLoading; + this.asyncChunkLoading = typeof options.asyncChunkLoading === 'boolean' + ? options.asyncChunkLoading + : true; + } + + apply(compiler: Compiler): void { + const { RuntimeGlobals } = compiler.webpack; + + const StartupChunkDependenciesRuntimeModule = + createStartupChunkDependenciesRuntimeModule( + compiler.webpack, + ); + + const StartupEntrypointRuntimeModule = createStartupEntrypointRuntimeModule( + compiler.webpack, + ); + + compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => { + const globalChunkLoading = compilation.outputOptions.chunkLoading; + + const isEnabledForChunk = (chunk: Chunk): boolean => { + const options = chunk.getEntryOptions(); + const chunkLoading = options && options.chunkLoading !== undefined + ? options.chunkLoading + : globalChunkLoading; + return chunkLoading === this.chunkLoading; + }; + + compilation.hooks.additionalTreeRuntimeRequirements.tap( + PLUGIN_NAME, + (chunk, set) => { + if (!isEnabledForChunk(chunk)) return; + + if (compilation.chunkGraph.hasChunkEntryDependentChunks(chunk)) { + set.add(RuntimeGlobals.startup); + set.add(RuntimeGlobals.ensureChunk); + set.add(RuntimeGlobals.ensureChunkIncludeEntries); + compilation.addRuntimeModule( + chunk, + new StartupChunkDependenciesRuntimeModule(this.asyncChunkLoading), + ); + } + }, + ); + + compilation.hooks.runtimeRequirementInTree + .for(RuntimeGlobals.startupEntrypoint) + .tap(PLUGIN_NAME, (chunk, set) => { + if (!isEnabledForChunk(chunk)) return; + + set.add(RuntimeGlobals.require); + set.add(RuntimeGlobals.ensureChunk); + set.add(RuntimeGlobals.ensureChunkIncludeEntries); + compilation.addRuntimeModule( + chunk, + new StartupEntrypointRuntimeModule(this.asyncChunkLoading), + ); + }); + }); + } +} diff --git a/packages/webpack/chunk-loading-webpack-plugin/src/StartupChunkDependenciesRuntimeModule.ts b/packages/webpack/chunk-loading-webpack-plugin/src/StartupChunkDependenciesRuntimeModule.ts new file mode 100644 index 0000000000..313526ef03 --- /dev/null +++ b/packages/webpack/chunk-loading-webpack-plugin/src/StartupChunkDependenciesRuntimeModule.ts @@ -0,0 +1,78 @@ +// 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 type { RuntimeModule } from 'webpack'; + +type StartupChunkDependenciesRuntimeModule = new( + asyncChunkLoading: boolean, +) => RuntimeModule; + +const runtimeTemplateBasicFunction = (args: string, body: string[]) => { + return `(${args}) => {\n${body.join('\n')}\n}`; +}; + +export function createStartupChunkDependenciesRuntimeModule( + webpack: typeof import('webpack'), +): StartupChunkDependenciesRuntimeModule { + const { RuntimeGlobals, RuntimeModule, Template } = webpack; + return class StartupChunkDependenciesRuntimeModule extends RuntimeModule { + asyncChunkLoading: boolean; + + constructor(asyncChunkLoading: boolean) { + super('Lynx startup chunk dependencies', RuntimeModule.STAGE_ATTACH); + this.asyncChunkLoading = asyncChunkLoading; + } + + override generate(): string { + const chunkGraph = this.chunkGraph!; + const chunk = this.chunk!; + const chunkIds = Array.from( + chunkGraph.getChunkEntryDependentChunksIterable(chunk), + ).map(chunk => chunk.id); + let startupCode: string[]; + + if (this.asyncChunkLoading === false) { + startupCode = chunkIds + .map(id => `${RuntimeGlobals.ensureChunk}(${JSON.stringify(id)});`) + .concat('return next();'); + // lazy bundle can't exports Promise + // TODO: handle Promise in lazy bundle exports to support chunk splitting + } else if (chunkIds.length === 0) { + startupCode = ['return next();']; + } else if (chunkIds.length === 1) { + startupCode = [ + `return ${RuntimeGlobals.ensureChunk}(${ + JSON.stringify(chunkIds[0]) + }).then(next);`, + ]; + } else if (chunkIds.length > 2) { + startupCode = [ + `return Promise.all(${ + JSON.stringify(chunkIds) + }.map(${RuntimeGlobals.ensureChunk}, ${RuntimeGlobals.require})).then(next);`, + ]; + } else { + startupCode = [ + 'return Promise.all([', + Template.indent( + chunkIds.map(id => + `${RuntimeGlobals.ensureChunk}(${JSON.stringify(id)})` + ).join(',\n'), + ), + ']).then(next);', + ]; + } + + return Template.asString([ + `var next = ${RuntimeGlobals.startup};`, + `${RuntimeGlobals.startup} = ${ + runtimeTemplateBasicFunction( + '', + startupCode, + ) + };`, + ]); + } + }; +} diff --git a/packages/webpack/chunk-loading-webpack-plugin/src/StartupEntrypointRuntimeModule.ts b/packages/webpack/chunk-loading-webpack-plugin/src/StartupEntrypointRuntimeModule.ts new file mode 100644 index 0000000000..42b750a037 --- /dev/null +++ b/packages/webpack/chunk-loading-webpack-plugin/src/StartupEntrypointRuntimeModule.ts @@ -0,0 +1,59 @@ +// 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 type { RuntimeModule } from 'webpack'; + +type StartupEntrypointRuntimeModule = new( + asyncChunkLoading: boolean, +) => RuntimeModule; + +const runtimeTemplateBasicFunction = (args: string, body: string[]) => { + return `(${args}) => {\n${body.join('\n')}\n}`; +}; + +const runtimeTemplateReturningFunction = (returnValue: string, args = '') => { + return `(${args}) => (${returnValue})`; +}; + +export function createStartupEntrypointRuntimeModule( + webpack: typeof import('webpack'), +): StartupEntrypointRuntimeModule { + const { RuntimeGlobals, RuntimeModule } = webpack; + return class StartupEntrypointRuntimeModule extends RuntimeModule { + asyncChunkLoading: boolean; + + constructor(asyncChunkLoading: boolean) { + super('Lynx startup entrypoint', RuntimeModule.STAGE_ATTACH); + this.asyncChunkLoading = asyncChunkLoading; + } + + override generate(): string { + return `${RuntimeGlobals.startupEntrypoint} = ${ + runtimeTemplateBasicFunction('result, chunkIds, fn', [ + '// arguments: chunkIds, moduleId are deprecated', + 'var moduleId = chunkIds;', + `if(!fn) chunkIds = result, fn = ${ + runtimeTemplateReturningFunction( + `${RuntimeGlobals.require}(${RuntimeGlobals.entryModuleId} = moduleId)`, + ) + };`, + ...(this.asyncChunkLoading + ? [ + `return Promise.all(chunkIds.map(${RuntimeGlobals.ensureChunk}, ${RuntimeGlobals.require})).then(${ + runtimeTemplateBasicFunction('', [ + 'var r = fn();', + 'return r === undefined ? result : r;', + ]) + })`, + ] + : [ + `chunkIds.map(${RuntimeGlobals.ensureChunk}, ${RuntimeGlobals.require})`, + 'var r = fn();', + 'return r === undefined ? result : r;', + ]), + ]) + }`; + } + }; +}