-
-
Notifications
You must be signed in to change notification settings - Fork 37
/
webpack.plugin.js
139 lines (136 loc) · 6.19 KB
/
webpack.plugin.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
//@ts-check
"use strict";
const webpack = require("webpack");
const { createHash } = require("crypto");
const _formatIdCache = new Map();
/** @type {(id: string, rootPath: string) => string} */
function formatId(id, rootPath) {
// Make sure all paths use /
id = id.replace(/\\/g, "/");
// For `[path]` we unwrap, format then rewrap
if (id[0] === "[" && id.endsWith("]")) {
return `[${formatId(id.slice(1, id.length - 1), rootPath)}]`;
}
// When dealing with `path1!path2`, format each segment separately
if (id.includes("!")) {
id = id
.split("!")
.map((s) => formatId(s, rootPath))
.join("!");
}
// Make the paths relative to the project's rooth path if possible
if (id.startsWith(rootPath)) {
id = id.slice(rootPath.length);
id = (id[0] === "/" ? "." : "./") + id;
}
let formatted = _formatIdCache.get(id);
if (formatted) return formatted;
// Check if we're dealing with a Yarn directory
let match = id.match(/^.*\/(\.?Yarn\/Berry|\.yarn)\/(.*)$/i);
if (!match) {
_formatIdCache.set(id, (formatted = id));
return formatted;
}
const [, yarn, filepath] = match;
// Check if we can extract the package name/version from the path
match =
filepath.match(/^unplugged\/([^/]+?)\-[\da-f]{10}\/node_modules\/(.*)$/i) ||
filepath.match(/^cache\/([^/]+?)\-[\da-f]{10}\-\d+\.zip\/node_modules\/(.*)$/i);
if (!match) {
formatted = `/${yarn.toLowerCase() === ".yarn" ? "." : ""}yarn/${filepath}`;
_formatIdCache.set(id, formatted);
return formatted;
}
const [, name, path] = match;
formatted = `${yarn.toLowerCase() === ".yarn" ? "." : "/"}yarn/${name}/${path}`;
_formatIdCache.set(id, formatted);
return formatted;
}
module.exports.formatId = formatId;
class WebpackPlugin {
_hashModuleCache = new Map();
/** @type {(mod: webpack.Module, rootPath: string) => string} */
hashModule(mod, rootPath) {
// Prefer `nameForCondition()` as it usually gives the actual file path
// while `identifier()` can have extra `!` or `|` suffixes, i.e. a hash that somehow differs between devices
const identifier = formatId(mod.nameForCondition() || mod.identifier(), rootPath);
let hash = this._hashModuleCache.get(identifier);
if (hash) return hash;
hash = createHash("sha1").update(identifier).digest("hex");
this._hashModuleCache.set(identifier, hash);
return hash;
}
/** @param {webpack.Compiler} compiler */
apply(compiler) {
// Output start/stop messages making the $ts-webpack-watch problemMatcher (provided by an extension) work
let compilationDepth = 0; // We ignore nested compilations
compiler.hooks.beforeCompile.tap("WebpackPlugin-BeforeCompile", (params) => {
if (compilationDepth++) return;
console.log("Compilation starting");
});
compiler.hooks.afterCompile.tap("WebpackPlugin-AfterCompile", () => {
if (--compilationDepth) return;
console.log("Compilation finished");
});
compiler.hooks.compilation.tap("WebpackPlugin-Compilation", (compilation) => {
const rootPath = (compilation.options.context || "").replace(/\\/g, "/");
compilation.options.optimization.chunkIds = false;
// Format `../../../Yarn/Berry/` with all the `cache`/`unplugged`/`__virtual__` to be more readable
// (i.e. `/yarn/package-npm-x.y.z/package/index.js` for global Yarn cache or `/.yarn/...` for local)
compilation.hooks.statsPrinter.tap("WebpackPlugin-StatsPrinter", (stats) => {
/** @type {(id: string | {}, context: any) => string} */
const tapModId = (id, context) => (typeof id === "string" ? formatId(context.formatModuleId(id), rootPath) : "???");
stats.hooks.print.for("module.name").tap("WebpackPlugin-ModuleName", tapModId);
});
// Include an `excludeModules` to `options.stats` to exclude modules loaded by dependencies
compilation.hooks.statsNormalize.tap("WebpackPlugin-StatsNormalize", (stats) => {
(stats.excludeModules || (stats.excludeModules = [])).push((name, { issuerPath }) => {
if (name.startsWith('external "')) return true;
const issuer = issuerPath && (issuerPath[issuerPath.length - 1].name || "").replace(/\\/g, "/");
if (!issuer) return false;
const lower = formatId(issuer, rootPath).toLowerCase();
if (lower.startsWith("/yarn/")) return true;
if (lower.startsWith(".yarn/")) return true;
return false;
});
});
// Determines how chunk IDs are generated, which is now actually deterministic
// (we make sure to clean Yarn paths to prevent issues with `../../Yarn/Berry` being different on devices)
compilation.hooks.chunkIds.tap("WebpackPlugin-ChunkIds", (chunks) => {
const chunkIds = new Map();
const overlapMap = new Set();
let minLength = 4; // show at least 3 characters
// Calculate the hashes for all the chunks
for (const chunk of chunks) {
if (chunk.id) {
console.log(`Chunk ${chunk.id} already has an ID`);
}
// We're kinda doing something similar to Webpack 5's DeterministicChunkIdsPlugin but different
const modules = compilation.chunkGraph.getChunkRootModules(chunk);
const hashes = modules.map((m) => this.hashModule(m, rootPath)).sort();
const hasher = createHash("sha1");
for (const hash of hashes) hasher.update(hash);
const hash = hasher.digest("hex");
// With a 160-bit value, a clash is very unlikely, but let's check anyway
if (chunkIds.has(hash)) throw new Error("Hash collision for chunk IDs");
chunkIds.set(chunk, hash);
chunk.id = hash;
// Make sure the minLength remains high enough to avoid collisions
for (let i = minLength; i < hash.length; i++) {
const part = hash.slice(0, i);
if (overlapMap.has(part)) continue;
overlapMap.add(part);
minLength = i;
break;
}
}
// Assign the shortened (collision-free) hashes for all the chunks
for (const [chunk, hash] of chunkIds) {
chunk.id = hash.slice(0, minLength);
chunk.ids = [chunk.id];
}
});
});
}
}
module.exports.WebpackPlugin = WebpackPlugin;