refactor: manifest plugin
Github Actions / lint (push) Waiting to run Details
Github Actions / validate-legacy-node (push) Waiting to run Details
Github Actions / benchmark (1/4) (push) Waiting to run Details
Github Actions / benchmark (2/4) (push) Waiting to run Details
Github Actions / benchmark (3/4) (push) Waiting to run Details
Github Actions / benchmark (4/4) (push) Waiting to run Details
Github Actions / basic (push) Waiting to run Details
Github Actions / unit (push) Waiting to run Details
Github Actions / integration (10.x, macos-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (10.x, macos-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (10.x, ubuntu-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (10.x, ubuntu-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (10.x, windows-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (10.x, windows-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (12.x, ubuntu-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (14.x, ubuntu-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (16.x, ubuntu-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (18.x, ubuntu-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (20.x, macos-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (20.x, macos-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (20.x, ubuntu-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (20.x, ubuntu-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (20.x, windows-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (20.x, windows-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (22.x, macos-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (22.x, macos-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (22.x, ubuntu-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (22.x, ubuntu-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (22.x, windows-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (22.x, windows-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (24.x, macos-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (24.x, macos-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (24.x, ubuntu-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (24.x, ubuntu-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (24.x, windows-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (24.x, windows-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (25.x, macos-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (25.x, macos-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (25.x, ubuntu-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (25.x, ubuntu-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (25.x, windows-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (25.x, windows-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (lts/*, ubuntu-latest, a, 1) (push) Blocked by required conditions Details
Github Actions / integration (lts/*, ubuntu-latest, b, 1) (push) Blocked by required conditions Details

This commit is contained in:
Alexander Akait 2025-10-22 14:03:33 +03:00 committed by GitHub
parent 3a73cc69d7
commit 57d55e492b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 770 additions and 278 deletions

View File

@ -315,7 +315,8 @@
"spacek",
"thelarkinn",
"behaviour",
"WHATWG"
"WHATWG",
"publicpath"
],
"ignoreRegExpList": [
"/Author.+/",

View File

@ -4,35 +4,69 @@
* Run `yarn fix:special` to update
*/
/**
* A function that receives the manifest object and returns the manifest string.
*/
export type HandlerFunction = (manifest: ManifestObject) => string;
/**
* Maps asset identifiers to their manifest entries.
*/
export type ManifestObject = Record<string, ManifestItem>;
export interface ManifestPluginOptions {
/**
* Enables/disables generation of the entrypoints manifest section.
*/
entrypoints?: boolean;
/**
* Specifies the filename of the output file on disk. By default the plugin will emit `manifest.json` inside the 'output.path' directory.
*/
filename?: string;
/**
* Allows filtering the files which make up the manifest.
*/
filter?: (item: ManifestItem) => boolean;
/**
* A function that receives the manifest object, modifies it, and returns the modified manifest.
*/
generate?: (manifest: ManifestObject) => ManifestObject;
/**
* Specifies a path prefix for all keys in the manifest.
*/
prefix?: string;
/**
* A function that receives the manifest object and returns the manifest string.
*/
handler?: HandlerFunction;
serialize?: (manifest: ManifestObject) => string;
}
/**
* Describes a manifest entry that links the emitted path to the producing asset.
* Describes a manifest entrypoint.
*/
export interface ManifestEntrypoint {
/**
* Contains the names of entrypoints.
*/
imports: string[];
/**
* Contains the names of parent entrypoints.
*/
parents?: string[];
}
/**
* Describes a manifest asset that links the emitted path to the producing asset.
*/
export interface ManifestItem {
/**
* The compilation asset that produced this manifest entry.
* The path absolute URL (this indicates that the path is absolute from the server's root directory) to file.
*/
asset?: import("../../lib/Compilation").Asset;
file: string;
/**
* The public path recorded in the manifest for this asset.
* The source path relative to the context.
*/
filePath: string;
src?: string;
}
/**
* The manifest object.
*/
export interface ManifestObject {
/**
* Contains the names of assets.
*/
assets: Record<string, ManifestItem>;
/**
* Contains the names of entrypoints.
*/
entrypoints: Record<string, ManifestEntrypoint>;
[k: string]: any;
}

View File

@ -1,30 +1,19 @@
This example demonstrates how to use webpack internal ManifestPlugin.
This example demonstrates how to use ManifestPlugin.
# example.js
```js
import("./baz");
```
import fooURL from "./foo.txt";
# foo.txt
const barURL = new URL("./bar.txt", import.meta.url);
```js
foo
```
async function loadAsync() {
return import("./async.js");
}
# bar.txt
await loadAsync();
```js
bar
```
# baz.js
```js
import foo from "./foo.txt";
import bar from "./bar.txt";
export default foo + bar;
export default [fooURL, barURL];
```
# webpack.config.js
@ -32,19 +21,22 @@ export default foo + bar;
```javascript
"use strict";
const YAML = require("yamljs");
const webpack = require("../../");
/** @type {webpack.Configuration} */
module.exports = {
devtool: "source-map",
output: {
chunkFilename: "[name].[contenthash].js"
},
optimization: {
chunkIds: "named" // To keep filename consistent between different modes (for example building only)
},
module: {
rules: [
{
test: /foo.txt/,
type: "asset/resource"
},
{
test: /bar.txt/,
use: require.resolve("file-loader")
}
]
@ -55,13 +47,21 @@ module.exports = {
}),
new webpack.ManifestPlugin({
filename: "manifest.yml",
handler(manifest) {
let _manifest = "";
for (const key in manifest) {
if (key === "manifest.json") continue;
_manifest += `- ${key}: '${manifest[key].filePath}'\n`;
prefix: "/nested/[publicpath]",
filter(item) {
if (/.map$/.test(item.file)) {
return false;
}
return _manifest;
return true;
},
generate(manifest) {
delete manifest.assets["manifest.json"];
manifest.custom = "value";
return manifest;
},
serialize(manifest) {
return YAML.stringify(manifest, 4);
}
})
]
@ -72,24 +72,105 @@ module.exports = {
```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"
"entrypoints": {
"main": {
"imports": [
"main.js"
]
}
},
"assets": {
"foo.txt": {
"file": "dist/3ee037f347c64cc372ad18857b0db91f.txt",
"src": "foo.txt"
},
"bar.txt": {
"file": "dist/a0145fafc7fab801e574.txt",
"src": "bar.txt"
},
"output.js.map": {
"file": "dist/output.js.map"
},
"main.js": {
"file": "dist/output.js"
},
"async_js.js.map": {
"file": "dist/async_js.d6fc644e617b14425795.js.map"
},
"async_js.js": {
"file": "dist/async_js.d6fc644e617b14425795.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'
entrypoints:
main:
imports:
- main.js
assets:
foo.txt:
file: /nested/dist/3ee037f347c64cc372ad18857b0db91f.txt
src: foo.txt
bar.txt:
file: /nested/dist/a0145fafc7fab801e574.txt
src: bar.txt
main.js:
file: /nested/dist/output.js
async_js.js:
file: /nested/dist/async_js.d6fc644e617b14425795.js
custom: value
```
# Collecting all initial scripts and styles
Here is a function to be able to get all initial scripts and styles:
```js
const fs = require("fs");
function importEntrypoints(manifest, name) {
const seen = new Set();
function getImported(entrypoint) {
const scripts = [];
const styles = [];
for (const item of entrypoint.imports) {
const importer = manifest.assets[item];
if (seen.has(item)) {
continue;
}
seen.add(item);
for (const parent of entrypoint.parents || []) {
const [parentStyles, parentScripts] = getImported(manifest.entrypoints[parent])
styles.push(...parentStyles);
scripts.push(...parentScripts);
}
if (/\.css$/.test(importer.file)) {
styles.push(importer.file);
} else {
scripts.push(importer.file);
}
}
return [styles, scripts];
}
return getImported(manifest.entrypoints[name]);
}
const manifest = JSON.parser(fs.readFilsSync("./manifest.json", "utf8"));
// Get all styles and scripts by entry name
const [styles, scripts] = importEntrypoints(manifest, "main");
```
# Info
@ -97,52 +178,51 @@ module.exports = {
## 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]
assets by info 893 bytes [immutable]
asset async_js.d6fc644e617b14425795.js 885 bytes [emitted] [immutable] 1 related asset
asset 3ee037f347c64cc372ad18857b0db91f.txt 4 bytes [emitted] [immutable] [from: foo.txt] (auxiliary name: main)
asset a0145fafc7fab801e574.txt 4 bytes [emitted] [immutable] [from: bar.txt] (auxiliary name: main)
asset output.js 15.2 KiB [emitted] (name: main) 1 related asset
asset manifest.json 601 bytes [emitted]
asset manifest.yml 395 bytes [emitted]
chunk (runtime: main) async_js.d6fc644e617b14425795.js 24 bytes [rendered]
> ./async.js ./example.js 6:8-28
./async.js 24 bytes [built] [code generated]
[exports: default]
[used exports unknown]
import() ./baz ./example.js 1:0-15
import() ./async.js ./example.js 6:8-28
chunk (runtime: main) output.js (main) 325 bytes (javascript) 4 bytes (asset) 7.67 KiB (runtime) [entry] [rendered]
> ./example.js main
runtime modules 7.67 KiB 9 modules
dependent modules 4 bytes (asset) 122 bytes (javascript) [dependent] 2 modules
./example.js 203 bytes [built] [code generated]
[exports: default]
[used exports unknown]
entry ./example.js main
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]
assets by info 205 bytes [immutable]
asset async_js.e737f493d3cb4089ee2e.js 197 bytes [emitted] [immutable] [minimized] 1 related asset
asset 3ee037f347c64cc372ad18857b0db91f.txt 4 bytes [emitted] [immutable] [from: foo.txt] (auxiliary name: main)
asset a0145fafc7fab801e574.txt 4 bytes [emitted] [immutable] [from: bar.txt] (auxiliary name: main)
asset output.js 3.18 KiB [emitted] [minimized] (name: main) 1 related asset
asset manifest.json 601 bytes [emitted]
asset manifest.yml 395 bytes [emitted]
chunk (runtime: main) async_js.e737f493d3cb4089ee2e.js 24 bytes [rendered]
> ./async.js ./example.js 6:8-28
./async.js 24 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]
import() ./async.js ./example.js 6:8-28
chunk (runtime: main) output.js (main) 325 bytes (javascript) 4 bytes (asset) 7.67 KiB (runtime) [entry] [rendered]
> ./example.js main
runtime modules 5.48 KiB 8 modules
./example.js 17 bytes [built] [code generated]
runtime modules 7.67 KiB 9 modules
dependent modules 4 bytes (asset) 122 bytes (javascript) [dependent] 2 modules
./example.js 203 bytes [built] [code generated]
[exports: default]
[no exports used]
entry ./example.js main
webpack X.X.X compiled successfully

View File

@ -0,0 +1 @@
export default "value";

View File

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

View File

@ -1 +1,11 @@
import("./baz");
import fooURL from "./foo.txt";
const barURL = new URL("./bar.txt", import.meta.url);
async function loadAsync() {
return import("./async.js");
}
await loadAsync();
export default [fooURL, barURL];

View File

@ -1,4 +1,4 @@
This example demonstrates how to use webpack internal ManifestPlugin.
This example demonstrates how to use ManifestPlugin.
# example.js
@ -6,24 +6,6 @@ This example demonstrates how to use webpack internal ManifestPlugin.
_{{example.js}}_
```
# foo.txt
```js
_{{foo.txt}}_
```
# bar.txt
```js
_{{bar.txt}}_
```
# baz.js
```js
_{{baz.js}}_
```
# webpack.config.js
```javascript
@ -42,6 +24,54 @@ _{{dist/manifest.json}}_
_{{dist/manifest.yml}}_
```
# Collecting all initial scripts and styles
Here is a function to be able to get all initial scripts and styles:
```js
const fs = require("fs");
function importEntrypoints(manifest, name) {
const seen = new Set();
function getImported(entrypoint) {
const scripts = [];
const styles = [];
for (const item of entrypoint.imports) {
const importer = manifest.assets[item];
if (seen.has(item)) {
continue;
}
seen.add(item);
for (const parent of entrypoint.parents || []) {
const [parentStyles, parentScripts] = getImported(manifest.entrypoints[parent])
styles.push(...parentStyles);
scripts.push(...parentScripts);
}
if (/\.css$/.test(importer.file)) {
styles.push(importer.file);
} else {
scripts.push(importer.file);
}
}
return [styles, scripts];
}
return getImported(manifest.entrypoints[name]);
}
const manifest = JSON.parser(fs.readFilsSync("./manifest.json", "utf8"));
// Get all styles and scripts by entry name
const [styles, scripts] = importEntrypoints(manifest, "main");
```
# Info
## Unoptimized

View File

@ -1,18 +1,21 @@
"use strict";
const YAML = require("yamljs");
const webpack = require("../../");
/** @type {webpack.Configuration} */
module.exports = {
devtool: "source-map",
output: {
chunkFilename: "[name].[contenthash].js"
},
optimization: {
chunkIds: "named" // To keep filename consistent between different modes (for example building only)
},
module: {
rules: [
{
test: /foo.txt/,
type: "asset/resource"
},
{
test: /bar.txt/,
use: require.resolve("file-loader")
}
]
@ -23,13 +26,21 @@ module.exports = {
}),
new webpack.ManifestPlugin({
filename: "manifest.yml",
handler(manifest) {
let _manifest = "";
for (const key in manifest) {
if (key === "manifest.json") continue;
_manifest += `- ${key}: '${manifest[key].filePath}'\n`;
prefix: "/nested/[publicpath]",
filter(item) {
if (/.map$/.test(item.file)) {
return false;
}
return _manifest;
return true;
},
generate(manifest) {
delete manifest.assets["manifest.json"];
manifest.custom = "value";
return manifest;
},
serialize(manifest) {
return YAML.stringify(manifest, 4);
}
})
]

View File

@ -296,6 +296,7 @@ const { isSourceEqual } = require("./util/source");
* @property {boolean=} development true, when asset is only used for development and doesn't count towards user-facing assets
* @property {boolean=} hotModuleReplacement true, when asset ships data for updating an existing application (HMR)
* @property {boolean=} javascriptModule true, when asset is javascript and an ESM
* @property {boolean=} manifest true, when file is a manifest
* @property {Record<string, null | string | string[]>=} related object of pointers to other assets, keyed by type of relation (only points from parent to child)
*/

View File

@ -5,22 +5,21 @@
"use strict";
const path = require("path");
const { RawSource } = require("webpack-sources");
const Compilation = require("./Compilation");
const HotUpdateChunk = require("./HotUpdateChunk");
const createSchemaValidation = require("./util/create-schema-validation");
/** @typedef {import("./Compiler")} Compiler */
/** @typedef {import("..").StatsCompilation} StatsCompilation */
/** @typedef {import("./Chunk")} Chunk */
/** @typedef {import("./Chunk").ChunkName} ChunkName */
/** @typedef {import("./Chunk").ChunkId} ChunkId */
/** @typedef {import("./Compilation").Asset} Asset */
/** @typedef {import("./Module")} Module */
/** @typedef {import("./NormalModule")} NormalModule */
/** @typedef {import("./config/defaults").WebpackOptionsNormalizedWithDefaults} WebpackOptions */
/** @typedef {import("./Compilation").AssetInfo} AssetInfo */
/** @typedef {import("../declarations/plugins/ManifestPlugin").ManifestPluginOptions} ManifestPluginOptions */
/** @typedef {import("../declarations/plugins/ManifestPlugin").ManifestObject} ManifestObject */
/** @typedef {import("../declarations/plugins/ManifestPlugin").ManifestEntrypoint} ManifestEntrypoint */
/** @typedef {import("../declarations/plugins/ManifestPlugin").ManifestItem} ManifestItem */
const PLUGIN_NAME = "ManifestPlugin";
@ -53,29 +52,16 @@ class ManifestPlugin {
constructor(options) {
validate(options);
/** @type {Required<ManifestPluginOptions>} */
/** @type {ManifestPluginOptions & Required<Omit<ManifestPluginOptions, "filter" | "generate">>} */
this.options = {
filename: "manifest.json",
handler: (manifest) => this._handleManifest(manifest),
prefix: "[publicpath]",
entrypoints: true,
serialize: (manifest) => JSON.stringify(manifest, null, 2),
...options
};
}
/**
* @param {ManifestObject} manifest manifest object
* @returns {string} manifest content
*/
_handleManifest(manifest) {
return JSON.stringify(
Object.keys(manifest).reduce((acc, cur) => {
acc[cur] = manifest[cur].filePath;
return acc;
}, /** @type {Record<string, string>} */ ({})),
null,
2
);
}
/**
* Apply the plugin
* @param {Compiler} compiler the compiler instance
@ -89,83 +75,156 @@ class ManifestPlugin {
stage: Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE
},
() => {
const assets = compilation.getAssets();
const hashDigestLength = compilation.outputOptions.hashDigestLength;
const publicPath = compilation.getPath(
compilation.outputOptions.publicPath
);
/** @type {Set<string>} */
const added = new Set();
/** @type {ManifestObject} */
const manifest = {};
/**
* @param {string | string[]} value value
* @returns {RegExp} regexp to remove hash
*/
const createHashRegExp = (value) =>
new RegExp(
`(?:\\.${Array.isArray(value) ? `(${value.join("|")})` : value})(?=\\.)`,
"gi"
);
/**
* @param {string} name name
* @param {AssetInfo | null} info asset info
* @returns {string} hash removed name
*/
const removeHash = (name) => {
const removeHash = (name, info) => {
// Handles hashes that match configured `hashDigestLength`
// i.e. index.XXXX.html -> index.html (html-webpack-plugin)
if (hashDigestLength <= 0) return name;
const reg = new RegExp(
`(\\.[a-f0-9]{${hashDigestLength},32})(?=\\.)`,
"gi"
);
const reg = createHashRegExp(`[a-f0-9]{${hashDigestLength},32}`);
return name.replace(reg, "");
};
/**
* @param {Chunk} chunk chunk
* @returns {ChunkName | ChunkId} chunk name or chunk id
*/
const getName = (chunk) => {
if (chunk.name) return chunk.name;
return chunk.id;
};
/** @type {ManifestObject} */
let manifest = {};
if (this.options.entrypoints) {
/** @type {ManifestObject["entrypoints"]} */
const entrypoints = {};
for (const [name, entrypoint] of compilation.entrypoints) {
const imports = [];
for (const chunk of entrypoint.chunks) {
for (const file of chunk.files) {
const name = getName(chunk);
imports.push(name ? `${name}.${extname(file)}` : file);
}
}
/** @type {ManifestEntrypoint} */
const item = { imports };
const parents = entrypoint
.getParents()
.map((item) => /** @type {string} */ (item.name));
if (parents.length > 0) {
item.parents = parents;
}
entrypoints[name] = item;
}
manifest.entrypoints = entrypoints;
}
/** @type {ManifestObject["assets"]} */
const assets = {};
/** @type {Set<string>} */
const added = new Set();
/**
* @param {string} file file
* @param {((file: string) => string)=} namer namer
* @param {string=} usedName usedName
* @returns {void}
*/
const handleFile = (file, namer) => {
const handleFile = (file, usedName) => {
if (added.has(file)) return;
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)
);
if (!asset) return;
const sourceFilename = asset.info.sourceFilename;
const name =
usedName ||
sourceFilename ||
// Fallback for unofficial plugins, just remove hash from filename
removeHash(file, asset.info);
const prefix = this.options.prefix.replace(
/\[publicpath\]/gi,
() => (publicPath === "auto" ? "/" : publicPath)
);
/** @type {ManifestItem} */
const item = { file: prefix + file };
if (sourceFilename) {
item.src = sourceFilename;
}
manifest[removeHash(name)] = {
filePath: publicPath
? publicPath +
(publicPath.endsWith("/") ? `${file}` : `/${file}`)
: file,
asset
};
if (this.options.filter) {
const needKeep = this.options.filter(item);
if (!needKeep) {
return;
}
}
assets[name] = item;
};
for (const chunk of compilation.chunks) {
if (chunk instanceof HotUpdateChunk) continue;
const chunkName = chunk.name;
for (const auxiliaryFile of chunk.auxiliaryFiles) {
handleFile(auxiliaryFile, (file) => path.basename(file));
handleFile(auxiliaryFile);
}
const name = getName(chunk);
for (const file of chunk.files) {
handleFile(file, (file) => {
if (chunkName) return `${chunkName}.${extname(file)}`;
return file;
});
handleFile(file, name ? `${name}.${extname(file)}` : file);
}
}
for (const asset of assets) {
for (const asset of compilation.getAssets()) {
if (asset.info.hotModuleReplacement) {
continue;
}
handleFile(asset.name);
}
manifest.assets = assets;
if (this.options.generate) {
manifest = this.options.generate(manifest);
}
compilation.emitAsset(
this.options.filename,
new RawSource(this.options.handler(manifest))
new RawSource(this.options.serialize(manifest)),
{ manifest: true }
);
}
);

View File

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

View File

@ -1,51 +1,98 @@
{
"definitions": {
"HandlerFunction": {
"description": "A function that receives the manifest object and returns the manifest string.",
"instanceof": "Function",
"tsType": "(manifest: ManifestObject) => string"
},
"ManifestItem": {
"description": "Describes a manifest entry that links the emitted path to the producing asset.",
"ManifestEntrypoint": {
"description": "Describes a manifest entrypoint.",
"type": "object",
"additionalProperties": false,
"properties": {
"asset": {
"description": "The compilation asset that produced this manifest entry.",
"tsType": "import('../../lib/Compilation').Asset"
"imports": {
"description": "Contains the names of entrypoints.",
"type": "array",
"items": {
"description": "The name of file.",
"type": "string",
"minLength": 1
}
},
"filePath": {
"description": "The public path recorded in the manifest for this asset.",
"parents": {
"description": "Contains the names of parent entrypoints.",
"type": "array",
"items": {
"description": "The entrypoint name.",
"type": "string",
"minLength": 1
}
}
},
"required": ["imports"]
},
"ManifestItem": {
"description": "Describes a manifest asset that links the emitted path to the producing asset.",
"type": "object",
"additionalProperties": false,
"properties": {
"file": {
"description": "The path absolute URL (this indicates that the path is absolute from the server's root directory) to file.",
"type": "string"
},
"src": {
"description": "The source path relative to the context.",
"type": "string"
}
},
"required": ["filePath"]
"required": ["file"]
},
"ManifestObject": {
"description": "Maps asset identifiers to their manifest entries.",
"description": "The manifest object.",
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/ManifestItem"
"additionalProperties": true,
"properties": {
"assets": {
"description": "Contains the names of assets.",
"type": "object",
"tsType": "Record<string, ManifestItem>"
},
"entrypoints": {
"description": "Contains the names of entrypoints.",
"type": "object",
"tsType": "Record<string, ManifestEntrypoint>"
}
},
"tsType": "Record<string, ManifestItem>"
"required": ["assets", "entrypoints"]
}
},
"title": "ManifestPluginOptions",
"type": "object",
"additionalProperties": false,
"properties": {
"entrypoints": {
"description": "Enables/disables generation of the entrypoints manifest section.",
"type": "boolean"
},
"filename": {
"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",
"absolutePath": false,
"minLength": 1
},
"handler": {
"oneOf": [
{
"$ref": "#/definitions/HandlerFunction"
}
]
"filter": {
"description": "Allows filtering the files which make up the manifest.",
"instanceof": "Function",
"tsType": "(item: ManifestItem) => boolean"
},
"generate": {
"description": "A function that receives the manifest object, modifies it, and returns the modified manifest.",
"instanceof": "Function",
"tsType": "(manifest: ManifestObject) => ManifestObject"
},
"prefix": {
"description": "Specifies a path prefix for all keys in the manifest.",
"type": "string"
},
"serialize": {
"description": "A function that receives the manifest object and returns the manifest string.",
"instanceof": "Function",
"tsType": "(manifest: ManifestObject) => string"
}
}
}

View File

@ -0,0 +1,3 @@
function doSomething() {}
doSomething();

View File

@ -0,0 +1,32 @@
import fs from "fs";
import path from "path";
import url from "../../asset-modules/_images/file.png";
it("should emit manifest with expected entries and paths with string publicPath", async () => {
await import(/* webpackChunkName: 'file' */ "./file.txt?foo");
await import("./module.js");
const manifest = JSON.parse(
fs.readFileSync(path.resolve(__dirname, "foo.json"), "utf-8")
);
const { entrypoints, assets } = manifest;
expect(entrypoints).toStrictEqual({ main: { imports: ["main.js"] } });
expect(Object.keys(assets).sort()).toEqual(
[
"main.js",
"module_js.js",
"file.js",
"file.txt?foo",
"third.party.js",
"../../asset-modules/_images/file.png"
].sort()
);
expect(assets["main.js"].file).toBe("/app/bundle0.js");
expect(assets["module_js.js"].file).toMatch(/\/app\/module_js\.[a-f0-9]+\.js/);
expect(assets["third.party.js"].file).toBe("/app/third.party.js");
expect(assets["../../asset-modules/_images/file.png"].file).toBe("/app/file-loader.png");
expect(assets["../../asset-modules/_images/file.png"].src).toBeDefined();
expect(assets["file.txt?foo"].file).toMatch(/\/app\/file\.[a-f0-9]+\.txt\?foo/);
expect(assets["file.txt?foo"].src).toBeDefined();
});

View File

@ -1,30 +1,115 @@
import fs from "fs";
import path from "path";
import "./style.css";
import "./dependency.js";
import url from "../../asset-modules/_images/file.png";
import(/* webpackChunkName: 'file' */ "./file.txt?foo");
it("should emit manifest with expected entries and paths with function publicPath", () => {
expect(url).toEqual("/dist/file-loader.png");
new URL("./file.txt", import.meta.url);
new URL("./public/other.txt", import.meta.url);
const manifest = JSON.parse(
fs.readFileSync(path.resolve(__dirname, "bar.json"), "utf-8")
);
function importEntrypoints(manifest, name) {
const seen = new Set();
const keys = Object.keys(manifest).sort();
expect(keys).toEqual(
function getImportedChunks(entrypoint) {
const scripts = [];
const styles = [];
for (const item of entrypoint.imports) {
const importee = manifest.assets[item];
if (seen.has(item)) {
continue;
}
seen.add(item);
for (const parent of entrypoint.parents || []) {
const [parentStyles, parentScripts] = getImportedChunks(manifest.entrypoints[parent])
styles.push(...parentStyles);
scripts.push(...parentScripts);
}
if (/\.css$/.test(importee.file)) {
styles.push(importee.file);
} else {
scripts.push(importee.file);
}
}
return [styles, scripts];
}
return getImportedChunks(manifest.entrypoints[name]);
}
it("should emit manifest with expected entries and paths with function publicPath", async () => {
// await import(/* webpackName: "my-name" */ "./nested/module.js?foo=bar#hash");
const manifest = __non_webpack_require__("./other.json");
const { custom, entrypoints, assets } = manifest;
expect(custom).toBe("value");
expect(entrypoints).toEqual({
"nested-shared": {
"imports": [
"runtime~nested-shared.js",
"nested-shared.js"
]
},
"shared": {
"imports": [
"shared.js",
"shared.css"
],
"parents": [
"nested-shared"
]
},
"foo": {
"imports": [
"commons-dependency_js.js",
"foo.js",
"foo.css"
],
"parents": [
"shared"
]
}
});
expect(importEntrypoints(manifest, "foo")).toEqual([
["/nested/shared.css", "/nested/foo.css"],
[
"/nested/runtime~nested-shared.js",
"/nested/nested-shared.js",
"/nested/shared.js",
"/nested/commons-dependency_js.js",
"/nested/foo.js"
],
]);
expect(Object.keys(assets).sort()).toEqual(
[
"commons-dependency_js.js",
"commons-dependency_js.js.map",
"file.txt",
"foo.js",
"foo.js.map",
"file.js",
"file.txt?foo",
"main.js",
"third.party.js",
"file.png"
"nested-shared.js",
"nested-shared.js.map",
"runtime~nested-shared.js",
"runtime~nested-shared.js.map",
"shared.css",
"shared.css.map",
"foo.css",
"foo.css.map",
"shared.js",
"shared.js.map",
"public/other.txt",
"third.party.js"
].sort()
);
expect(manifest["main.js"]).toMatch(/\/dist\/bundle1\.js/);
expect(manifest["file.js"]).toMatch(/\/dist\/file\.[a-f0-9]+\.js/);
expect(manifest["file.txt?foo"]).toMatch(/\/dist\/file\.[a-f0-9]+\.txt\?foo/);
expect(manifest["third.party.js"]).toBe("/dist/third.party.js");
expect(manifest["file.png"]).toBe("/dist/file-loader.png");
expect(assets["foo.js"].file).toBe("/nested/foo.js");
expect(assets["file.txt?foo"].file).toMatch(/\/nested\/file\.[a-f0-9]+\.txt\?foo/);
expect(assets["file.txt?foo"].src).toBeDefined();
});

View File

@ -1,30 +0,0 @@
import fs from "fs";
import path from "path";
import url from "../../asset-modules/_images/file.png";
import(/* webpackChunkName: 'file' */ "./file.txt?foo");
it("should emit manifest with expected entries and paths with string publicPath", () => {
expect(url).toEqual("/app/file-loader.png");
const manifest = JSON.parse(
fs.readFileSync(path.resolve(__dirname, "foo.json"), "utf-8")
);
const keys = Object.keys(manifest).sort();
expect(keys).toEqual(
[
"file.js",
"file.txt?foo",
"main.js",
"third.party.js",
"file.png"
].sort()
);
expect(manifest["main.js"]).toMatch(/\/app\/bundle0\.js/);
expect(manifest["file.js"]).toMatch(/\/app\/file\.[a-f0-9]+\.js/);
expect(manifest["file.txt?foo"]).toMatch(/\/app\/file\.[a-f0-9]+\.txt\?foo/);
expect(manifest["third.party.js"]).toBe("/app/third.party.js");
expect(manifest["file.png"]).toBe("/app/file-loader.png");
});

View File

@ -0,0 +1 @@
export default "dyn";

View File

@ -0,0 +1,3 @@
function doSomething() {}
doSomething();

View File

@ -0,0 +1,3 @@
function doNothing() {}
doNothing();

View File

@ -0,0 +1 @@
file

View File

@ -0,0 +1,3 @@
body {
color: red;
}

View File

@ -0,0 +1,5 @@
import "./shared.css";
function doSomething() {}
doSomething();

View File

@ -0,0 +1,3 @@
body {
background: red;
}

View File

@ -0,0 +1,17 @@
"use strict";
module.exports = {
findBundle(i) {
if (i === 0) {
return ["bundle0.js"];
}
return [
"runtime~nested-shared.js",
"nested-shared.js",
"shared.js",
"commons-dependency_js.js",
"foo.js"
];
}
};

View File

@ -29,15 +29,15 @@ class CopyPlugin {
/** @type {import("../../../../").Configuration[]} */
module.exports = [
{
node: {
__dirname: false,
__filename: false
},
entry: "./index-1.js",
output: {
publicPath: "/app/",
publicPath: () => "/app/",
chunkFilename: "[name].[contenthash].js",
assetModuleFilename: "[name].[contenthash][ext][query]"
},
optimization: {
chunkIds: "named"
},
plugins: [
new CopyPlugin(),
new webpack.ManifestPlugin({
@ -61,22 +61,28 @@ module.exports = [
}
},
{
entry: "./index-2.js",
node: {
__dirname: false,
__filename: false
target: "web",
entry: {
"nested-shared": {
import: "./nested-shared.js"
},
shared: {
dependOn: "nested-shared",
import: "./shared.js?foo=bar"
},
foo: {
dependOn: "shared",
import: "./index-2.js"
}
},
output: {
publicPath: (_data) => "/dist/",
publicPath: "auto",
filename: "[name].js",
chunkFilename: "[name].[contenthash].js",
cssChunkFilename: "[name].[contenthash].css",
assetModuleFilename: "[name].[contenthash][ext][query]"
},
plugins: [
new CopyPlugin(),
new webpack.ManifestPlugin({
filename: "bar.json"
})
],
devtool: "source-map",
module: {
rules: [
{
@ -91,6 +97,41 @@ module.exports = [
}
}
]
},
plugins: [
new CopyPlugin(),
new webpack.ManifestPlugin({
filename: "other.json",
filter(item) {
if (/file-loader.png/.test(item.file)) {
return false;
}
return true;
},
generate: (manifest) => {
manifest.custom = "value";
return manifest;
},
prefix: "/nested[publicpath]",
serialize: (manifest) => JSON.stringify(manifest)
})
],
experiments: {
css: true
},
optimization: {
chunkIds: "named",
splitChunks: {
cacheGroups: {
commons: {
enforce: true,
test: /dependency\.js$/,
chunks: "initial"
}
}
},
runtimeChunk: { name: (entrypoint) => `runtime~${entrypoint.name}` }
}
}
];

71
types.d.ts vendored
View File

@ -8671,6 +8671,11 @@ declare interface KnownAssetInfo {
*/
javascriptModule?: boolean;
/**
* true, when file is a manifest
*/
manifest?: boolean;
/**
* object of pointers to other assets, keyed by type of relation (only points from parent to child)
*/
@ -9977,25 +9982,55 @@ declare interface MakeDirectoryOptions {
}
/**
* Describes a manifest entry that links the emitted path to the producing asset.
* Describes a manifest entrypoint.
*/
declare interface ManifestEntrypoint {
/**
* Contains the names of entrypoints.
*/
imports: string[];
/**
* Contains the names of parent entrypoints.
*/
parents?: string[];
}
/**
* Describes a manifest asset that links the emitted path to the producing asset.
*/
declare interface ManifestItem {
/**
* The compilation asset that produced this manifest entry.
* The path absolute URL (this indicates that the path is absolute from the server's root directory) to file.
*/
asset?: Asset;
file: string;
/**
* The public path recorded in the manifest for this asset.
* The source path relative to the context.
*/
filePath: string;
src?: string;
}
/**
* The manifest object.
*/
declare interface ManifestObject {
[index: string]: ManifestItem;
[index: string]: any;
/**
* Contains the names of assets.
*/
assets: Record<string, ManifestItem>;
/**
* Contains the names of entrypoints.
*/
entrypoints: Record<string, ManifestEntrypoint>;
}
declare class ManifestPlugin {
constructor(options: ManifestPluginOptions);
options: Required<ManifestPluginOptions>;
options: ManifestPluginOptions &
Required<Omit<ManifestPluginOptions, "filter" | "generate">>;
/**
* Apply the plugin
@ -10003,15 +10038,35 @@ declare class ManifestPlugin {
apply(compiler: Compiler): void;
}
declare interface ManifestPluginOptions {
/**
* Enables/disables generation of the entrypoints manifest section.
*/
entrypoints?: boolean;
/**
* Specifies the filename of the output file on disk. By default the plugin will emit `manifest.json` inside the 'output.path' directory.
*/
filename?: string;
/**
* Allows filtering the files which make up the manifest.
*/
filter?: (item: ManifestItem) => boolean;
/**
* A function that receives the manifest object, modifies it, and returns the modified manifest.
*/
generate?: (manifest: ManifestObject) => ManifestObject;
/**
* Specifies a path prefix for all keys in the manifest.
*/
prefix?: string;
/**
* A function that receives the manifest object and returns the manifest string.
*/
handler?: (manifest: ManifestObject) => string;
serialize?: (manifest: ManifestObject) => string;
}
declare interface MapOptions {
/**