fix: review

This commit is contained in:
Hai 2025-09-21 07:38:43 +08:00
parent a51d2349ee
commit 5197fd7f03
17 changed files with 391 additions and 154 deletions

View File

@ -9,4 +9,11 @@ export interface ManifestPluginOptions {
* Specifies the filename of the output file on disk. By default the plugin will emit `manifest.json` inside the 'output.path' directory. * Specifies the filename of the output file on disk. By default the plugin will emit `manifest.json` inside the 'output.path' directory.
*/ */
filename?: string; filename?: string;
/**
* A custom Function to create the manifest.
*/
handle?: (
manifest: Record<string, string>,
stats: import("../../lib/stats/DefaultStatsFactoryPlugin").StatsCompilation
) => string;
} }

View File

@ -0,0 +1,149 @@
This example demonstrates how to use webpack internal ManifestPlugin.
# example.js
```js
import("./baz");
```
# foo.txt
```js
foo
```
# bar.txt
```js
bar
```
# baz.js
```js
import foo from "./foo.txt";
import bar from "./bar.txt";
export default foo + bar;
```
# webpack.config.js
```javascript
"use strict";
const webpack = require("../../");
/** @type {webpack.Configuration} */
module.exports = {
devtool: "source-map",
module: {
rules: [
{
test: /foo.txt/,
type: "asset/resource"
},
{
test: /bar.txt/,
use: require.resolve("file-loader")
}
]
},
plugins: [
new webpack.ManifestPlugin({
filename: "manifest.json"
}),
new webpack.ManifestPlugin({
filename: "manifest.yml",
handle(manifest) {
let _manifest = "";
for (const key in manifest) {
if (key === "manifest.json") continue;
_manifest += `- ${key}: '${manifest[key]}'\n`;
}
return _manifest;
}
})
]
};
```
# dist/manifest.json
```json
{
"output.js.map": "dist/output.js.map",
"main.js": "dist/output.js",
"bar.txt": "dist/a0145fafc7fab801e574631452de554b.txt",
"foo.txt": "dist/3ee037f347c64cc372ad.txt",
"1.output.js.map": "dist/1.output.js.map",
"1.output.js": "dist/1.output.js"
}
```
# dist/manifest.yml
```yml
- output.js.map: 'dist/output.js.map'
- main.js: 'dist/output.js'
- bar.txt: 'dist/a0145fafc7fab801e574631452de554b.txt'
- foo.txt: 'dist/3ee037f347c64cc372ad.txt'
- 1.output.js.map: 'dist/1.output.js.map'
- 1.output.js: 'dist/1.output.js'
```
# Info
## Unoptimized
```
assets by path *.js 11.9 KiB
asset output.js 9.61 KiB [emitted] (name: main) 1 related asset
asset 1.output.js 2.3 KiB [emitted] 1 related asset
assets by path *.txt 8 bytes
asset 3ee037f347c64cc372ad.txt 4 bytes [emitted] [immutable] [from: foo.txt]
asset a0145fafc7fab801e574631452de554b.txt 4 bytes [emitted] [immutable] [from: bar.txt]
asset manifest.json 260 bytes [emitted]
asset manifest.yml 240 bytes [emitted]
chunk (runtime: main) output.js (main) 17 bytes (javascript) 5.48 KiB (runtime) [entry] [rendered]
> ./example.js main
runtime modules 5.48 KiB 8 modules
./example.js 17 bytes [built] [code generated]
[used exports unknown]
entry ./example.js main
chunk (runtime: main) 1.output.js 207 bytes (javascript) 4 bytes (asset) [rendered]
> ./baz ./example.js 1:0-15
dependent modules 122 bytes (javascript) 4 bytes (asset) [dependent] 2 modules
./baz.js 85 bytes [built] [code generated]
[exports: default]
[used exports unknown]
import() ./baz ./example.js 1:0-15
webpack X.X.X compiled successfully
```
## Production mode
```
assets by path *.js 2.17 KiB
asset output.js 1.94 KiB [emitted] [minimized] (name: main) 1 related asset
asset 293.output.js 237 bytes [emitted] [minimized] 1 related asset
assets by path *.txt 8 bytes
asset 3ee037f347c64cc372ad.txt 4 bytes [emitted] [immutable] [from: foo.txt]
asset a0145fafc7fab801e574631452de554b.txt 4 bytes [emitted] [immutable] [from: bar.txt]
asset manifest.json 268 bytes [emitted]
asset manifest.yml 248 bytes [emitted]
chunk (runtime: main) 293.output.js 4 bytes (asset) 249 bytes (javascript) [rendered]
> ./baz ./example.js 1:0-15
./baz.js + 2 modules 207 bytes [built] [code generated]
[exports: default]
import() ./baz ./example.js 1:0-15
./foo.txt 4 bytes (asset) 42 bytes (javascript) [built] [code generated]
[no exports]
chunk (runtime: main) output.js (main) 17 bytes (javascript) 5.48 KiB (runtime) [entry] [rendered]
> ./example.js main
runtime modules 5.48 KiB 8 modules
./example.js 17 bytes [built] [code generated]
[no exports used]
entry ./example.js main
webpack X.X.X compiled successfully
```

