Skip to content
5 changes: 5 additions & 0 deletions .changeset/flat-hats-flow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lynx-js/chunk-loading-webpack-plugin": patch
---

Add `StartupChunkDependenciesRuntimeModule` to fix `RuntimeGlobals.ensureChunkHandler` not found when using chunk splitting
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// 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';

/**
* 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,
);

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;
set.add(RuntimeGlobals.startup);
set.add(RuntimeGlobals.ensureChunk);
set.add(RuntimeGlobals.ensureChunkIncludeEntries);
compilation.addRuntimeModule(
chunk,
new StartupChunkDependenciesRuntimeModule(this.asyncChunkLoading),
);
},
);
});
}
}
Original file line number Diff line number Diff line change
@@ -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 ChunkLoadingRuntimeModule 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,
)
};`,
]);
}
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,17 @@ it('should work with chunk loading require', async function() {
expect(lynx.requireModuleAsync).toBeCalled();
});
});

it('should contain startup chunk dependencies code', async () => {
const bundlePath = path.join(
__dirname,
'rspack.bundle.js',
);
const content = await fs.promises.readFile(bundlePath, 'utf-8');
// why appears twice:
// 1. Injected by StartupChunkDependenciesPlugin
// 2. Existing in test case's own bundle
expect((content.match(/Lynx startup chunk dependencies/g) || []).length).toBe(
2,
);
});
Loading