From 768bfdac205b9376e61f7c0edd721952404a27ce Mon Sep 17 00:00:00 2001 From: Kelvin Schoofs Date: Sun, 7 Nov 2021 19:50:58 +0100 Subject: [PATCH] Fix/improve map-error.js utility --- CHANGELOG.md | 5 + map-error.js | 15 ++- webpack.plugin.js | 257 +++++++++++++++++++++++----------------------- 3 files changed, 147 insertions(+), 130 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02b754b..b123165 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/map-error.js b/map-error.js index 629e71a..a8ecb29 100644 --- a/map-error.js +++ b/map-error.js @@ -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>} */ 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); diff --git a/webpack.plugin.js b/webpack.plugin.js index a7c71e3..d81816f 100644 --- a/webpack.plugin.js +++ b/webpack.plugin.js @@ -1,134 +1,139 @@ - //@ts-check -'use strict'; +"use strict"; + +const webpack = require("webpack"); +const { createHash } = require("crypto"); -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 { - _formatIdCache = new Map(); - /** @type {(id: string, rootPath: string) => string} */ - 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 `[${this.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('!'); - } - // 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 = this._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); - 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 = 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; + } } - 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}`; - this._formatIdCache.set(id, formatted); - return formatted; + // 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]; } - 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;