View File

@ -0,0 +1 @@
bar

View File

@ -0,0 +1,4 @@
import foo from "./foo.txt";
import bar from "./bar.txt";
export default foo + bar;

View File

@ -0,0 +1 @@
require("../build-common");

View File

@ -0,0 +1 @@
import("./baz");

View File

@ -0,0 +1 @@
foo

View File

@ -0,0 +1,57 @@
This example demonstrates how to use webpack internal ManifestPlugin.
# example.js
```js
_{{example.js}}_
```
# foo.txt
```js
_{{foo.txt}}_
```
# bar.txt
```js
_{{bar.txt}}_
```
# baz.js
```js
_{{baz.js}}_
```
# webpack.config.js
```javascript
_{{webpack.config.js}}_
```
# dist/manifest.json
```json
_{{dist/manifest.json}}_
```
# dist/manifest.yml
```yml
_{{dist/manifest.yml}}_
```
# Info
## Unoptimized
```
_{{stdout}}_
```
## Production mode
```
_{{production:stdout}}_
```

View File

@ -0,0 +1,36 @@
"use strict";
const webpack = require("../../");
/** @type {webpack.Configuration} */
module.exports = {
devtool: "source-map",
module: {
rules: [
{
test: /foo.txt/,
type: "asset/resource"
},
{
test: /bar.txt/,
use: require.resolve("file-loader")
}
]
},
plugins: [
new webpack.ManifestPlugin({
filename: "manifest.json"
}),
new webpack.ManifestPlugin({
filename: "manifest.yml",
handle(manifest) {
let _manifest = "";
for (const key in manifest) {
if (key === "manifest.json") continue;
_manifest += `- ${key}: '${manifest[key]}'\n`;
}
return _manifest;
}
})
]
};

View File

@ -355,6 +355,9 @@ module.exports = mergeExports(fn, {
get Stats() { get Stats() {
return require("./Stats"); return require("./Stats");
}, },
get ManifestPlugin() {
return require("./stats/ManifestPlugin");
},
get Template() { get Template() {
return require("./Template"); return require("./Template");
}, },
@ -670,9 +673,6 @@ module.exports = mergeExports(fn, {
get SyncModuleIdsPlugin() { get SyncModuleIdsPlugin() {
return require("./ids/SyncModuleIdsPlugin"); return require("./ids/SyncModuleIdsPlugin");
} }
},
get ManifestPlugin() {
return require("./stats/ManifestPlugin");
} }
} }
}); });

View File

