diff --git a/package-lock.json b/package-lock.json index d267de89..7126aa6c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6297,9 +6297,8 @@ }, "node_modules/json-stringify-safe": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/json5": { "version": "1.0.2", @@ -6400,9 +6399,8 @@ }, "node_modules/lodash": { "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.memoize": { "version": "4.1.2", @@ -6734,9 +6732,8 @@ }, "node_modules/nock": { "version": "13.3.1", - "resolved": "https://registry.npmjs.org/nock/-/nock-13.3.1.tgz", - "integrity": "sha512-vHnopocZuI93p2ccivFyGuUfzjq2fxNyNurp7816mlT5V5HF4SzXu8lvLrVzBbNqzs+ODooZ6OksuSUNM7Njkw==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^4.1.0", "json-stringify-safe": "^5.0.1", @@ -7388,9 +7385,8 @@ }, "node_modules/propagate": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", - "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -10593,6 +10589,8 @@ "ts-loader-webpack-4": "npm:ts-loader@^8.4.0", "typescript": "^5.0.4", "webpack-4": "npm:webpack@^4.46.0", + "webpack-cli": "^5.1.4", + "webpack-node-externals": "^3.0.0", "webpack-sources-webpack-4": "npm:webpack-sources@^1.4.1" }, "engines": { @@ -11039,6 +11037,8 @@ "ts-loader-webpack-4": "npm:ts-loader@^8.4.0", "typescript": "^5.0.4", "webpack-4": "npm:webpack@^4.46.0", + "webpack-cli": "^5.1.4", + "webpack-node-externals": "^3.0.0", "webpack-sources-webpack-4": "npm:webpack-sources@^1.4.1" } }, @@ -14860,8 +14860,6 @@ }, "json-stringify-safe": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", "dev": true }, "json5": { @@ -14922,8 +14920,6 @@ }, "lodash": { "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, "lodash.memoize": { @@ -15159,8 +15155,6 @@ }, "nock": { "version": "13.3.1", - "resolved": "https://registry.npmjs.org/nock/-/nock-13.3.1.tgz", - "integrity": "sha512-vHnopocZuI93p2ccivFyGuUfzjq2fxNyNurp7816mlT5V5HF4SzXu8lvLrVzBbNqzs+ODooZ6OksuSUNM7Njkw==", "dev": true, "requires": { "debug": "^4.1.0", @@ -15578,8 +15572,6 @@ }, "propagate": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", - "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", "dev": true }, "prr": { diff --git a/tools/webpack-plugin/.gitignore b/tools/webpack-plugin/.gitignore new file mode 100644 index 00000000..2f9f20c3 --- /dev/null +++ b/tools/webpack-plugin/.gitignore @@ -0,0 +1 @@ +webpackBuild/ diff --git a/tools/webpack-plugin/package.json b/tools/webpack-plugin/package.json index 039c73d4..c5bc9c3d 100644 --- a/tools/webpack-plugin/package.json +++ b/tools/webpack-plugin/package.json @@ -9,6 +9,7 @@ }, "scripts": { "build": "tsc -b ./tsconfig.build.json", + "build:webpack": "webpack", "clean": "tsc -b ./tsconfig.build.json --clean && rimraf \"lib\"", "format": "prettier --write '**/*.ts'", "lint": "eslint . --ext .ts", @@ -45,9 +46,11 @@ "jest": "^29.5.0", "ts-jest": "^29.1.0", "ts-loader": "^9.4.3 || ^8.4.0", + "ts-loader-webpack-4": "npm:ts-loader@^8.4.0", "typescript": "^5.0.4", "webpack-4": "npm:webpack@^4.46.0", - "ts-loader-webpack-4": "npm:ts-loader@^8.4.0", + "webpack-cli": "^5.1.4", + "webpack-node-externals": "^3.0.0", "webpack-sources-webpack-4": "npm:webpack-sources@^1.4.1" }, "dependencies": { diff --git a/tools/webpack-plugin/src/BacktracePluginV4.ts b/tools/webpack-plugin/src/BacktracePluginV4.ts index e541262c..1288a50b 100644 --- a/tools/webpack-plugin/src/BacktracePluginV4.ts +++ b/tools/webpack-plugin/src/BacktracePluginV4.ts @@ -3,6 +3,8 @@ import crypto from 'crypto'; import path from 'path'; import { Compiler, WebpackPluginInstance } from 'webpack'; import { BacktraceWebpackSourceGenerator } from './BacktraceWebpackSourceGenerator'; +import { statsPrinter } from './helpers/statsPrinter'; +import { AssetStats } from './models/AssetStats'; import { BacktracePluginOptions } from './models/BacktracePluginOptions'; export class BacktracePluginV4 implements WebpackPluginInstance { @@ -20,62 +22,113 @@ export class BacktracePluginV4 implements WebpackPluginInstance { } public apply(compiler: Compiler) { - const assetDebugIds = new Map(); + const assetStats = new Map(); compiler.hooks.emit.tap(BacktracePluginV4.name, (compilation) => { + const logger = compilation.getLogger(BacktracePluginV4.name); for (const key in compilation.assets) { let source = compilation.assets[key]; - let debugId; - if (key.match(/.(c|m)?jsx?$/)) { - debugId = crypto.randomUUID(); - assetDebugIds.set(key, debugId); + if (key.match(/\.(c|m)?jsx?$/)) { + const debugId = crypto.randomUUID(); + const stats: AssetStats = { debugId }; + assetStats.set(key, stats); + logger.log(`[${key}] generated debug ID ${debugId}`); - source = this._sourceGenerator.addDebugIdToSource(source as never, debugId) as typeof source; - source = this._sourceGenerator.addDebugIdCommentToSource(source as never, debugId) as typeof source; + logger.time(`[${key}] inject source snippet`); + try { + source = this._sourceGenerator.addDebugIdToSource(source as never, debugId) as typeof source; + logger.timeEnd(`[${key}] inject source snippet`); + stats.sourceSnippet = true; + } catch (err) { + stats.sourceSnippet = err instanceof Error ? err : new Error('Unknown error.'); + } + + logger.time(`[${key}] inject sourcemap key`); + try { + source = this._sourceGenerator.addDebugIdCommentToSource( + source as never, + debugId, + ) as typeof source; + logger.timeEnd(`[${key}] inject sourcemap key`); + stats.sourceComment = true; + } catch (err) { + stats.sourceComment = err instanceof Error ? err : new Error('Unknown error.'); + } } else if (key.match(/\.(c|m)?jsx?\.map$/)) { // The .map replacement should account for most of the use cases const sourceKey = key.replace(/.map$/, ''); - debugId = assetDebugIds.get(sourceKey); - if (!debugId) { + const stats = assetStats.get(sourceKey); + if (!stats) { continue; } - source = this._sourceGenerator.addDebugIdToRawSourceMap(source as never, debugId) as never; + logger.time(`[${key}] append sourcemap key`); + try { + source = this._sourceGenerator.addDebugIdToRawSourceMap( + source as never, + stats.debugId, + ) as never; + logger.timeEnd(`[${key}] append sourcemap key`); + stats.sourceMapAppend = true; + } catch (err) { + stats.sourceMapAppend = err instanceof Error ? err : new Error('Unknown error.'); + } } compilation.assets[key] = source; } }); - const uploader = this._sourceMapUploader; - if (uploader) { - compiler.hooks.afterEmit.tapPromise(BacktracePluginV4.name, async (compilation) => { - const outputPath = compilation.outputOptions.path; - if (!outputPath) { - throw new Error('Output path is required to upload sourcemaps.'); + compiler.hooks.afterEmit.tapPromise(BacktracePluginV4.name, async (compilation) => { + const logger = compilation.getLogger(BacktracePluginV4.name); + + const outputPath = compilation.outputOptions.path; + if (!outputPath) { + throw new Error('Output path is required to upload sourcemaps.'); + } + + for (const key in compilation.assets) { + if (!key.match(/\.(c|m)?jsx?\.map$/)) { + continue; } - for (const key in compilation.assets) { - if (!key.match(/\.(c|m)?jsx?\.map$/)) { - continue; - } + const sourceKey = key.replace(/.map$/, ''); + const stats = assetStats.get(sourceKey); + if (!stats) { + continue; + } - const sourceKey = key.replace(/.map$/, ''); - const debugId = assetDebugIds.get(sourceKey); - if (!debugId) { - continue; - } + const sourceMapAsset = compilation.getAsset(key); + if (!sourceMapAsset) { + stats.sourceMapUpload = false; + continue; + } - const sourceMapAsset = compilation.getAsset(key); - if (!sourceMapAsset) { - continue; - } + if (!this._sourceMapUploader) { + stats.sourceMapUpload = false; + continue; + } - const sourceMapPath = path.join(outputPath, sourceMapAsset.name); - await uploader.upload(sourceMapPath, debugId); + const sourceMapPath = path.join(outputPath, sourceMapAsset.name); + + logger.time(`[${key}] upload sourcemap`); + try { + const result = await this._sourceMapUploader.upload(sourceMapPath, stats.debugId); + logger.timeEnd(`[${key}] upload sourcemap`); + stats.sourceMapUpload = result; + } catch (err) { + stats.sourceMapAppend = err instanceof Error ? err : new Error('Unknown error.'); } - }); - } + logger.timeEnd(`[${key}] upload sourcemap`); + } + }); + + compiler.hooks.afterEmit.tap(BacktracePluginV4.name, (compilation) => { + const printer = statsPrinter(compilation.getLogger(BacktracePluginV4.name)); + for (const [key, stats] of assetStats) { + printer(key, stats); + } + }); } } diff --git a/tools/webpack-plugin/src/BacktracePluginV5.ts b/tools/webpack-plugin/src/BacktracePluginV5.ts index e130a457..fcbf87a3 100644 --- a/tools/webpack-plugin/src/BacktracePluginV5.ts +++ b/tools/webpack-plugin/src/BacktracePluginV5.ts @@ -4,6 +4,8 @@ import path from 'path'; import { AssetInfo, Compilation, Compiler, WebpackPluginInstance } from 'webpack'; import { SourceMapSource } from 'webpack-sources'; import { BacktraceWebpackSourceGenerator } from './BacktraceWebpackSourceGenerator'; +import { statsPrinter } from './helpers/statsPrinter'; +import { AssetStats } from './models/AssetStats'; import { BacktracePluginOptions } from './models/BacktracePluginOptions'; export class BacktracePluginV5 implements WebpackPluginInstance { @@ -21,8 +23,7 @@ export class BacktracePluginV5 implements WebpackPluginInstance { } public apply(compiler: Compiler) { - const assetDebugIds = new Map(); - const processedSourceMapsForSources = new Set(); + const assetStats = new Map(); compiler.hooks.thisCompilation.tap(BacktracePluginV5.name, (compilation) => { compilation.hooks.processAssets.tap( @@ -31,11 +32,45 @@ export class BacktracePluginV5 implements WebpackPluginInstance { stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONS, }, (assets) => { + const logger = compilation.getLogger(BacktracePluginV5.name); + for (const key in assets) { + if (!key.match(/\.(c|m)?jsx?$/)) { + continue; + } + const debugId = crypto.randomUUID(); - assetDebugIds.set(key, debugId); + assetStats.set(key, { debugId }); + + logger.log(`[${key}] generated debug ID ${debugId}`); + } + }, + ); + + compilation.hooks.processAssets.tap( + { + name: BacktracePluginV5.name, + stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONS, + }, + (assets) => { + const logger = compilation.getLogger(BacktracePluginV5.name); + + for (const key in assets) { + const stats = assetStats.get(key); + if (!stats) { + continue; + } + + const debugId = stats.debugId; - this.injectSourceSnippet(compilation, key, debugId); + logger.time(`[${key}] inject source snippet`); + try { + this.injectSourceSnippet(compilation, key, debugId); + logger.timeEnd(`[${key}] inject source snippet`); + stats.sourceSnippet = true; + } catch (err) { + stats.sourceSnippet = err instanceof Error ? err : new Error('Unknown error.'); + } } }, ); @@ -47,14 +82,26 @@ export class BacktracePluginV5 implements WebpackPluginInstance { additionalAssets: true, }, (assets) => { + const logger = compilation.getLogger(BacktracePluginV5.name); + for (const key in assets) { - const debugId = assetDebugIds.get(key); - if (!debugId) { + const stats = assetStats.get(key); + if (!stats) { continue; } - if (this.injectSourceMapDebugId(compilation, key, debugId)) { - processedSourceMapsForSources.add(key); + const debugId = stats.debugId; + + logger.time(`[${key}] inject sourcemap key`); + try { + if (this.injectSourceMapDebugId(compilation, key, debugId)) { + logger.timeEnd(`[${key}] inject sourcemap key`); + stats.sourceMapAppend = true; + } else { + stats.sourceMapAppend = false; + } + } catch (err) { + stats.sourceComment = err instanceof Error ? err : new Error('Unknown error.'); } } return; @@ -68,24 +115,43 @@ export class BacktracePluginV5 implements WebpackPluginInstance { stage: Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE, }, (assets) => { + const logger = compilation.getLogger(BacktracePluginV5.name); + for (const key in assets) { const asset = compilation.getAsset(key); if (!asset) { continue; } - const debugId = assetDebugIds.get(key); - if (!debugId) { + const stats = assetStats.get(key); + if (!stats) { continue; } - this.injectSourceComment(compilation, key, debugId); + const debugId = stats.debugId; + + logger.time(`[${key}] inject source comment`); + try { + this.injectSourceComment(compilation, key, debugId); + logger.timeEnd(`[${key}] inject source comment`); + stats.sourceComment = true; + } catch (err) { + stats.sourceComment = err instanceof Error ? err : new Error('Unknown error.'); + } // If the sourcemap has not been processed for some reason, // attempt to manually append the information - if (!processedSourceMapsForSources.has(key)) { - if (this.appendSourceMapDebugId(compilation, key, debugId, processedSourceMaps)) { - processedSourceMapsForSources.add(key); + if (!stats.sourceMapAppend) { + logger.time(`[${key}] append sourcemap key`); + try { + if (this.appendSourceMapDebugId(compilation, key, debugId, processedSourceMaps)) { + logger.timeEnd(`[${key}] append sourcemap key`); + stats.sourceMapAppend = true; + } else { + stats.sourceMapAppend = false; + } + } catch (err) { + stats.sourceMapAppend = err instanceof Error ? err : new Error('Unknown error.'); } } } @@ -93,42 +159,61 @@ export class BacktracePluginV5 implements WebpackPluginInstance { ); }); - const uploader = this._sourceMapUploader; - if (uploader) { - compiler.hooks.afterEmit.tapPromise(BacktracePluginV5.name, async (compilation) => { - const outputPath = compilation.outputOptions.path; - if (!outputPath) { - throw new Error('Output path is required to upload sourcemaps.'); + compiler.hooks.afterEmit.tapPromise(BacktracePluginV5.name, async (compilation) => { + const logger = compilation.getLogger(BacktracePluginV5.name); + + const outputPath = compilation.outputOptions.path; + if (!outputPath) { + throw new Error('Output path is required to upload sourcemaps.'); + } + + for (const key in compilation.assets) { + const asset = compilation.getAsset(key); + if (!asset) { + continue; } - for (const key in compilation.assets) { - const asset = compilation.getAsset(key); - if (!asset) { - continue; - } + const stats = assetStats.get(key); + if (!stats) { + continue; + } - const debugId = assetDebugIds.get(key); - if (!debugId) { - continue; - } + const sourceMapKeys = this.getSourceMapKeys(asset.info); + if (!sourceMapKeys) { + stats.sourceMapUpload = false; + continue; + } - const sourceMapKeys = this.getSourceMapKeys(asset.info); - if (!sourceMapKeys) { + if (!this._sourceMapUploader) { + stats.sourceMapUpload = false; + continue; + } + + for (const key of sourceMapKeys) { + const sourceMapAsset = compilation.getAsset(key); + if (!sourceMapAsset) { continue; } - for (const key of sourceMapKeys) { - const sourceMapAsset = compilation.getAsset(key); - if (!sourceMapAsset) { - continue; - } - - const sourceMapPath = path.join(outputPath, sourceMapAsset.name); - await uploader.upload(sourceMapPath, debugId); + logger.time(`[${key}] upload sourcemap`); + const sourceMapPath = path.join(outputPath, sourceMapAsset.name); + try { + const result = await this._sourceMapUploader.upload(sourceMapPath, stats.debugId); + logger.timeEnd(`[${key}] upload sourcemap`); + stats.sourceMapUpload = result; + } catch (err) { + stats.sourceMapUpload = err instanceof Error ? err : new Error('Unknown error.'); } } - }); - } + } + }); + + compiler.hooks.afterEmit.tap(BacktracePluginV5.name, (compilation) => { + const printer = statsPrinter(compilation.getLogger(BacktracePluginV5.name)); + for (const [key, stats] of assetStats) { + printer(key, stats); + } + }); } private injectSourceSnippet(compilation: Compilation, key: string, debugId: string): boolean { diff --git a/tools/webpack-plugin/src/helpers/statsPrinter.ts b/tools/webpack-plugin/src/helpers/statsPrinter.ts new file mode 100644 index 00000000..3c6e8564 --- /dev/null +++ b/tools/webpack-plugin/src/helpers/statsPrinter.ts @@ -0,0 +1,64 @@ +import webpack from 'webpack'; +import { AssetStats } from '../models/AssetStats'; + +function statToString(stat: boolean | string | Error) { + if (typeof stat === 'string') { + return stat; + } + + if (typeof stat === 'boolean') { + return stat ? 'successful' : 'skipped'; + } + + return stat.message; +} + +export function statsPrinter(logger: webpack.Compilation['logger']) { + return function printStats(key: string, stats: AssetStats) { + const errors = [stats.sourceComment, stats.sourceMapAppend, stats.sourceMapUpload, stats.sourceSnippet].some( + (v) => v instanceof Error, + ); + + const infoLog = errors + ? (...args: unknown[]) => logger.error(...args) + : (...args: unknown[]) => logger.info(...args); + + const debugLog = errors + ? (...args: unknown[]) => logger.error(...args) + : (...args: unknown[]) => logger.log(...args); + + if (!errors) { + if (stats.sourceMapUpload !== false && !(stats.sourceMapUpload instanceof Error)) { + infoLog(`[${key}] processed file and uploaded sourcemap successfully`); + } else { + infoLog(`[${key}] processed file successfully`); + } + } else { + debugLog(`[${key}] processed file with errors`); + } + + debugLog(`\tdebugId: ${stats.debugId}`); + + if (stats.sourceSnippet != null) { + debugLog(`\tsource snippet append: ${statToString(stats.sourceSnippet) ?? ''}`); + } + + if (stats.sourceComment != null) { + debugLog(`\tsource comment append: ${statToString(stats.sourceComment) ?? ''}`); + } + + if (stats.sourceMapAppend != null) { + debugLog(`\tsourcemap key append: ${statToString(stats.sourceMapAppend) ?? ''}`); + } + + if (stats.sourceMapUpload != null) { + if (stats.sourceMapUpload === false || stats.sourceMapUpload instanceof Error) { + debugLog(`\tsourcemap upload: ${statToString(stats.sourceMapUpload)}`); + } else { + debugLog( + `\tsourcemap upload: yes, rxid: ${stats.sourceMapUpload.rxid}, debugId: ${stats.sourceMapUpload.debugId}`, + ); + } + } + }; +} diff --git a/tools/webpack-plugin/src/models/AssetStats.ts b/tools/webpack-plugin/src/models/AssetStats.ts new file mode 100644 index 00000000..e28cfeb1 --- /dev/null +++ b/tools/webpack-plugin/src/models/AssetStats.ts @@ -0,0 +1,9 @@ +import { UploadResult } from '@backtrace/sourcemap-tools'; + +export interface AssetStats { + readonly debugId: string; + sourceSnippet?: boolean | string | Error; + sourceComment?: boolean | string | Error; + sourceMapAppend?: boolean | string | Error; + sourceMapUpload?: false | UploadResult | Error; +} diff --git a/tools/webpack-plugin/webpack.config.js b/tools/webpack-plugin/webpack.config.js new file mode 100644 index 00000000..5a09c65a --- /dev/null +++ b/tools/webpack-plugin/webpack.config.js @@ -0,0 +1,39 @@ +const path = require('path'); +const { BacktracePlugin } = require('./lib'); +const nodeExternals = require('webpack-node-externals'); + +/** @type {import('webpack').Configuration} */ +module.exports = { + entry: './src/index.ts', + devtool: 'source-map', + mode: 'production', + target: 'node', + externalsPresets: { node: true }, + resolve: { + extensions: ['.ts', '.js'], + }, + stats: { + logging: 'verbose', + }, + module: { + rules: [ + { + test: /.ts$/, + loader: 'ts-loader', + options: { + configFile: 'tsconfig.build.json', + }, + }, + ], + }, + output: { + path: path.join(__dirname, './webpackBuild'), + filename: '[name].js', + }, + externals: [ + nodeExternals({ + additionalModuleDirs: ['../../node_modules'], + }), + ], + plugins: [new BacktracePlugin({})], +};