Skip to content
Open
33 changes: 23 additions & 10 deletions packages/rspack/src/Compilation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -792,24 +793,36 @@ 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 = {}) {
const pathData = normalizePathData(data);
return this.#inner.getPath(filename, pathData);
getPath(filename: Filename, data: PathData = {}) {
if (!data.hash) {
data = {
hash: this.hash ?? undefined,
...data,
};
}
return this.getAssetPath(filename, data);
}

getPathWithInfo(filename: string, data: PathData = {}) {
const pathData = normalizePathData(data);
return this.#inner.getPathWithInfo(filename, pathData);
getPathWithInfo(filename: Filename, data: PathData = {}) {
if (!data.hash) {
data = {
hash: this.hash ?? undefined,
...data,
};
}
return this.getAssetPathWithInfo(filename, data);
}

getAssetPath(filename: string, data: PathData = {}) {
getAssetPath(filename: Filename, data: PathData = {}) {
const template = typeof filename === 'function' ? filename(data) : filename;
Comment thread
SyMind marked this conversation as resolved.
const pathData = normalizePathData(data);
return this.#inner.getAssetPath(filename, pathData);
return this.#inner.getAssetPath(template, pathData);
}

getAssetPathWithInfo(filename: string, data: PathData = {}) {
getAssetPathWithInfo(filename: Filename, data: PathData = {}) {
const template = typeof filename === 'function' ? filename(data) : filename;
const pathData = normalizePathData(data);
return this.#inner.getAssetPathWithInfo(filename, pathData);
return this.#inner.getAssetPathWithInfo(template, pathData);
}

getLogger(name: string | (() => string)) {
Expand Down
27 changes: 27 additions & 0 deletions packages/rspack/src/builtin-plugin/JavascriptModulesPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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;
Comment thread
SyMind marked this conversation as resolved.
}
if (chunk.canBeInitial()) {
return outputOptions.filename;
}
return outputOptions.chunkFilename;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
require('./shared');
module.exports = 'async';
Original file line number Diff line number Diff line change
@@ -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);
});
});
Original file line number Diff line number Diff line change
@@ -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()],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = 'shared';
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
it("should resolve filename callbacks passed to the path helpers", () => {
expect(1).toBe(1);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
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');

// 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',
);
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()],
};
45 changes: 45 additions & 0 deletions website/docs/en/plugins/javascript-modules-plugin.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,48 @@ class MyJsMinimizerPlugin {
}
}
```

## Static methods

### `getChunkFilenameTemplate`

<ApiMeta addedVersion="2.2.2" />

```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 });
Comment thread
SyMind marked this conversation as resolved.
console.log(filename);
}
});
});
}
}
```

:::info Difference from webpack
Hot update chunks are not currently supported because they are not yet exposed to the JavaScript side.
:::
44 changes: 44 additions & 0 deletions website/docs/zh/plugins/javascript-modules-plugin.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,47 @@ class MyJsMinimizerPlugin {
}
}
```

## 静态方法

### `getChunkFilenameTemplate`

<ApiMeta addedVersion="2.2.2" />

```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 的差异
尚不支持 hot update chunk,因其未暴露到 JavaScript 侧。
:::
Loading