@ -12,28 +12,16 @@ const HotUpdateChunk = require("../HotUpdateChunk");
const createSchemaValidation = require("../util/create-schema-validation"); const createSchemaValidation = require("../util/create-schema-validation");
/** @typedef {import("../Compiler")} Compiler */ /** @typedef {import("../Compiler")} Compiler */
/** @typedef {import("..").StatsCompilation} StatsCompilation */
/** @typedef {import("../Chunk")} Chunk */ /** @typedef {import("../Chunk")} Chunk */
/** @typedef {import("../Compilation").Asset} Asset */
/** @typedef {import("../Module")} Module */ /** @typedef {import("../Module")} Module */
/** @typedef {import("../NormalModule")} NormalModule */ /** @typedef {import("../NormalModule")} NormalModule */
/** @typedef {import("../config/defaults").WebpackOptionsNormalizedWithDefaults} WebpackOptions */ /** @typedef {import("../config/defaults").WebpackOptionsNormalizedWithDefaults} WebpackOptions */
/** @typedef {import("../../declarations/plugins/ManifestPlugin").ManifestPluginOptions} ManifestPluginOptions */ /** @typedef {import("../../declarations/plugins/ManifestPlugin").ManifestPluginOptions} ManifestPluginOptions */
/** const PLUGIN_NAME = "ManifestPlugin";
* @typedef {object} Asset
* @property {string} name
* @property {string} path
*/
/**
* @typedef {{ name: string, stage: number }} TapOptions
*/
/** @type {TapOptions} */
const TAP_OPTIONS = {
name: "ManifestPlugin",
stage: Compilation.PROCESS_ASSETS_STAGE_REPORT
};
const validate = createSchemaValidation( const validate = createSchemaValidation(
require("../../schemas/plugins/ManifestPlugin.check"), require("../../schemas/plugins/ManifestPlugin.check"),
@ -63,12 +51,12 @@ class ManifestPlugin {
constructor(options) { constructor(options) {
validate(options); validate(options);
const defaultOptions = {
filename: "manifest.json"
};
/** @type {Required<ManifestPluginOptions>} */ /** @type {Required<ManifestPluginOptions>} */
this.options = Object.assign(defaultOptions, options); this.options = {
filename: "manifest.json",
handle: (manifest, _stats) => JSON.stringify(manifest, null, 2),
...options
};
} }
/** /**
@ -77,112 +65,83 @@ class ManifestPlugin {
* @returns {void} * @returns {void}
*/ */
apply(compiler) { apply(compiler) {
/** @type {Map<string,Module>} */ /** @type {WeakMap<Compilation, StatsCompilation>} */
const moduleAssets = new Map(); const cachedStats = new WeakMap();
const outputFilename = path.resolve( compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
/** @type {WebpackOptions} */ (compiler.options).output.path, compilation.hooks.processAssets.tap(
this.options.filename {
name: PLUGIN_NAME,
stage: Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE
},
() => {
let stats =
/** @type {StatsCompilation | undefined} */ cachedStats.get(
compilation
); );
const manifestAssetName = path.relative( if (!stats) {
/** @type {WebpackOptions} */ (compiler.options).output.path, stats = compilation.getStats().toJson({
outputFilename
);
compiler.hooks.compilation.tap(TAP_OPTIONS, (compilation) => {
compilation.hooks.moduleAsset.tap(TAP_OPTIONS, (module, asset) => {
moduleAssets.set(asset, module);
});
});
compiler.hooks.thisCompilation.tap(TAP_OPTIONS, (compilation) => {
compilation.hooks.processAssets.tap(TAP_OPTIONS, () => {
const stats = compilation.getStats().toJson({
all: false, all: false,
assets: true, assets: true,
cachedAssets: true, cachedAssets: true,
assetsSpace: Infinity, assetsSpace: Infinity,
ids: true,
publicPath: true publicPath: true
}); });
cachedStats.set(compilation, stats);
}
/** @type {Map<string, Asset>} */ /** @type {Set<string>} */
const mapByPath = new Map(); const added = new Set();
/**
* @type {{name: string, file: string}[]}
*/
const items = [];
/** /**
* @param {Asset} asset asset * @param {string} file file
* @param {((file: string) => string)=} namer namer
* @returns {void} * @returns {void}
*/ */
const addToMap = (asset) => { const handleFile = (file, namer) => {
const { path } = asset; if (added.has(file)) return;
mapByPath.set(path, asset); added.add(file);
let name = namer ? namer(file) : file;
const asset = compilation.getAsset(file);
if (asset && asset.info.sourceFilename) {
name = path.join(
path.dirname(file),
path.basename(asset.info.sourceFilename)
);
}
items.push({ name, file });
}; };
for (const chunk of compilation.chunks) { for (const chunk of compilation.chunks) {
if (chunk instanceof HotUpdateChunk) continue; if (chunk instanceof HotUpdateChunk) continue;
const chunkName = chunk.name; const chunkName = chunk.name;
for (const auxiliaryFile of chunk.auxiliaryFiles) { for (const auxiliaryFile of chunk.auxiliaryFiles) {
addToMap({ handleFile(auxiliaryFile, (file) => path.basename(file));
name: path.basename(auxiliaryFile),
path: auxiliaryFile
});
} }
for (const file of chunk.files) {
for (const chunkFilename of chunk.files) { handleFile(file, (file) => {
const name = chunkName if (chunkName) return `${chunkName}.${extname(file)}`;
? `${chunkName}.${extname(chunkFilename)}` return file;
: chunkFilename;
addToMap({
name,
path: chunkFilename
}); });
} }
} }
if (stats.assets) { if (stats.assets) {
// module assets are included in `chunk.auxiliaryFiles`, so we add them after chunk assets
for (const asset of stats.assets) { for (const asset of stats.assets) {
let moduleAssetName; if (asset.info.hotModuleReplacement) {
const module = /** @type {NormalModule} */ (
moduleAssets.get(asset.name)
);
if (module && module.userRequest) {
moduleAssetName = path.join(
path.dirname(asset.name),
path.basename(module.userRequest)
);
} else if (asset.info.sourceFilename) {
moduleAssetName = path.join(
path.dirname(asset.name),
path.basename(asset.info.sourceFilename)
);
}
if (moduleAssetName) {
addToMap({
name: moduleAssetName,
path: asset.name
});
continue; continue;
} }
handleFile(asset.name);
// We will handle them later
if (
(asset.chunks && asset.chunks.length > 0) ||
(asset.auxiliaryChunks && asset.auxiliaryChunks.length > 0)
) {
continue;
}
addToMap({
name: asset.name,
path: asset.name
});
} }
} }
/** @type {Record<string,string>} */ /** @type {Record<string, string>} */
const manifest = {}; const manifest = {};
const hashDigestLength = compilation.outputOptions.hashDigestLength; const hashDigestLength = compilation.outputOptions.hashDigestLength;
@ -191,30 +150,29 @@ class ManifestPlugin {
* @returns {string} hash removed name * @returns {string} hash removed name
*/ */
const removeHash = (name) => { const removeHash = (name) => {
// Handles hashes that match configured `hashDigestLength`
// i.e. index.XXXX.html -> index.html (html-webpack-plugin)
if (hashDigestLength <= 0) return name; if (hashDigestLength <= 0) return name;
const reg = new RegExp( const reg = new RegExp(
`(\\.[a-f0-9]{${hashDigestLength}})(?=\\.)?`, `(\\.[a-f0-9]{${hashDigestLength}})(?=\\.)`,
"gi" "gi"
); );
return name.replace(reg, ""); return name.replace(reg, "");
}; };
for (const [_name, item] of mapByPath) { for (const { name, file } of items) {
manifest[removeHash(item.name)] = stats.publicPath manifest[removeHash(name)] = stats.publicPath
? stats.publicPath + ? stats.publicPath +
(stats.publicPath.endsWith("/") (stats.publicPath.endsWith("/") ? `${file}` : `/${file}`)
? `${item.path}` : file;
: `/${item.path}`)
: item.path;
} }
compilation.emitAsset( compilation.emitAsset(
manifestAssetName, this.options.filename,
new RawSource(JSON.stringify(manifest, null, 2)) new RawSource(this.options.handle(manifest, stats))
);
}
); );
moduleAssets.clear();
});
}); });
} }
} }

