From 5afd81818dee34a714b97d7bc387f4de08e65000 Mon Sep 17 00:00:00 2001 From: kakiuwang-ui Date: Sat, 22 Aug 2026 00:22:58 +0800 Subject: [PATCH 1/6] feat(javascript): add JavascriptModulesPlugin.getChunkFilenameTemplate Aligns with webpack's JavascriptModulesPlugin.getChunkFilenameTemplate so plugins can resolve the filename template rspack renders a JS chunk with. Closes #10012 --- .../builtin-plugin/JavascriptModulesPlugin.ts | 27 +++++++ .../get-chunk-filename-template/async.js | 2 + .../get-chunk-filename-template/index.js | 7 ++ .../rspack.config.js | 72 +++++++++++++++++++ .../get-chunk-filename-template/shared.js | 1 + .../webpack/javascript-modules-plugin.mdx | 48 +++++++++++++ .../webpack/javascript-modules-plugin.mdx | 47 ++++++++++++ 7 files changed, 204 insertions(+) create mode 100644 tests/rspack-test/configCases/hooks/get-chunk-filename-template/async.js create mode 100644 tests/rspack-test/configCases/hooks/get-chunk-filename-template/index.js create mode 100644 tests/rspack-test/configCases/hooks/get-chunk-filename-template/rspack.config.js create mode 100644 tests/rspack-test/configCases/hooks/get-chunk-filename-template/shared.js diff --git a/packages/rspack/src/builtin-plugin/JavascriptModulesPlugin.ts b/packages/rspack/src/builtin-plugin/JavascriptModulesPlugin.ts index 8e2ea3057e73..57be77c5b220 100644 --- a/packages/rspack/src/builtin-plugin/JavascriptModulesPlugin.ts +++ b/packages/rspack/src/builtin-plugin/JavascriptModulesPlugin.ts @@ -3,6 +3,7 @@ import { type BuiltinPlugin, BuiltinPluginName } from '@rspack/binding'; import * as liteTapable from '@rspack/lite-tapable'; import type { Chunk } from '../Chunk'; import { type Compilation, checkCompilation } from '../Compilation'; +import type { Filename, OutputNormalized } from '../config'; import type Hash from '../util/hash'; import { createBuiltinPlugin, RspackBuiltinPlugin } from './base'; @@ -33,4 +34,30 @@ export class JavascriptModulesPlugin extends RspackBuiltinPlugin { } return hooks; } + + /** + * Returns the filename template that is used to render the JavaScript file + * of the given chunk. + * + * Mirrors the resolution order of `get_js_chunk_filename_template` in + * `crates/rspack_core/src/options/output.rs`, which itself aligns with + * webpack's `JavascriptModulesPlugin.getChunkFilenameTemplate`. + * + * Note: webpack additionally returns `output.hotUpdateChunkFilename` for + * `HotUpdateChunk` instances. Rspack keeps hot update chunks inside the Rust + * HMR pipeline and never surfaces them to the JavaScript side, so no chunk + * reachable from here can be a hot update chunk. + */ + static getChunkFilenameTemplate( + chunk: Chunk, + outputOptions: OutputNormalized, + ): Filename | undefined { + if (chunk.filenameTemplate) { + return chunk.filenameTemplate; + } + if (chunk.canBeInitial()) { + return outputOptions.filename; + } + return outputOptions.chunkFilename; + } } diff --git a/tests/rspack-test/configCases/hooks/get-chunk-filename-template/async.js b/tests/rspack-test/configCases/hooks/get-chunk-filename-template/async.js new file mode 100644 index 000000000000..03d036263e1b --- /dev/null +++ b/tests/rspack-test/configCases/hooks/get-chunk-filename-template/async.js @@ -0,0 +1,2 @@ +require('./shared'); +module.exports = 'async'; diff --git a/tests/rspack-test/configCases/hooks/get-chunk-filename-template/index.js b/tests/rspack-test/configCases/hooks/get-chunk-filename-template/index.js new file mode 100644 index 000000000000..9eaab253ca3f --- /dev/null +++ b/tests/rspack-test/configCases/hooks/get-chunk-filename-template/index.js @@ -0,0 +1,7 @@ +require("./shared"); + +it("should render every chunk with the template returned by getChunkFilenameTemplate", () => { + return import("./async").then(() => { + expect(true).toBe(true); + }); +}); diff --git a/tests/rspack-test/configCases/hooks/get-chunk-filename-template/rspack.config.js b/tests/rspack-test/configCases/hooks/get-chunk-filename-template/rspack.config.js new file mode 100644 index 000000000000..d7d30ed11c81 --- /dev/null +++ b/tests/rspack-test/configCases/hooks/get-chunk-filename-template/rspack.config.js @@ -0,0 +1,72 @@ +const { javascript } = require('@rspack/core'); + +const pluginName = 'plugin'; + +class Plugin { + apply(compiler) { + let called = false; + compiler.hooks.compilation.tap(pluginName, (compilation) => { + compilation.hooks.afterSeal.tap(pluginName, () => { + called = true; + + const templates = {}; + for (const chunk of compilation.chunks) { + const template = + javascript.JavascriptModulesPlugin.getChunkFilenameTemplate( + chunk, + compilation.outputOptions, + ); + templates[chunk.name || chunk.id] = template; + + // The returned template has to be the one rspack actually rendered + // the chunk with. + expect([...chunk.files]).toContain( + compilation.getPath(template, { + chunk, + contentHashType: 'javascript', + }), + ); + } + + expect(templates).toEqual({ + // initial chunk without its own template -> output.filename + main: '[name].js', + // async chunk without its own template -> output.chunkFilename + async_js: 'async-[name].js', + // chunk carrying its own template -> that template wins + 'shared-shared_js': 'shared-[name].js', + }); + }); + }); + compiler.hooks.done.tap(pluginName, (stats) => { + expect(stats.toJson().errors.length).toBe(0); + expect(called).toBe(true); + }); + } +} + +/**@type {import("@rspack/core").Configuration}*/ +module.exports = { + context: __dirname, + mode: 'development', + entry: './index.js', + target: 'node', + output: { + filename: '[name].js', + chunkFilename: 'async-[name].js', + }, + optimization: { + chunkIds: 'named', + splitChunks: { + cacheGroups: { + shared: { + chunks: 'all', + test: /shared/, + filename: 'shared-[name].js', + enforce: true, + }, + }, + }, + }, + plugins: [new Plugin()], +}; diff --git a/tests/rspack-test/configCases/hooks/get-chunk-filename-template/shared.js b/tests/rspack-test/configCases/hooks/get-chunk-filename-template/shared.js new file mode 100644 index 000000000000..88693e341609 --- /dev/null +++ b/tests/rspack-test/configCases/hooks/get-chunk-filename-template/shared.js @@ -0,0 +1 @@ +module.exports = 'shared'; diff --git a/website/docs/en/plugins/webpack/javascript-modules-plugin.mdx b/website/docs/en/plugins/webpack/javascript-modules-plugin.mdx index 1b754a4fd2b1..759b21b4e4c8 100644 --- a/website/docs/en/plugins/webpack/javascript-modules-plugin.mdx +++ b/website/docs/en/plugins/webpack/javascript-modules-plugin.mdx @@ -34,3 +34,51 @@ class MyJsMinimizerPlugin { } } ``` + +## Static methods + +### `getChunkFilenameTemplate` + + + +```ts +function getChunkFilenameTemplate( + chunk: Chunk, + outputOptions: Output, +): Filename | undefined; +``` + +Returns the filename template that is used to render the JavaScript file of the +given chunk, resolved in this order: + +1. `chunk.filenameTemplate`, if the chunk carries its own template (for example + a chunk produced by a `splitChunks` cache group with `filename` set). +2. [`output.filename`](/config/output#outputfilename), if the chunk can be initial. +3. [`output.chunkFilename`](/config/output#outputchunkfilename) otherwise. + +```js +class MyPlugin { + apply(compiler) { + const { JavascriptModulesPlugin } = compiler.rspack.javascript; + compiler.hooks.compilation.tap('MyPlugin', (compilation) => { + compilation.hooks.afterSeal.tap('MyPlugin', () => { + for (const chunk of compilation.chunks) { + const template = JavascriptModulesPlugin.getChunkFilenameTemplate( + chunk, + compilation.outputOptions, + ); + const filename = compilation.getPath(template, { chunk }); + console.log(filename); + } + }); + }); + } +} +``` + +:::info Difference from webpack +webpack also returns [`output.hotUpdateChunkFilename`](/config/output#outputhotupdatechunkfilename) +for hot update chunks. Rspack keeps hot update chunks inside its Rust HMR +pipeline and never exposes them to the JavaScript side, so no chunk you can +reach from a plugin is a hot update chunk. +::: diff --git a/website/docs/zh/plugins/webpack/javascript-modules-plugin.mdx b/website/docs/zh/plugins/webpack/javascript-modules-plugin.mdx index 92fd54d4e8e1..d83818f01dc1 100644 --- a/website/docs/zh/plugins/webpack/javascript-modules-plugin.mdx +++ b/website/docs/zh/plugins/webpack/javascript-modules-plugin.mdx @@ -34,3 +34,50 @@ class MyJsMinimizerPlugin { } } ``` + +## 静态方法 + +### `getChunkFilenameTemplate` + + + +```ts +function getChunkFilenameTemplate( + chunk: Chunk, + outputOptions: Output, +): Filename | undefined; +``` + +返回用于生成该 chunk 对应 JavaScript 文件的文件名模板,按以下顺序解析: + +1. `chunk.filenameTemplate`,如果该 chunk 自带模板(例如由设置了 `filename` 的 + `splitChunks` cacheGroup 产生的 chunk)。 +2. [`output.filename`](/config/output#outputfilename),如果该 chunk 可以是 initial chunk。 +3. 否则为 [`output.chunkFilename`](/config/output#outputchunkfilename)。 + +```js +class MyPlugin { + apply(compiler) { + const { JavascriptModulesPlugin } = compiler.rspack.javascript; + compiler.hooks.compilation.tap('MyPlugin', (compilation) => { + compilation.hooks.afterSeal.tap('MyPlugin', () => { + for (const chunk of compilation.chunks) { + const template = JavascriptModulesPlugin.getChunkFilenameTemplate( + chunk, + compilation.outputOptions, + ); + const filename = compilation.getPath(template, { chunk }); + console.log(filename); + } + }); + }); + } +} +``` + +:::info 与 webpack 的差异 +webpack 还会为 hot update chunk 返回 +[`output.hotUpdateChunkFilename`](/config/output#outputhotupdatechunkfilename)。 +Rspack 的 hot update chunk 完全保留在 Rust 侧的 HMR 流程中,不会暴露给 JavaScript 层, +因此插件能拿到的 chunk 都不会是 hot update chunk。 +::: From d055e35be3184ffb74bcc7b4b0531508f0195f0a Mon Sep 17 00:00:00 2001 From: kakiuwang-ui Date: Mon, 24 Aug 2026 20:32:57 +0800 Subject: [PATCH 2/6] docs(javascript): simplify the hot update chunk note Apply review suggestions from @LingyuCoder. --- .../docs/en/plugins/webpack/javascript-modules-plugin.mdx | 5 +---- .../docs/zh/plugins/webpack/javascript-modules-plugin.mdx | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/website/docs/en/plugins/webpack/javascript-modules-plugin.mdx b/website/docs/en/plugins/webpack/javascript-modules-plugin.mdx index 759b21b4e4c8..782cd2590d19 100644 --- a/website/docs/en/plugins/webpack/javascript-modules-plugin.mdx +++ b/website/docs/en/plugins/webpack/javascript-modules-plugin.mdx @@ -77,8 +77,5 @@ class MyPlugin { ``` :::info Difference from webpack -webpack also returns [`output.hotUpdateChunkFilename`](/config/output#outputhotupdatechunkfilename) -for hot update chunks. Rspack keeps hot update chunks inside its Rust HMR -pipeline and never exposes them to the JavaScript side, so no chunk you can -reach from a plugin is a hot update chunk. +Hot update chunks are not currently supported because they are not yet exposed to the JavaScript side. ::: diff --git a/website/docs/zh/plugins/webpack/javascript-modules-plugin.mdx b/website/docs/zh/plugins/webpack/javascript-modules-plugin.mdx index d83818f01dc1..91c4a42039e6 100644 --- a/website/docs/zh/plugins/webpack/javascript-modules-plugin.mdx +++ b/website/docs/zh/plugins/webpack/javascript-modules-plugin.mdx @@ -76,8 +76,5 @@ class MyPlugin { ``` :::info 与 webpack 的差异 -webpack 还会为 hot update chunk 返回 -[`output.hotUpdateChunkFilename`](/config/output#outputhotupdatechunkfilename)。 -Rspack 的 hot update chunk 完全保留在 Rust 侧的 HMR 流程中,不会暴露给 JavaScript 层, -因此插件能拿到的 chunk 都不会是 hot update chunk。 +尚不支持 hot update chunk,因其未暴露到 JavaScript 侧。 ::: From 02331fa2ee3bded6a32062888b6f9bca954ee3ef Mon Sep 17 00:00:00 2001 From: kakiuwang-ui Date: Mon, 24 Aug 2026 20:33:04 +0800 Subject: [PATCH 3/6] fix(core): evaluate Filename callbacks in Compilation path helpers getPath and getPathWithInfo are documented as taking a `Filename` (website/docs/en/api/javascript-api/compilation.mdx), which includes the function form, but the TypeScript signatures only accepted a string and the value was passed straight through to the string-only N-API method. Evaluate the callback with the path data before rendering, matching webpack's Compilation.getAssetPath. getAssetPath and getAssetPathWithInfo get the same treatment for consistency. --- packages/rspack/src/Compilation.ts | 34 ++++++++--- .../hooks/get-path-filename-callback/index.js | 3 + .../rspack.config.js | 57 +++++++++++++++++++ 3 files changed, 86 insertions(+), 8 deletions(-) create mode 100644 tests/rspack-test/configCases/hooks/get-path-filename-callback/index.js create mode 100644 tests/rspack-test/configCases/hooks/get-path-filename-callback/rspack.config.js diff --git a/packages/rspack/src/Compilation.ts b/packages/rspack/src/Compilation.ts index 9cdab15cac30..94056ec6c7c7 100644 --- a/packages/rspack/src/Compilation.ts +++ b/packages/rspack/src/Compilation.ts @@ -31,6 +31,7 @@ import type { ChunkGraph } from './ChunkGraph'; import type { Compiler } from './Compiler'; import type { ContextModuleFactory } from './ContextModuleFactory'; import type { + Filename, OutputNormalized, RspackOptionsNormalized, RspackPluginInstance, @@ -95,6 +96,17 @@ export type ChunkPathData = { contentHash?: Record | string; }; +/** + * Resolve a `Filename` to a template string. + * + * `output.filename`, `output.chunkFilename` and a chunk's own filename template + * may all be functions. Align with webpack, which evaluates the function with + * the path data before rendering the placeholders it returns. + */ +function resolveFilename(filename: Filename, data: PathData): string { + return typeof filename === 'function' ? filename(data) : filename; +} + function normalizePathData(data: PathData = {}): JsPathData { const pathData: JsPathData = { filename: data.filename, @@ -789,24 +801,30 @@ BREAKING CHANGE: Asset processing hooks in Compilation has been merged into a si this.#warnings.splice(0, this.#warnings.length, ...warnings); } - getPath(filename: string, data: PathData = {}) { + getPath(filename: Filename, data: PathData = {}) { const pathData = normalizePathData(data); - return this.#inner.getPath(filename, pathData); + return this.#inner.getPath(resolveFilename(filename, data), pathData); } - getPathWithInfo(filename: string, data: PathData = {}) { + getPathWithInfo(filename: Filename, data: PathData = {}) { const pathData = normalizePathData(data); - return this.#inner.getPathWithInfo(filename, pathData); + return this.#inner.getPathWithInfo( + resolveFilename(filename, data), + pathData, + ); } - getAssetPath(filename: string, data: PathData = {}) { + getAssetPath(filename: Filename, data: PathData = {}) { const pathData = normalizePathData(data); - return this.#inner.getAssetPath(filename, pathData); + return this.#inner.getAssetPath(resolveFilename(filename, data), pathData); } - getAssetPathWithInfo(filename: string, data: PathData = {}) { + getAssetPathWithInfo(filename: Filename, data: PathData = {}) { const pathData = normalizePathData(data); - return this.#inner.getAssetPathWithInfo(filename, pathData); + return this.#inner.getAssetPathWithInfo( + resolveFilename(filename, data), + pathData, + ); } getLogger(name: string | (() => string)) { diff --git a/tests/rspack-test/configCases/hooks/get-path-filename-callback/index.js b/tests/rspack-test/configCases/hooks/get-path-filename-callback/index.js new file mode 100644 index 000000000000..a7cf7aaac784 --- /dev/null +++ b/tests/rspack-test/configCases/hooks/get-path-filename-callback/index.js @@ -0,0 +1,3 @@ +it("should resolve filename callbacks passed to the path helpers", () => { + expect(1).toBe(1); +}); diff --git a/tests/rspack-test/configCases/hooks/get-path-filename-callback/rspack.config.js b/tests/rspack-test/configCases/hooks/get-path-filename-callback/rspack.config.js new file mode 100644 index 000000000000..0a77cad8d652 --- /dev/null +++ b/tests/rspack-test/configCases/hooks/get-path-filename-callback/rspack.config.js @@ -0,0 +1,57 @@ +const pluginName = 'plugin'; + +class Plugin { + apply(compiler) { + let called = false; + compiler.hooks.compilation.tap(pluginName, (compilation) => { + compilation.hooks.processAssets.tap(pluginName, () => { + called = true; + const chunk = Array.from(compilation.chunks).find( + (c) => c.name === 'main', + ); + expect(chunk).toBeDefined(); + + // a string template keeps working + expect(compilation.getPath('[name].js', { chunk })).toBe('main.js'); + + // a callback is evaluated, and placeholders it returns are still rendered + expect( + compilation.getPath(() => 'from-callback-[name].js', { chunk }), + ).toBe('from-callback-main.js'); + + // the callback receives the path data it was called with + let seen; + compilation.getPath( + (pathData) => { + seen = pathData; + return '[name].js'; + }, + { chunk, contentHashType: 'javascript' }, + ); + expect(seen.chunk).toBe(chunk); + expect(seen.contentHashType).toBe('javascript'); + + // the other three helpers accept callbacks too + expect(compilation.getAssetPath(() => '[name].js', { chunk })).toBe( + 'main.js', + ); + expect( + compilation.getPathWithInfo(() => '[name].js', { chunk }).path, + ).toBe('main.js'); + expect( + compilation.getAssetPathWithInfo(() => '[name].js', { chunk }).path, + ).toBe('main.js'); + }); + }); + compiler.hooks.done.tap(pluginName, (stats) => { + expect(stats.toJson().errors.length).toBe(0); + expect(called).toBe(true); + }); + } +} + +/**@type {import("@rspack/core").Configuration}*/ +module.exports = { + context: __dirname, + plugins: [new Plugin()], +}; From 75e2b5cf2c8f5434fd1b8bdb5b2398c42f64e9f1 Mon Sep 17 00:00:00 2001 From: kakiuwang-ui Date: Thu, 27 Aug 2026 21:49:24 +0800 Subject: [PATCH 4/6] docs(javascript): drop the stale addedVersion on getChunkFilenameTemplate 2.2.0 shipped without this API, and the recently added compilation.runtimeTemplate section does not carry an ApiMeta either. --- website/docs/en/plugins/javascript-modules-plugin.mdx | 2 -- website/docs/zh/plugins/javascript-modules-plugin.mdx | 2 -- 2 files changed, 4 deletions(-) diff --git a/website/docs/en/plugins/javascript-modules-plugin.mdx b/website/docs/en/plugins/javascript-modules-plugin.mdx index 782cd2590d19..998d5b98c793 100644 --- a/website/docs/en/plugins/javascript-modules-plugin.mdx +++ b/website/docs/en/plugins/javascript-modules-plugin.mdx @@ -39,8 +39,6 @@ class MyJsMinimizerPlugin { ### `getChunkFilenameTemplate` - - ```ts function getChunkFilenameTemplate( chunk: Chunk, diff --git a/website/docs/zh/plugins/javascript-modules-plugin.mdx b/website/docs/zh/plugins/javascript-modules-plugin.mdx index 91c4a42039e6..0949beec57e6 100644 --- a/website/docs/zh/plugins/javascript-modules-plugin.mdx +++ b/website/docs/zh/plugins/javascript-modules-plugin.mdx @@ -39,8 +39,6 @@ class MyJsMinimizerPlugin { ### `getChunkFilenameTemplate` - - ```ts function getChunkFilenameTemplate( chunk: Chunk, From b3654b1014c1c8d9b37b84c3d1ac7f8bc24d4b45 Mon Sep 17 00:00:00 2001 From: Cong-Cong Pan Date: Fri, 28 Aug 2026 17:23:03 +0800 Subject: [PATCH 5/6] fix: align filename callback handling with webpack --- packages/rspack/src/Compilation.ts | 41 ++++++++----------- .../rspack.config.js | 30 ++++++++++++++ .../en/plugins/javascript-modules-plugin.mdx | 2 + .../zh/plugins/javascript-modules-plugin.mdx | 2 + 4 files changed, 52 insertions(+), 23 deletions(-) diff --git a/packages/rspack/src/Compilation.ts b/packages/rspack/src/Compilation.ts index 292866594f4f..c3e68363a8f9 100644 --- a/packages/rspack/src/Compilation.ts +++ b/packages/rspack/src/Compilation.ts @@ -97,17 +97,6 @@ export type ChunkPathData = { contentHash?: Record | string; }; -/** - * Resolve a `Filename` to a template string. - * - * `output.filename`, `output.chunkFilename` and a chunk's own filename template - * may all be functions. Align with webpack, which evaluates the function with - * the path data before rendering the placeholders it returns. - */ -function resolveFilename(filename: Filename, data: PathData): string { - return typeof filename === 'function' ? filename(data) : filename; -} - function normalizePathData(data: PathData = {}): JsPathData { const pathData: JsPathData = { filename: data.filename, @@ -805,29 +794,35 @@ BREAKING CHANGE: Asset processing hooks in Compilation has been merged into a si } getPath(filename: Filename, data: PathData = {}) { - const pathData = normalizePathData(data); - return this.#inner.getPath(resolveFilename(filename, data), pathData); + if (!data.hash) { + data = { + hash: this.hash ?? undefined, + ...data, + }; + } + return this.getAssetPath(filename, data); } getPathWithInfo(filename: Filename, data: PathData = {}) { - const pathData = normalizePathData(data); - return this.#inner.getPathWithInfo( - resolveFilename(filename, data), - pathData, - ); + if (!data.hash) { + data = { + hash: this.hash ?? undefined, + ...data, + }; + } + return this.getAssetPathWithInfo(filename, data); } getAssetPath(filename: Filename, data: PathData = {}) { + const template = typeof filename === 'function' ? filename(data) : filename; const pathData = normalizePathData(data); - return this.#inner.getAssetPath(resolveFilename(filename, data), pathData); + return this.#inner.getAssetPath(template, pathData); } getAssetPathWithInfo(filename: Filename, data: PathData = {}) { + const template = typeof filename === 'function' ? filename(data) : filename; const pathData = normalizePathData(data); - return this.#inner.getAssetPathWithInfo( - resolveFilename(filename, data), - pathData, - ); + return this.#inner.getAssetPathWithInfo(template, pathData); } getLogger(name: string | (() => string)) { diff --git a/tests/rspack-test/configCases/hooks/get-path-filename-callback/rspack.config.js b/tests/rspack-test/configCases/hooks/get-path-filename-callback/rspack.config.js index 0a77cad8d652..bdf514c6cf59 100644 --- a/tests/rspack-test/configCases/hooks/get-path-filename-callback/rspack.config.js +++ b/tests/rspack-test/configCases/hooks/get-path-filename-callback/rspack.config.js @@ -31,6 +31,36 @@ class Plugin { expect(seen.chunk).toBe(chunk); expect(seen.contentHashType).toBe('javascript'); + // getPath helpers provide the compilation hash to callbacks by default + const pathWithDefaultHash = compilation.getPath('[fullhash].js'); + expect(compilation.getPath(({ hash }) => `${hash}.js`)).toBe( + pathWithDefaultHash, + ); + expect( + compilation.getPath((pathData) => { + pathData.hash = 'callback-hash'; + return '[fullhash].js'; + }), + ).toBe('callback-hash.js'); + + // with-info helpers pass one mutable info object through the callback + // and placeholder rendering + const defaultHash = pathWithDefaultHash.slice(0, -'.js'.length); + let callbackInfo; + const pathWithInfo = compilation.getPathWithInfo(({ hash }, info) => { + expect(hash).toBe(defaultHash); + callbackInfo = info; + info.custom = 'from-callback'; + info.fullhash = 'from-callback'; + return '[fullhash].js'; + }); + expect(pathWithInfo.path).toBe(pathWithDefaultHash); + expect(pathWithInfo.info).toBe(callbackInfo); + expect(pathWithInfo.info.custom).toBe('from-callback'); + expect(new Set(pathWithInfo.info.fullhash)).toEqual( + new Set(['from-callback', defaultHash]), + ); + // the other three helpers accept callbacks too expect(compilation.getAssetPath(() => '[name].js', { chunk })).toBe( 'main.js', diff --git a/website/docs/en/plugins/javascript-modules-plugin.mdx b/website/docs/en/plugins/javascript-modules-plugin.mdx index 998d5b98c793..5742a348e6be 100644 --- a/website/docs/en/plugins/javascript-modules-plugin.mdx +++ b/website/docs/en/plugins/javascript-modules-plugin.mdx @@ -39,6 +39,8 @@ class MyJsMinimizerPlugin { ### `getChunkFilenameTemplate` + + ```ts function getChunkFilenameTemplate( chunk: Chunk, diff --git a/website/docs/zh/plugins/javascript-modules-plugin.mdx b/website/docs/zh/plugins/javascript-modules-plugin.mdx index 0949beec57e6..eda393481612 100644 --- a/website/docs/zh/plugins/javascript-modules-plugin.mdx +++ b/website/docs/zh/plugins/javascript-modules-plugin.mdx @@ -39,6 +39,8 @@ class MyJsMinimizerPlugin { ### `getChunkFilenameTemplate` + + ```ts function getChunkFilenameTemplate( chunk: Chunk, From 6f7470f91d842425790f5388c8ea379ee1838430 Mon Sep 17 00:00:00 2001 From: Cong-Cong Pan Date: Sat, 29 Aug 2026 21:09:07 +0800 Subject: [PATCH 6/6] fix: support filename callbacks in parallel loaders --- crates/node_binding/napi-binding.d.ts | 2 +- .../rspack_binding_api/src/compilation/mod.rs | 9 ++-- packages/rspack/src/Compilation.ts | 8 +++- packages/rspack/src/loader-runner/index.ts | 20 +++++++- packages/rspack/src/loader-runner/worker.ts | 48 +++++++++++++++---- .../loader-parallel/loader-context/index.js | 8 ++++ .../loader-parallel/loader-context/loader.js | 38 +++++++++++++++ 7 files changed, 115 insertions(+), 18 deletions(-) diff --git a/crates/node_binding/napi-binding.d.ts b/crates/node_binding/napi-binding.d.ts index e1c9f343a6a4..8d498309acc4 100644 --- a/crates/node_binding/napi-binding.d.ts +++ b/crates/node_binding/napi-binding.d.ts @@ -309,7 +309,7 @@ export declare class JsCompilation { getWarnings(): Array getStats(): JsStats getAssetPath(filename: string, data: JsPathData): string - getAssetPathWithInfo(filename: string, data: JsPathData): PathWithInfo + getAssetPathWithInfo(filename: string, data: JsPathData, assetInfo?: AssetInfo | undefined | null): PathWithInfo getPath(filename: string, data: JsPathData): string getPathWithInfo(filename: string, data: JsPathData): PathWithInfo addFileDependencies(deps: Array): void diff --git a/crates/rspack_binding_api/src/compilation/mod.rs b/crates/rspack_binding_api/src/compilation/mod.rs index dd2387a70ec6..3cfdb25622b1 100644 --- a/crates/rspack_binding_api/src/compilation/mod.rs +++ b/crates/rspack_binding_api/src/compilation/mod.rs @@ -534,15 +534,18 @@ impl JsCompilation { &self, filename: String, data: JsPathData, + asset_info: Option, ) -> Result { let compilation = self.as_ref()?; + let filename: rspack_core::Filename = filename.into(); + let mut asset_info = asset_info.map(Into::into).unwrap_or_default(); #[allow(clippy::disallowed_methods)] - let res = futures::executor::block_on( - compilation.get_asset_path_with_info(&filename.into(), data.to_path_data(compilation)?), + let path = futures::executor::block_on( + filename.render(data.to_path_data(compilation)?, Some(&mut asset_info)), ) .to_napi_result()?; - Ok(res.into()) + Ok((path, asset_info).into()) } #[napi] diff --git a/packages/rspack/src/Compilation.ts b/packages/rspack/src/Compilation.ts index c3e68363a8f9..53fa1fbdf508 100644 --- a/packages/rspack/src/Compilation.ts +++ b/packages/rspack/src/Compilation.ts @@ -820,9 +820,13 @@ BREAKING CHANGE: Asset processing hooks in Compilation has been merged into a si } getAssetPathWithInfo(filename: Filename, data: PathData = {}) { - const template = typeof filename === 'function' ? filename(data) : filename; + const info: AssetInfo = {}; + const template = + typeof filename === 'function' ? filename(data, info) : filename; const pathData = normalizePathData(data); - return this.#inner.getAssetPathWithInfo(template, pathData); + const result = this.#inner.getAssetPathWithInfo(template, pathData, info); + Object.assign(info, result.info); + return { path: result.path, info }; } getLogger(name: string | (() => string)) { diff --git a/packages/rspack/src/loader-runner/index.ts b/packages/rspack/src/loader-runner/index.ts index 87fb2a98d4d5..747a4bf3f73c 100644 --- a/packages/rspack/src/loader-runner/index.ts +++ b/packages/rspack/src/loader-runner/index.ts @@ -780,6 +780,7 @@ export async function runLoaders( }, }, _compilation: { + hash: compiler._lastCompilation!.hash, options: { output: { // css-loader @@ -952,7 +953,16 @@ export async function runLoaders( case RequestType.CompilationGetPathWithInfo: { const filename = args[0]; const data = args[1]; - return compiler._lastCompilation!.getPathWithInfo(filename, data); + const initialInfo = args[2]; + return compiler._lastCompilation!.getPathWithInfo( + initialInfo + ? (_pathData, info) => { + Object.assign(info!, initialInfo); + return filename; + } + : filename, + data, + ); } case RequestType.CompilationGetAssetPath: { const filename = args[0]; @@ -962,8 +972,14 @@ export async function runLoaders( case RequestType.CompilationGetAssetPathWithInfo: { const filename = args[0]; const data = args[1]; + const initialInfo = args[2]; return compiler._lastCompilation!.getAssetPathWithInfo( - filename, + initialInfo + ? (_pathData, info) => { + Object.assign(info!, initialInfo); + return filename; + } + : filename, data, ); } diff --git a/packages/rspack/src/loader-runner/worker.ts b/packages/rspack/src/loader-runner/worker.ts index a9556046b9a6..ac132ae11ae7 100644 --- a/packages/rspack/src/loader-runner/worker.ts +++ b/packages/rspack/src/loader-runner/worker.ts @@ -303,29 +303,57 @@ async function loaderImpl( loaderContext._compilation = { ...loaderContext._compilation, - getPath(filename, data) { - return sendRequest(RequestType.CompilationGetPath, filename, data).wait(); + getPath(filename, data = {}) { + if (!data.hash) { + data = { + hash: loaderContext._compilation.hash ?? undefined, + ...data, + }; + } + const template = + typeof filename === 'function' ? filename(data) : filename; + return sendRequest(RequestType.CompilationGetPath, template, data).wait(); }, - getPathWithInfo(filename, data) { - return sendRequest( + getPathWithInfo(filename, data = {}) { + if (!data.hash) { + data = { + hash: loaderContext._compilation.hash ?? undefined, + ...data, + }; + } + const info = {}; + const template = + typeof filename === 'function' ? filename(data, info) : filename; + const result = sendRequest( RequestType.CompilationGetPathWithInfo, - filename, + template, data, + info, ).wait(); + Object.assign(info, result.info); + return { path: result.path, info }; }, - getAssetPath(filename, data) { + getAssetPath(filename, data = {}) { + const template = + typeof filename === 'function' ? filename(data) : filename; return sendRequest( RequestType.CompilationGetAssetPath, - filename, + template, data, ).wait(); }, - getAssetPathWithInfo(filename, data) { - return sendRequest( + getAssetPathWithInfo(filename, data = {}) { + const info = {}; + const template = + typeof filename === 'function' ? filename(data, info) : filename; + const result = sendRequest( RequestType.CompilationGetAssetPathWithInfo, - filename, + template, data, + info, ).wait(); + Object.assign(info, result.info); + return { path: result.path, info }; }, } as LoaderContext['_compilation']; diff --git a/tests/rspack-test/configCases/loader-parallel/loader-context/index.js b/tests/rspack-test/configCases/loader-parallel/loader-context/index.js index 1abf26ed8d35..efb7315e2ae2 100644 --- a/tests/rspack-test/configCases/loader-parallel/loader-context/index.js +++ b/tests/rspack-test/configCases/loader-parallel/loader-context/index.js @@ -4,5 +4,13 @@ it('should expose loader context APIs in parallel loaders', () => { logger: true, resolve: true, getResolve: true, + path: 'path-hash.js', + assetPath: '[asset-hash].js', + pathWithInfo: 'info-hash-info-hash.js', + pathWithInfoIdentity: true, + pathWithInfoCustom: 'from-callback', + pathWithInfoFullhash: true, + assetPathWithInfo: 'asset-with-info.js', + assetPathWithInfoCustom: 'from-asset-callback', }); }); diff --git a/tests/rspack-test/configCases/loader-parallel/loader-context/loader.js b/tests/rspack-test/configCases/loader-parallel/loader-context/loader.js index 1f37c990004a..cb3e8245a982 100644 --- a/tests/rspack-test/configCases/loader-parallel/loader-context/loader.js +++ b/tests/rspack-test/configCases/loader-parallel/loader-context/loader.js @@ -3,6 +3,33 @@ module.exports = function () { const callback = this.async(); const logger = this.getLogger('parallel-loader'); const getResolve = this.getResolve(); + const compilation = this._compilation; + + const path = compilation.getPath((data) => { + data.hash = 'path-hash'; + return '[fullhash].js'; + }); + const assetPath = compilation.getAssetPath( + ({ hash }) => `[${hash}].js`, + { hash: 'asset-hash' }, + ); + + let callbackInfo; + const pathWithInfo = compilation.getPathWithInfo( + ({ hash }, info) => { + callbackInfo = info; + info.custom = 'from-callback'; + info.fullhash = 'from-callback'; + return `[fullhash]-${hash}.js`; + }, + { hash: 'info-hash' }, + ); + const assetPathWithInfo = compilation.getAssetPathWithInfo( + (_data, info) => { + info.custom = 'from-asset-callback'; + return 'asset-with-info.js'; + }, + ); logger.clear(); logger.info('loader context APIs are available'); @@ -32,6 +59,17 @@ module.exports = function () { logger: typeof logger.clear === 'function', resolve: resolveRequest?.path.endsWith('dependency.js'), getResolve: getResolveRequest?.path.endsWith('dependency.js'), + path, + assetPath, + pathWithInfo: pathWithInfo.path, + pathWithInfoIdentity: pathWithInfo.info === callbackInfo, + pathWithInfoCustom: pathWithInfo.info.custom, + pathWithInfoFullhash: + new Set(pathWithInfo.info.fullhash).size === 2 && + pathWithInfo.info.fullhash.includes('from-callback') && + pathWithInfo.info.fullhash.includes('info-hash'), + assetPathWithInfo: assetPathWithInfo.path, + assetPathWithInfoCustom: assetPathWithInfo.info.custom, })}`, ); },