Fix/improve map-error.js utility

pull/373/head
Kelvin Schoofs 3 years ago
parent 2c9ff0314c
commit 768bfdac20

@ -1,6 +1,11 @@
# Changelog
## Unreleased
### Development changes
- Fix/improve `map-error.js` utility (now also uses `formatId` from `webpack.plugin.js`)
## v1.24.0 (2021-11-02)
### Changes

@ -1,8 +1,11 @@
const sm = require('source-map');
const rl = require('readline');
const path = require('path');
const fs = require('fs');
const { formatId } = require('./webpack.plugin.js');
/** @type {Record<string, Promise<sm.BasicSourceMapConsumer>>} */
const maps = {};
@ -18,8 +21,11 @@ for (const file of fs.readdirSync('./dist')) {
console.log();
const SOURCE_NAME_REGEX = /^\s*at .*? \(.*?[/\\]dist[/\\]((?:\d+\.)?extension\.js):(\d+):(\d+)\)$/;
const SOURCE_ANOM_REGEX = /^\s*at .*?[/\\]dist[/\\]((?:\d+\.)?extension\.js):(\d+):(\d+)$/;
const SOURCE_NAME_REGEX = /^\s*at .*? \(.*?[/\\]dist[/\\]((?:[\da-zA-Z]+\.)?extension\.js):(\d+):(\d+)\)$/;
const SOURCE_ANOM_REGEX = /^\s*at .*?[/\\]dist[/\\]((?:[\da-zA-Z]+\.)?extension\.js):(\d+):(\d+)$/;
const ROOT_PATH = path.resolve(__dirname);
const FORMAT_ID = process.argv.includes('--format-id');
let error = '';
rl.createInterface(process.stdin).on('line', async l => {
@ -44,10 +50,11 @@ rl.createInterface(process.stdin).on('line', async l => {
continue;
}
const ws = stack.match(/^\s*/)[0];
const source = FORMAT_ID ? formatId(pos.source, ROOT_PATH) : pos.source;
if (named && pos.name) {
stack = `${ws}at ${pos.name} (${pos.source}:${pos.line}:${pos.column})`;
stack = `${ws}at ${pos.name} (${source}:${pos.line}:${pos.column})`;
} else {
stack = `${ws}at ${pos.source}:${pos.line}:${pos.column}`;
stack = `${ws}at ${source}:${pos.line}:${pos.column}`;
}
}
console.log(stack);

@ -1,60 +1,65 @@
//@ts-check
'use strict';
"use strict";
const webpack = require('webpack');
const { createHash } = require('crypto');
const webpack = require("webpack");
const { createHash } = require("crypto");
class WebpackPlugin {
_formatIdCache = new Map();
const _formatIdCache = new Map();
/** @type {(id: string, rootPath: string) => string} */
formatId(id, rootPath) {
function formatId(id, rootPath) {
// Make sure all paths use /
id = id.replace(/\\/g, '/');
id = id.replace(/\\/g, "/");
// For `[path]` we unwrap, format then rewrap
if (id[0] === '[' && id.endsWith(']')) {
return `[${this.formatId(id.slice(1, id.length - 1), rootPath)}]`;
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 => this.formatId(s, rootPath)).join('!');
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;
id = (id[0] === "/" ? "." : "./") + id;
}
let formatted = this._formatIdCache.get(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) {
this._formatIdCache.set(id, formatted = id);
_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);
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}`;
this._formatIdCache.set(id, formatted);
formatted = `/${yarn.toLowerCase() === ".yarn" ? "." : ""}yarn/${filepath}`;
_formatIdCache.set(id, formatted);
return formatted;
}
const [, name, path] = match;
formatted = `${yarn.toLowerCase() === '.yarn' ? '.' : '/'}yarn/${name}/${path}`;
this._formatIdCache.set(id, formatted);
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 = this.formatId(mod.nameForCondition() || mod.identifier(), rootPath);
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');
hash = createHash("sha1").update(identifier).digest("hex");
this._hashModuleCache.set(identifier, hash);
return hash;
}
@ -62,39 +67,39 @@ class WebpackPlugin {
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) => {
compiler.hooks.beforeCompile.tap("WebpackPlugin-BeforeCompile", (params) => {
if (compilationDepth++) return;
console.log('Compilation starting');
console.log("Compilation starting");
});
compiler.hooks.afterCompile.tap('WebpackPlugin-AfterCompile', () => {
compiler.hooks.afterCompile.tap("WebpackPlugin-AfterCompile", () => {
if (--compilationDepth) return;
console.log('Compilation finished');
console.log("Compilation finished");
});
compiler.hooks.compilation.tap('WebpackPlugin-Compilation', compilation => {
const rootPath = (compilation.options.context || '').replace(/\\/g, '/');
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 => {
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);
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 => {
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, '/');
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;
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 => {
compilation.hooks.chunkIds.tap("WebpackPlugin-ChunkIds", (chunks) => {
const chunkIds = new Map();
const overlapMap = new Set();
let minLength = 4; // show at least 3 characters
@ -105,12 +110,12 @@ class WebpackPlugin {
}
// 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');
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');
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');
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

Loading…
Cancel
Save