View File

@ -3,4 +3,4 @@
* DO NOT MODIFY BY HAND. * DO NOT MODIFY BY HAND.
* Run `yarn fix:special` to update * Run `yarn fix:special` to update
*/ */
"use strict";function r(e,{instancePath:t="",parentData:a,parentDataProperty:o,rootData:n=e}={}){if(!e||"object"!=typeof e||Array.isArray(e))return r.errors=[{params:{type:"object"}}],!1;{const t=0;for(const t in e)if("filename"!==t)return r.errors=[{params:{additionalProperty:t}}],!1;if(0===t&&void 0!==e.filename&&"string"!=typeof e.filename)return r.errors=[{params:{type:"string"}}],!1}return r.errors=null,!0}module.exports=r,module.exports.default=r; const r=/^(?:[A-Za-z]:[\\/]|\\\\|\/)/;function e(t,{instancePath:a="",parentData:n,parentDataProperty:o,rootData:s=t}={}){if(!t||"object"!=typeof t||Array.isArray(t))return e.errors=[{params:{type:"object"}}],!1;{const a=0;for(const r in t)if("filename"!==r&&"handle"!==r)return e.errors=[{params:{additionalProperty:r}}],!1;if(0===a){if(void 0!==t.filename){let a=t.filename;const n=0;if(0===n){if("string"!=typeof a)return e.errors=[{params:{type:"string"}}],!1;if(a.includes("!")||!1!==r.test(a))return e.errors=[{params:{}}],!1;if(a.length<1)return e.errors=[{params:{}}],!1}var i=0===n}else i=!0;if(i)if(void 0!==t.handle){const r=0;if(!(t.handle instanceof Function))return e.errors=[{params:{}}],!1;i=0===r}else i=!0}}return e.errors=null,!0}module.exports=e,module.exports.default=e;

