parent
2c9ff0314c
commit
b31879d5f4
@ -1,134 +1,139 @@
|
|||||||
|
|
||||||
//@ts-check
|
//@ts-check
|
||||||
'use strict';
|
"use strict";
|
||||||
|
|
||||||
|
const webpack = require("webpack");
|
||||||
|
const { createHash } = require("crypto");
|
||||||
|
|
||||||
const webpack = require('webpack');
|
const _formatIdCache = new Map();
|
||||||
const { createHash } = require('crypto');
|
/** @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 {
|
class WebpackPlugin {
|
||||||
_formatIdCache = new Map();
|
_hashModuleCache = new Map();
|
||||||
/** @type {(id: string, rootPath: string) => string} */
|
/** @type {(mod: webpack.Module, rootPath: string) => string} */
|
||||||
formatId(id, rootPath) {
|
hashModule(mod, rootPath) {
|
||||||
// Make sure all paths use /
|
// Prefer `nameForCondition()` as it usually gives the actual file path
|
||||||
id = id.replace(/\\/g, '/');
|
// while `identifier()` can have extra `!` or `|` suffixes, i.e. a hash that somehow differs between devices
|
||||||
// For `[path]` we unwrap, format then rewrap
|
const identifier = formatId(mod.nameForCondition() || mod.identifier(), rootPath);
|
||||||
if (id[0] === '[' && id.endsWith(']')) {
|
let hash = this._hashModuleCache.get(identifier);
|
||||||
return `[${this.formatId(id.slice(1, id.length - 1), rootPath)}]`;
|
if (hash) return hash;
|
||||||
}
|
hash = createHash("sha1").update(identifier).digest("hex");
|
||||||
// When dealing with `path1!path2`, format each segment separately
|
this._hashModuleCache.set(identifier, hash);
|
||||||
if (id.includes('!')) {
|
return hash;
|
||||||
id = id.split('!').map(s => this.formatId(s, rootPath)).join('!');
|
}
|
||||||
}
|
/** @param {webpack.Compiler} compiler */
|
||||||
// Make the paths relative to the project's rooth path if possible
|
apply(compiler) {
|
||||||
if (id.startsWith(rootPath)) {
|
// Output start/stop messages making the $ts-webpack-watch problemMatcher (provided by an extension) work
|
||||||
id = id.slice(rootPath.length);
|
let compilationDepth = 0; // We ignore nested compilations
|
||||||
id = (id[0] === '/' ? '.' : './') + id;
|
compiler.hooks.beforeCompile.tap("WebpackPlugin-BeforeCompile", (params) => {
|
||||||
}
|
if (compilationDepth++) return;
|
||||||
let formatted = this._formatIdCache.get(id);
|
console.log("Compilation starting");
|
||||||
if (formatted) return formatted;
|
});
|
||||||
// Check if we're dealing with a Yarn directory
|
compiler.hooks.afterCompile.tap("WebpackPlugin-AfterCompile", () => {
|
||||||
let match = id.match(/^.*\/(\.?Yarn\/Berry|\.yarn)\/(.*)$/i);
|
if (--compilationDepth) return;
|
||||||
if (!match) {
|
console.log("Compilation finished");
|
||||||
this._formatIdCache.set(id, formatted = id);
|
});
|
||||||
return formatted;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const [, yarn, filepath] = match;
|
// Assign the shortened (collision-free) hashes for all the chunks
|
||||||
// Check if we can extract the package name/version from the path
|
for (const [chunk, hash] of chunkIds) {
|
||||||
match = filepath.match(/^unplugged\/([^/]+?)\-[\da-f]{10}\/node_modules\/(.*)$/i)
|
chunk.id = hash.slice(0, minLength);
|
||||||
|| filepath.match(/^cache\/([^/]+?)\-[\da-f]{10}\-\d+\.zip\/node_modules\/(.*)$/i);
|
chunk.ids = [chunk.id];
|
||||||
if (!match) {
|
|
||||||
formatted = `/${yarn.toLowerCase() === '.yarn' ? '.' : ''}yarn/${filepath}`;
|
|
||||||
this._formatIdCache.set(id, formatted);
|
|
||||||
return formatted;
|
|
||||||
}
|
}
|
||||||
const [, name, path] = match;
|
});
|
||||||
formatted = `${yarn.toLowerCase() === '.yarn' ? '.' : '/'}yarn/${name}/${path}`;
|
});
|
||||||
this._formatIdCache.set(id, formatted);
|
}
|
||||||
return formatted;
|
|
||||||
}
|
|
||||||
_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 = this.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' ? this.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 = this.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;
|
module.exports.WebpackPlugin = WebpackPlugin;
|
||||||
|
Loading…
Reference in new issue