View File

@ -5,7 +5,14 @@
"properties": { "properties": {
"filename": { "filename": {
"description": "Specifies the filename of the output file on disk. By default the plugin will emit `manifest.json` inside the 'output.path' directory.", "description": "Specifies the filename of the output file on disk. By default the plugin will emit `manifest.json` inside the 'output.path' directory.",
"type": "string" "type": "string",
"absolutePath": false,
"minLength": 1
},
"handle": {
"description": "A custom Function to create the manifest.",
"instanceof": "Function",
"tsType": "((manifest: Record<string, string>, stats: import('../../lib/stats/DefaultStatsFactoryPlugin').StatsCompilation) => string)"
} }
} }
} }

View File

@ -0,0 +1,7 @@
"use strict";
module.exports = [
// each time returns different OriginalSource in webpack.config.js:33
// this prevents hit in inmemory cache
/^Pack got invalid because of write to: RealContentHashPlugin|analyse|third.party.js$/
];

View File

@ -39,7 +39,7 @@ module.exports = {
}, },
plugins: [ plugins: [
new CopyPlugin(), new CopyPlugin(),
new webpack.experiments.ManifestPlugin({ new webpack.ManifestPlugin({
filename: "test.json" filename: "test.json"
}) })
], ],

View File

@ -1,7 +1,7 @@
"use strict"; "use strict";
module.exports = [ module.exports = [
// each time returns different OriginalSource in webpack.config.js:78 // each time returns different OriginalSource in webpack.config.js:108
// this prevents hit in inmemory cache // this prevents hit in inmemory cache
/^Pack got invalid because of write to: RealContentHashPlugin|analyse|index\.html$/ /^Pack got invalid because of write to: RealContentHashPlugin|analyse|index\.html$/
]; ];

10
types.d.ts vendored
View File

@ -9772,6 +9772,14 @@ declare interface ManifestPluginOptions {
* Specifies the filename of the output file on disk. By default the plugin will emit `manifest.json` inside the 'output.path' directory. * Specifies the filename of the output file on disk. By default the plugin will emit `manifest.json` inside the 'output.path' directory.
*/ */
filename?: string; filename?: string;
/**
* A custom Function to create the manifest.
*/
handle?: (
manifest: Record<string, string>,
stats: StatsCompilation
) => string;
} }
declare interface MapOptions { declare interface MapOptions {
/** /**
@ -18749,7 +18757,6 @@ declare namespace exports {
export namespace ids { export namespace ids {
export { SyncModuleIdsPlugin }; export { SyncModuleIdsPlugin };
} }
export { ManifestPlugin };
} }
export type ExternalItemFunctionCallback = ( export type ExternalItemFunctionCallback = (
data: ExternalItemFunctionData, data: ExternalItemFunctionData,
@ -18883,6 +18890,7 @@ declare namespace exports {
EntryPlugin as SingleEntryPlugin, EntryPlugin as SingleEntryPlugin,
SourceMapDevToolPlugin, SourceMapDevToolPlugin,
Stats, Stats,
ManifestPlugin,
Template, Template,
WatchIgnorePlugin, WatchIgnorePlugin,
WebpackError, WebpackError,