From 82bdcc4ad433d6332294b2bbee5d057fe7806a61 Mon Sep 17 00:00:00 2001 From: Sebastian Alex Date: Mon, 26 Jun 2023 10:48:01 +0000 Subject: [PATCH 1/5] webpack plugin: replaces version plugins with a simpler plugin using SourceProcessor --- tools/webpack-plugin/package.json | 2 +- tools/webpack-plugin/src/BacktracePlugin.ts | 94 ++++-- tools/webpack-plugin/src/BacktracePluginV4.ts | 46 --- tools/webpack-plugin/src/BacktracePluginV5.ts | 173 ---------- .../src/BacktraceWebpackSourceGenerator.ts | 39 --- tools/webpack-plugin/src/index.ts | 3 - .../src/models/BacktracePluginOptions.ts | 4 +- .../webpack-plugin/tests/e2e/createE2ETest.ts | 30 ++ tools/webpack-plugin/tests/e2e/helpers.ts | 10 +- .../e2e/no-sourcemaps/no-sourcemaps.spec.ts | 29 +- .../BacktraceWebpackSourceGenerator.spec.ts | 300 ------------------ 11 files changed, 138 insertions(+), 592 deletions(-) delete mode 100644 tools/webpack-plugin/src/BacktracePluginV4.ts delete mode 100644 tools/webpack-plugin/src/BacktracePluginV5.ts delete mode 100644 tools/webpack-plugin/src/BacktraceWebpackSourceGenerator.ts delete mode 100644 tools/webpack-plugin/tests/unit/BacktraceWebpackSourceGenerator.spec.ts diff --git a/tools/webpack-plugin/package.json b/tools/webpack-plugin/package.json index 039c73d4..3bad683e 100644 --- a/tools/webpack-plugin/package.json +++ b/tools/webpack-plugin/package.json @@ -12,7 +12,7 @@ "clean": "tsc -b ./tsconfig.build.json --clean && rimraf \"lib\"", "format": "prettier --write '**/*.ts'", "lint": "eslint . --ext .ts", - "watch": "tsc -b ./tsconfig.packages.json -w", + "watch": "tsc -b ./tsconfig.build.json -w", "test": "npm run test:webpackv5 && npm run test:webpackv4", "test:webpackv4": "NODE_ENV=test jest --config ./webpack4.jest.config.js", "test:webpackv5": "NODE_ENV=test jest", diff --git a/tools/webpack-plugin/src/BacktracePlugin.ts b/tools/webpack-plugin/src/BacktracePlugin.ts index 542a5326..3e405aaa 100644 --- a/tools/webpack-plugin/src/BacktracePlugin.ts +++ b/tools/webpack-plugin/src/BacktracePlugin.ts @@ -1,19 +1,77 @@ -import webpack from 'webpack'; -import { BacktracePluginV4 } from './BacktracePluginV4'; -import { BacktracePluginV5 } from './BacktracePluginV5'; - -let BacktracePlugin: typeof BacktracePluginV4 | typeof BacktracePluginV5; - -const version = process.env.WEBPACK_VERSION ?? webpack.version[0]; -switch (version) { - case '4': - BacktracePlugin = BacktracePluginV4; - break; - case '5': - BacktracePlugin = BacktracePluginV5; - break; - default: - throw new Error(`Webpack version ${version} is not supported.`); -} +import { DebugIdGenerator, SourceMapUploader, SourceProcessor } from '@backtrace/sourcemap-tools'; +import path from 'path'; +import webpack, { WebpackPluginInstance } from 'webpack'; +import { BacktracePluginOptions } from './models/BacktracePluginOptions'; + +export class BacktracePlugin implements WebpackPluginInstance { + private readonly _sourceMapProcessor: SourceProcessor; + private readonly _sourceMapUploader?: SourceMapUploader; + + constructor(public readonly options?: BacktracePluginOptions) { + this._sourceMapProcessor = new SourceProcessor(options?.debugIdGenerator ?? new DebugIdGenerator()); + + this._sourceMapUploader = + options?.sourceMapUploader ?? (options?.uploadUrl ? new SourceMapUploader(options.uploadUrl) : undefined); + } + + public apply(compiler: webpack.Compiler) { + compiler.hooks.afterEmit.tapPromise(BacktracePlugin.name, async (compilation) => { + const logger = compilation.getLogger(BacktracePlugin.name); + if (!compilation.outputOptions.path) { + logger.error('skipping everything because outputOptions.path is not set, a bug?'); + return; + } + + const entries: [string, string, string][] = []; + + for (const asset in compilation.assets) { + if (!asset.match(/\.(c|m)?jsx?$/)) { + logger.debug(`[${asset}] skipping processing, extension does not match`); + continue; + } -export { BacktracePlugin }; + const map = asset + '.map'; + if (!compilation.assets[map]) { + logger.debug(`[${asset}] skipping processing, map file not found`); + continue; + } + + const assetPath = path.join(compilation.outputOptions.path, asset); + const sourceMapPath = path.join(compilation.outputOptions.path, map); + + logger.debug(`adding asset ${assetPath} with sourcemap ${sourceMapPath}`); + entries.push([asset, assetPath, sourceMapPath]); + } + + logger.log(`received ${entries.length} files for processing`); + + for (const [asset, sourcePath, sourceMapPath] of entries) { + logger.time(`[${asset}] process source and sourcemap`); + + let debugId: string; + try { + debugId = await this._sourceMapProcessor.processSourceAndSourceMapFiles(sourcePath, sourceMapPath); + logger.timeEnd(`[${asset}] process source and sourcemap`); + } catch (err) { + logger.timeEnd(`[${asset}] process source and sourcemap`); + logger.error(`[${asset}] process source and sourcemap failed:`, err); + continue; + } + + if (this._sourceMapUploader) { + logger.time(`[${asset}] upload sourcemap`); + try { + await this._sourceMapUploader.upload(sourceMapPath, debugId); + logger.timeEnd(`[${asset}] upload sourcemap`); + } catch (err) { + logger.timeEnd(`[${asset}] upload sourcemap`); + logger.error(`[${asset}] upload sourcemap failed:`, err); + } + logger.log(`[${asset}] file processed and sourcemap uploaded`); + } else { + logger.log(`[${asset}] file processed`); + } + } + }); + } +} diff --git a/tools/webpack-plugin/src/BacktracePluginV4.ts b/tools/webpack-plugin/src/BacktracePluginV4.ts deleted file mode 100644 index 04ecf814..00000000 --- a/tools/webpack-plugin/src/BacktracePluginV4.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { ContentAppender, DebugIdGenerator } from '@backtrace/sourcemap-tools'; -import crypto from 'crypto'; -import { Compiler, WebpackPluginInstance } from 'webpack'; -import { BacktraceWebpackSourceGenerator } from './BacktraceWebpackSourceGenerator'; -import { BacktracePluginOptions } from './models/BacktracePluginOptions'; - -export class BacktracePluginV4 implements WebpackPluginInstance { - private readonly _sourceGenerator: BacktraceWebpackSourceGenerator; - - constructor(public readonly options?: BacktracePluginOptions) { - this._sourceGenerator = new BacktraceWebpackSourceGenerator( - options?.debugIdGenerator ?? new DebugIdGenerator(), - new ContentAppender(), - ); - } - - public apply(compiler: Compiler) { - const assetDebugIds = new Map(); - - compiler.hooks.emit.tap(BacktracePluginV4.name, (compilation) => { - 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); - - source = this._sourceGenerator.addDebugIdToSource(source as never, debugId) as typeof source; - source = this._sourceGenerator.addDebugIdCommentToSource(source as never, debugId) as typeof source; - } 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) { - continue; - } - - source = this._sourceGenerator.addDebugIdToRawSourceMap(source as never, debugId) as never; - } - - compilation.assets[key] = source; - } - }); - } -} diff --git a/tools/webpack-plugin/src/BacktracePluginV5.ts b/tools/webpack-plugin/src/BacktracePluginV5.ts deleted file mode 100644 index 5ae43047..00000000 --- a/tools/webpack-plugin/src/BacktracePluginV5.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { ContentAppender, DebugIdGenerator } from '@backtrace/sourcemap-tools'; -import crypto from 'crypto'; -import { Compilation, Compiler, WebpackPluginInstance } from 'webpack'; -import { SourceMapSource } from 'webpack-sources'; -import { BacktraceWebpackSourceGenerator } from './BacktraceWebpackSourceGenerator'; -import { BacktracePluginOptions } from './models/BacktracePluginOptions'; - -export class BacktracePluginV5 implements WebpackPluginInstance { - private readonly _sourceGenerator: BacktraceWebpackSourceGenerator; - - constructor(public readonly options?: BacktracePluginOptions) { - this._sourceGenerator = new BacktraceWebpackSourceGenerator( - options?.debugIdGenerator ?? new DebugIdGenerator(), - new ContentAppender(), - ); - } - - public apply(compiler: Compiler) { - const assetDebugIds = new Map(); - const processedSourceMapsForSources = new Set(); - - compiler.hooks.thisCompilation.tap(BacktracePluginV5.name, (compilation) => { - compilation.hooks.processAssets.tap( - { - name: BacktracePluginV5.name, - stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONS, - }, - (assets) => { - for (const key in assets) { - const debugId = crypto.randomUUID(); - assetDebugIds.set(key, debugId); - - this.injectSourceSnippet(compilation, key, debugId); - } - }, - ); - - compilation.hooks.processAssets.tap( - { - name: BacktracePluginV5.name, - stage: Compilation.PROCESS_ASSETS_STAGE_DEV_TOOLING, - additionalAssets: true, - }, - (assets) => { - for (const key in assets) { - const debugId = assetDebugIds.get(key); - if (!debugId) { - continue; - } - - if (this.injectSourceMapDebugId(compilation, key, debugId)) { - processedSourceMapsForSources.add(key); - } - } - return; - }, - ); - - const processedSourceMaps = new Set(); - compilation.hooks.processAssets.tap( - { - name: BacktracePluginV5.name, - stage: Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE, - }, - (assets) => { - for (const key in assets) { - const asset = compilation.getAsset(key); - if (!asset) { - continue; - } - - const debugId = assetDebugIds.get(key); - if (!debugId) { - continue; - } - - this.injectSourceComment(compilation, key, debugId); - - // 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); - } - } - } - }, - ); - }); - } - - private injectSourceSnippet(compilation: Compilation, key: string, debugId: string): boolean { - const asset = compilation.getAsset(key); - if (!asset) { - return false; - } - - const newSource = this._sourceGenerator.addDebugIdToSource(asset.source as never, debugId); - - compilation.updateAsset(key, newSource as never); - return true; - } - - private injectSourceMapDebugId(compilation: Compilation, key: string, debugId: string): boolean { - const asset = compilation.getAsset(key); - if (!asset) { - return false; - } - - if (!(asset.source instanceof SourceMapSource)) { - return false; - } - - const newSource = this._sourceGenerator.addDebugIdToSourceMap(asset.source, debugId); - compilation.updateAsset(key, newSource as never); - - return true; - } - - private injectSourceComment(compilation: Compilation, key: string, debugId: string): boolean { - const asset = compilation.getAsset(key); - if (!asset) { - return false; - } - - const newSource = this._sourceGenerator.addDebugIdCommentToSource(asset.source as never, debugId); - compilation.updateAsset(key, newSource as never); - - return true; - } - - /** - * Manually appends debug ID keys to the sourcemap file. - */ - private appendSourceMapDebugId( - compilation: Compilation, - key: string, - debugId: string, - processedSourceMaps: Set, - ): boolean { - const assetInfo = compilation.assetsInfo.get(key); - if (!assetInfo) { - return false; - } - - let sourceMapKeys = assetInfo.related?.sourceMap; - if (!sourceMapKeys) { - return false; - } - - if (!Array.isArray(sourceMapKeys)) { - sourceMapKeys = [sourceMapKeys]; - } - - for (const sourceMapKey of sourceMapKeys) { - if (processedSourceMaps.has(sourceMapKey)) { - continue; - } - - const sourceMapAsset = compilation.getAsset(sourceMapKey); - if (!sourceMapAsset) { - continue; - } - - const newSource = this._sourceGenerator.addDebugIdToRawSourceMap(sourceMapAsset.source as never, debugId); - compilation.updateAsset(sourceMapKey, newSource as never); - - processedSourceMaps.add(sourceMapKey); - } - - return true; - } -} diff --git a/tools/webpack-plugin/src/BacktraceWebpackSourceGenerator.ts b/tools/webpack-plugin/src/BacktraceWebpackSourceGenerator.ts deleted file mode 100644 index 19b2bc74..00000000 --- a/tools/webpack-plugin/src/BacktraceWebpackSourceGenerator.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { ContentAppender, DebugIdGenerator } from '@backtrace/sourcemap-tools'; -import type { Source } from 'webpack-sources'; -import { ConcatSource, RawSource, SourceMapSource } from 'webpack-sources'; - -export class BacktraceWebpackSourceGenerator { - constructor( - private readonly _debugIdGenerator: DebugIdGenerator, - private readonly _contentAppender: ContentAppender, - ) {} - - public addDebugIdToSource(source: Source, debugId: string): ConcatSource { - const sourceSnippet = this._debugIdGenerator.generateSourceSnippet(debugId); - return new ConcatSource(source, '\n' + sourceSnippet); - } - - public addDebugIdCommentToSource(source: Source, debugId: string): ConcatSource { - const comment = this._debugIdGenerator.generateSourceComment(debugId); - return new ConcatSource(source, '\n' + comment); - } - - public addDebugIdToSourceMap(sourceMapSource: SourceMapSource, debugId: string): SourceMapSource { - const { source, map } = sourceMapSource.sourceAndMap(); - if (!map) { - return sourceMapSource; - } - - const newMap = this._debugIdGenerator.addSourceMapKey(map, debugId); - - // The file name does not matter at this point, and it is set to 'x' in Webpack - return new SourceMapSource(source as string, 'x', newMap as never); - } - - public addDebugIdToRawSourceMap(source: Source, debugId: string): RawSource { - let sourceMapSource = (source.source() as Buffer).toString('utf8'); - const debugSourceMapObj = this._debugIdGenerator.addSourceMapKey({}, debugId); - sourceMapSource = this._contentAppender.appendToJSON(sourceMapSource, debugSourceMapObj); - return new RawSource(sourceMapSource); - } -} diff --git a/tools/webpack-plugin/src/index.ts b/tools/webpack-plugin/src/index.ts index 7b12f012..127704fa 100644 --- a/tools/webpack-plugin/src/index.ts +++ b/tools/webpack-plugin/src/index.ts @@ -1,7 +1,4 @@ import { BacktracePlugin } from './BacktracePlugin'; - -export { BacktracePluginV4 } from './BacktracePluginV4'; -export { BacktracePluginV5 } from './BacktracePluginV5'; export { BacktracePluginOptions } from './models/BacktracePluginOptions'; export { BacktracePlugin }; export default BacktracePlugin; diff --git a/tools/webpack-plugin/src/models/BacktracePluginOptions.ts b/tools/webpack-plugin/src/models/BacktracePluginOptions.ts index 862564fc..72388fbd 100644 --- a/tools/webpack-plugin/src/models/BacktracePluginOptions.ts +++ b/tools/webpack-plugin/src/models/BacktracePluginOptions.ts @@ -1,5 +1,7 @@ -import { DebugIdGenerator } from '@backtrace/sourcemap-tools'; +import { DebugIdGenerator, SourceMapUploader } from '@backtrace/sourcemap-tools'; export interface BacktracePluginOptions { debugIdGenerator?: DebugIdGenerator; + sourceMapUploader?: SourceMapUploader; + uploadUrl?: string | URL; } diff --git a/tools/webpack-plugin/tests/e2e/createE2ETest.ts b/tools/webpack-plugin/tests/e2e/createE2ETest.ts index 698ec401..43c0f5ed 100644 --- a/tools/webpack-plugin/tests/e2e/createE2ETest.ts +++ b/tools/webpack-plugin/tests/e2e/createE2ETest.ts @@ -1,5 +1,7 @@ +import { SourceMapUploader } from '@backtrace/sourcemap-tools'; import assert from 'assert'; import fs from 'fs'; +import path from 'path'; import webpack from 'webpack'; import { asyncWebpack, @@ -17,6 +19,7 @@ interface E2ETestOptions { testSourceComment?: boolean; testSourceMap?: boolean; testSourceEval?: boolean; + testSourceMapUpload?: boolean; } export function createE2ETest( @@ -24,9 +27,21 @@ export function createE2ETest( opts?: E2ETestOptions, ) { webpackModeTest((mode) => { + function mockUploader() { + return jest.spyOn(SourceMapUploader.prototype, 'upload').mockImplementation((_, debugId) => + Promise.resolve({ + debugId: debugId ?? crypto.randomUUID(), + rxid: crypto.randomUUID(), + }), + ); + } + let result: webpack.Stats; + let uploadSpy: ReturnType; beforeAll(async () => { + uploadSpy = mockUploader(); + const config = configBuilder(mode); if (config.output?.path) { await removeDir(config.output.path); @@ -96,5 +111,20 @@ export function createE2ETest( } }); } + + if (opts?.testSourceMapUpload ?? true) { + it('should upload sourcemaps using SourceMapUploader', async () => { + const outputDir = result.compilation.outputOptions.path; + assert(outputDir); + + const mapFiles = await getFiles(outputDir, /.js.map$/); + expect(mapFiles.length).toBeGreaterThan(0); + + const uploadedFiles = uploadSpy.mock.calls.map((c) => path.resolve(c[0])); + for (const file of mapFiles) { + expect(uploadedFiles).toContain(path.resolve(file)); + } + }); + } }); } diff --git a/tools/webpack-plugin/tests/e2e/helpers.ts b/tools/webpack-plugin/tests/e2e/helpers.ts index dc215b18..b13ae6b3 100644 --- a/tools/webpack-plugin/tests/e2e/helpers.ts +++ b/tools/webpack-plugin/tests/e2e/helpers.ts @@ -1,3 +1,4 @@ +import { SourceMapUploader } from '@backtrace/sourcemap-tools'; import fs from 'fs'; import path from 'path'; import webpack from 'webpack'; @@ -30,7 +31,14 @@ export function getBaseConfig(config: webpack.Configuration, options?: BaseConfi }, ], }, - plugins: [new BacktracePlugin(options?.pluginOptions ?? { debugIdGenerator: new TestDebugIdGenerator() })], + plugins: [ + new BacktracePlugin( + options?.pluginOptions ?? { + debugIdGenerator: new TestDebugIdGenerator(), + sourceMapUploader: new SourceMapUploader('http://localhost'), + }, + ), + ], ...config, }; } diff --git a/tools/webpack-plugin/tests/e2e/no-sourcemaps/no-sourcemaps.spec.ts b/tools/webpack-plugin/tests/e2e/no-sourcemaps/no-sourcemaps.spec.ts index 58bd4bc7..9ee2d8eb 100644 --- a/tools/webpack-plugin/tests/e2e/no-sourcemaps/no-sourcemaps.spec.ts +++ b/tools/webpack-plugin/tests/e2e/no-sourcemaps/no-sourcemaps.spec.ts @@ -1,13 +1,13 @@ +import { SourceProcessor } from '@backtrace/sourcemap-tools'; import path from 'path'; -import { createE2ETest } from '../createE2ETest'; -import { getBaseConfig } from '../helpers'; +import { asyncWebpack, expectSuccess, getBaseConfig, removeDir, webpackModeTest } from '../helpers'; describe('No sourcemaps', () => { const outputDir = path.join(__dirname, './output'); - createE2ETest( - (mode) => - getBaseConfig( + webpackModeTest((mode) => { + it('should not call SourceProcessor when devtool is false', async () => { + const config = getBaseConfig( { mode, devtool: false, @@ -18,9 +18,18 @@ describe('No sourcemaps', () => { }, }, { tsconfigPath: path.join(__dirname, './tsconfig.test.json') }, - ), - { - testSourceMap: false, - }, - ); + ); + + if (config.output?.path) { + await removeDir(config.output.path); + } + + const sourceProcessorSpy = jest.spyOn(SourceProcessor.prototype, 'processSourceAndSourceMap'); + + const webpackResult = await asyncWebpack(config); + expectSuccess(webpackResult); + + expect(sourceProcessorSpy).not.toBeCalled(); + }, 120000); + }); }); diff --git a/tools/webpack-plugin/tests/unit/BacktraceWebpackSourceGenerator.spec.ts b/tools/webpack-plugin/tests/unit/BacktraceWebpackSourceGenerator.spec.ts deleted file mode 100644 index 45109996..00000000 --- a/tools/webpack-plugin/tests/unit/BacktraceWebpackSourceGenerator.spec.ts +++ /dev/null @@ -1,300 +0,0 @@ -import { ContentAppender, DebugIdGenerator } from '@backtrace/sourcemap-tools'; -import webpack from 'webpack'; -import { ConcatSource, RawSource, SourceMapSource } from 'webpack-sources'; -import { BacktraceWebpackSourceGenerator } from '../../src/BacktraceWebpackSourceGenerator'; - -describe('BacktraceWebpackSourceGenerator', () => { - function createTestSourceMap() { - const sourceMap: ConstructorParameters[2] = { - file: 'x', - mappings: 'aACE', - names: ['x'], - sources: ['x'], - version: 3, - }; - - return sourceMap; - } - - describe('addDebugIdToSource', () => { - it('should append source snippet generated by debugIdGenerator', () => { - const expected = 'def'; - - const source = new RawSource('abc'); - const debugIdGenerator = new DebugIdGenerator(); - const contentAppender = new ContentAppender(); - - jest.spyOn(debugIdGenerator, 'generateSourceSnippet').mockReturnValue(expected); - - const sourceGenerator = new BacktraceWebpackSourceGenerator(debugIdGenerator, contentAppender); - const actualSource = sourceGenerator.addDebugIdToSource(source, 'x'); - const actual = actualSource.source(); - - expect(actual).toContain('abc'); - expect(actual).toContain(expected); - }); - - it('should pass uuid to debugIdGenerator', () => { - const expected = 'def'; - - const source = new RawSource('abc'); - const debugIdGenerator = new DebugIdGenerator(); - const contentAppender = new ContentAppender(); - - const spy = jest.spyOn(debugIdGenerator, 'generateSourceSnippet').mockReturnValue(expected); - - const sourceGenerator = new BacktraceWebpackSourceGenerator(debugIdGenerator, contentAppender); - sourceGenerator.addDebugIdToSource(source, expected); - - expect(spy).toBeCalledWith(expected); - }); - - it('should return an instance of ConcatSource', () => { - const source = new RawSource('abc'); - const debugIdGenerator = new DebugIdGenerator(); - const contentAppender = new ContentAppender(); - - const sourceGenerator = new BacktraceWebpackSourceGenerator(debugIdGenerator, contentAppender); - const actual = sourceGenerator.addDebugIdToSource(source, 'def'); - - expect(actual).toBeInstanceOf(ConcatSource); - }); - - it('should not modify original source', () => { - const expected = 'abc'; - const source = new RawSource(expected); - const debugIdGenerator = new DebugIdGenerator(); - const contentAppender = new ContentAppender(); - - const sourceGenerator = new BacktraceWebpackSourceGenerator(debugIdGenerator, contentAppender); - sourceGenerator.addDebugIdToSource(source, 'def'); - - expect(source.source()).toEqual(expected); - }); - }); - - describe('addDebugIdCommentToSource', () => { - it('should append comment snippet generated by debugIdGenerator', () => { - const expected = 'def'; - - const source = new RawSource('abc'); - const debugIdGenerator = new DebugIdGenerator(); - const contentAppender = new ContentAppender(); - - jest.spyOn(debugIdGenerator, 'generateSourceComment').mockReturnValue(expected); - - const sourceGenerator = new BacktraceWebpackSourceGenerator(debugIdGenerator, contentAppender); - const actualSource = sourceGenerator.addDebugIdCommentToSource(source, 'x'); - const actual = actualSource.source(); - - expect(actual).toContain('abc'); - expect(actual).toContain(expected); - }); - - it('should pass uuid to debugIdGenerator', () => { - const expected = 'def'; - - const source = new RawSource('abc'); - const debugIdGenerator = new DebugIdGenerator(); - const contentAppender = new ContentAppender(); - - const spy = jest.spyOn(debugIdGenerator, 'generateSourceComment').mockReturnValue(expected); - - const sourceGenerator = new BacktraceWebpackSourceGenerator(debugIdGenerator, contentAppender); - sourceGenerator.addDebugIdCommentToSource(source, expected); - - expect(spy).toBeCalledWith(expected); - }); - - it('should return an instance of ConcatSource', () => { - const source = new RawSource('abc'); - const debugIdGenerator = new DebugIdGenerator(); - const contentAppender = new ContentAppender(); - - const sourceGenerator = new BacktraceWebpackSourceGenerator(debugIdGenerator, contentAppender); - const actual = sourceGenerator.addDebugIdCommentToSource(source, 'def'); - - expect(actual).toBeInstanceOf(ConcatSource); - }); - - it('should not modify original source', () => { - const expected = 'abc'; - const source = new RawSource(expected); - const debugIdGenerator = new DebugIdGenerator(); - const contentAppender = new ContentAppender(); - - const sourceGenerator = new BacktraceWebpackSourceGenerator(debugIdGenerator, contentAppender); - sourceGenerator.addDebugIdCommentToSource(source, 'def'); - - expect(source.source()).toEqual(expected); - }); - }); - - // We do not support this on Webpack 4, nor it is used - if (webpack.version[0] !== '4') { - describe('addDebugIdToSourceMap', () => { - it('should append whole object generated by debugIdGenerator', () => { - const source = new SourceMapSource('abc', 'x', createTestSourceMap()); - const expected = { - ...source.map(), - debugId: '123', - newKey2: 456, - }; - - const debugIdGenerator = new DebugIdGenerator(); - const contentAppender = new ContentAppender(); - - jest.spyOn(debugIdGenerator, 'addSourceMapKey').mockReturnValue(expected); - - const sourceGenerator = new BacktraceWebpackSourceGenerator(debugIdGenerator, contentAppender); - const actualSourceMapSource = sourceGenerator.addDebugIdToSourceMap(source, 'x'); - - const { map: actualMap } = actualSourceMapSource.sourceAndMap(); - expect(actualMap).toEqual(expected); - }); - - it('should pass sourcemap to debugIdGenerator', () => { - const expected = createTestSourceMap(); - - const source = new SourceMapSource('abc', 'x', expected); - const debugIdGenerator = new DebugIdGenerator(); - const contentAppender = new ContentAppender(); - - const spy = jest - .spyOn(debugIdGenerator, 'addSourceMapKey') - .mockReturnValue({ ...createTestSourceMap(), debugId: '123' }); - - const sourceGenerator = new BacktraceWebpackSourceGenerator(debugIdGenerator, contentAppender); - sourceGenerator.addDebugIdToSourceMap(source, 'def'); - - expect(spy).toBeCalledWith(expected, expect.anything()); - }); - - it('should pass uuid to debugIdGenerator', () => { - const expected = 'def'; - - const source = new SourceMapSource('abc', 'x', createTestSourceMap()); - const debugIdGenerator = new DebugIdGenerator(); - const contentAppender = new ContentAppender(); - - const spy = jest - .spyOn(debugIdGenerator, 'addSourceMapKey') - .mockReturnValue({ ...createTestSourceMap(), debugId: '123' }); - - const sourceGenerator = new BacktraceWebpackSourceGenerator(debugIdGenerator, contentAppender); - sourceGenerator.addDebugIdToSourceMap(source, expected); - - expect(spy).toBeCalledWith(expect.anything(), expected); - }); - - it('should return an instance of SourceMapSource', () => { - const source = new SourceMapSource('abc', 'x', createTestSourceMap()); - const debugIdGenerator = new DebugIdGenerator(); - const contentAppender = new ContentAppender(); - - const sourceGenerator = new BacktraceWebpackSourceGenerator(debugIdGenerator, contentAppender); - const actual = sourceGenerator.addDebugIdToSourceMap(source, 'def'); - - expect(actual).toBeInstanceOf(SourceMapSource); - }); - - it('should not modify original source', () => { - const expected = 'abc'; - const source = new SourceMapSource(expected, 'x', createTestSourceMap()); - const debugIdGenerator = new DebugIdGenerator(); - const contentAppender = new ContentAppender(); - - const sourceGenerator = new BacktraceWebpackSourceGenerator(debugIdGenerator, contentAppender); - sourceGenerator.addDebugIdCommentToSource(source, 'def'); - - const { source: actualSourceMap } = source.sourceAndMap(); - expect(actualSourceMap.toString()).toEqual(expected); - }); - - it('should not modify original sourcemap', () => { - const expected = createTestSourceMap(); - const modifiedSourceMap = { - ...expected, - debugId: '123', - newKey2: 456, - }; - - const source = new SourceMapSource('abc', 'x', expected); - const debugIdGenerator = new DebugIdGenerator(); - const contentAppender = new ContentAppender(); - - jest.spyOn(debugIdGenerator, 'addSourceMapKey').mockReturnValue(modifiedSourceMap); - - const sourceGenerator = new BacktraceWebpackSourceGenerator(debugIdGenerator, contentAppender); - sourceGenerator.addDebugIdToSourceMap(source, 'x'); - - const { map: actualMap } = source.sourceAndMap(); - expect(actualMap).toEqual(expected); - }); - }); - } - - describe('addDebugIdToRawSourceMap', () => { - it('should append whole object generated by debugIdGenerator', () => { - const sourceMap = createTestSourceMap(); - const expected = { - ...sourceMap, - debugId: '123', - newKey2: 456, - }; - - const source = new RawSource(JSON.stringify(sourceMap)); - const debugIdGenerator = new DebugIdGenerator(); - const contentAppender = new ContentAppender(); - - jest.spyOn(debugIdGenerator, 'addSourceMapKey').mockReturnValue(expected); - - const sourceGenerator = new BacktraceWebpackSourceGenerator(debugIdGenerator, contentAppender); - const actualSource = sourceGenerator.addDebugIdToRawSourceMap(source, 'x'); - - const actual = JSON.parse(actualSource.source()); - expect(actual).toEqual(expected); - }); - - it('should pass uuid to debugIdGenerator', () => { - const expected = 'def'; - - const source = new RawSource(JSON.stringify(createTestSourceMap())); - const debugIdGenerator = new DebugIdGenerator(); - const contentAppender = new ContentAppender(); - - const spy = jest - .spyOn(debugIdGenerator, 'addSourceMapKey') - .mockReturnValue({ ...createTestSourceMap(), debugId: '123' }); - - const sourceGenerator = new BacktraceWebpackSourceGenerator(debugIdGenerator, contentAppender); - sourceGenerator.addDebugIdToRawSourceMap(source, expected); - - expect(spy).toBeCalledWith(expect.anything(), expected); - }); - - it('should return an instance of RawSource', () => { - const source = new RawSource(JSON.stringify(createTestSourceMap())); - const debugIdGenerator = new DebugIdGenerator(); - const contentAppender = new ContentAppender(); - - const sourceGenerator = new BacktraceWebpackSourceGenerator(debugIdGenerator, contentAppender); - const actual = sourceGenerator.addDebugIdToRawSourceMap(source, 'def'); - - expect(actual).toBeInstanceOf(RawSource); - }); - - it('should not modify original source', () => { - const expected = JSON.stringify(createTestSourceMap()); - const source = new RawSource(expected); - const debugIdGenerator = new DebugIdGenerator(); - const contentAppender = new ContentAppender(); - - const sourceGenerator = new BacktraceWebpackSourceGenerator(debugIdGenerator, contentAppender); - sourceGenerator.addDebugIdCommentToSource(source, 'def'); - - const { source: actualSourceMap } = source.sourceAndMap(); - expect(actualSourceMap).toEqual(expected); - }); - }); -}); From 02c591690153f8770b90a1b5faabce69e93db826 Mon Sep 17 00:00:00 2001 From: Sebastian Alex Date: Mon, 26 Jun 2023 11:52:41 +0000 Subject: [PATCH 2/5] webpack plugin: removes unit tests --- tools/webpack-plugin/jest.config.js | 6 ------ tools/webpack-plugin/package.json | 3 --- tools/webpack-plugin/webpack4.jest.config.js | 7 ------- 3 files changed, 16 deletions(-) delete mode 100644 tools/webpack-plugin/jest.config.js delete mode 100644 tools/webpack-plugin/webpack4.jest.config.js diff --git a/tools/webpack-plugin/jest.config.js b/tools/webpack-plugin/jest.config.js deleted file mode 100644 index c0919546..00000000 --- a/tools/webpack-plugin/jest.config.js +++ /dev/null @@ -1,6 +0,0 @@ -/** @type {import('ts-jest').JestConfigWithTsJest} */ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testPathIgnorePatterns: ['e2e'], -}; diff --git a/tools/webpack-plugin/package.json b/tools/webpack-plugin/package.json index 3bad683e..97754e47 100644 --- a/tools/webpack-plugin/package.json +++ b/tools/webpack-plugin/package.json @@ -13,9 +13,6 @@ "format": "prettier --write '**/*.ts'", "lint": "eslint . --ext .ts", "watch": "tsc -b ./tsconfig.build.json -w", - "test": "npm run test:webpackv5 && npm run test:webpackv4", - "test:webpackv4": "NODE_ENV=test jest --config ./webpack4.jest.config.js", - "test:webpackv5": "NODE_ENV=test jest", "test:e2e": "npm run test:e2e:webpackv5 && npm run test:e2e:webpackv4", "test:e2e:webpackv4": "NODE_ENV=test jest --config ./webpack4.e2e.jest.config.js", "test:e2e:webpackv5": "NODE_ENV=test jest --config ./e2e.jest.config.js" diff --git a/tools/webpack-plugin/webpack4.jest.config.js b/tools/webpack-plugin/webpack4.jest.config.js deleted file mode 100644 index ea8797bb..00000000 --- a/tools/webpack-plugin/webpack4.jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -/** @type {import('ts-jest').JestConfigWithTsJest} */ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - setupFiles: ['./tests/setupWebpackV4.ts'], - testPathIgnorePatterns: ['e2e'], -}; From 5da306bce60a0c02fcf327a6ffc0e87b258ea1ac Mon Sep 17 00:00:00 2001 From: Sebastian Alex Date: Wed, 28 Jun 2023 09:34:20 +0000 Subject: [PATCH 3/5] webpack plugin: simplifies options, refactors logging --- tools/webpack-plugin/src/BacktracePlugin.ts | 45 ++++++++++--------- .../src/models/BacktracePluginOptions.ts | 15 +++++-- 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/tools/webpack-plugin/src/BacktracePlugin.ts b/tools/webpack-plugin/src/BacktracePlugin.ts index 3e405aaa..2d7fe73a 100644 --- a/tools/webpack-plugin/src/BacktracePlugin.ts +++ b/tools/webpack-plugin/src/BacktracePlugin.ts @@ -4,14 +4,14 @@ import webpack, { WebpackPluginInstance } from 'webpack'; import { BacktracePluginOptions } from './models/BacktracePluginOptions'; export class BacktracePlugin implements WebpackPluginInstance { - private readonly _sourceMapProcessor: SourceProcessor; + private readonly _sourceProcessor: SourceProcessor; private readonly _sourceMapUploader?: SourceMapUploader; constructor(public readonly options?: BacktracePluginOptions) { - this._sourceMapProcessor = new SourceProcessor(options?.debugIdGenerator ?? new DebugIdGenerator()); - - this._sourceMapUploader = - options?.sourceMapUploader ?? (options?.uploadUrl ? new SourceMapUploader(options.uploadUrl) : undefined); + this._sourceProcessor = new SourceProcessor(new DebugIdGenerator()); + this._sourceMapUploader = options?.uploadUrl + ? new SourceMapUploader(options.uploadUrl, options.uploadOptions) + : undefined; } public apply(compiler: webpack.Compiler) { @@ -46,30 +46,31 @@ export class BacktracePlugin implements WebpackPluginInstance { logger.log(`received ${entries.length} files for processing`); for (const [asset, sourcePath, sourceMapPath] of entries) { - logger.time(`[${asset}] process source and sourcemap`); - let debugId: string; + + logger.time(`[${asset}] process source and sourcemap`); try { - debugId = await this._sourceMapProcessor.processSourceAndSourceMapFiles(sourcePath, sourceMapPath); - logger.timeEnd(`[${asset}] process source and sourcemap`); + debugId = await this._sourceProcessor.processSourceAndSourceMapFiles(sourcePath, sourceMapPath); } catch (err) { - logger.timeEnd(`[${asset}] process source and sourcemap`); logger.error(`[${asset}] process source and sourcemap failed:`, err); continue; + } finally { + logger.timeEnd(`[${asset}] process source and sourcemap`); + } + + if (!this._sourceMapUploader) { + logger.info(`[${asset}] file processed`); + continue; } - if (this._sourceMapUploader) { - logger.time(`[${asset}] upload sourcemap`); - try { - await this._sourceMapUploader.upload(sourceMapPath, debugId); - logger.timeEnd(`[${asset}] upload sourcemap`); - } catch (err) { - logger.timeEnd(`[${asset}] upload sourcemap`); - logger.error(`[${asset}] upload sourcemap failed:`, err); - } - logger.log(`[${asset}] file processed and sourcemap uploaded`); - } else { - logger.log(`[${asset}] file processed`); + logger.time(`[${asset}] upload sourcemap`); + try { + await this._sourceMapUploader.upload(sourceMapPath, debugId); + logger.info(`[${asset}] file processed and sourcemap uploaded`); + } catch (err) { + logger.error(`[${asset}] upload sourcemap failed:`, err); + } finally { + logger.timeEnd(`[${asset}] upload sourcemap`); } } }); diff --git a/tools/webpack-plugin/src/models/BacktracePluginOptions.ts b/tools/webpack-plugin/src/models/BacktracePluginOptions.ts index 72388fbd..e402d1e4 100644 --- a/tools/webpack-plugin/src/models/BacktracePluginOptions.ts +++ b/tools/webpack-plugin/src/models/BacktracePluginOptions.ts @@ -1,7 +1,16 @@ -import { DebugIdGenerator, SourceMapUploader } from '@backtrace/sourcemap-tools'; +import { SourceMapUploaderOptions } from '@backtrace/sourcemap-tools'; export interface BacktracePluginOptions { - debugIdGenerator?: DebugIdGenerator; - sourceMapUploader?: SourceMapUploader; + /** + * Upload URL for uploading sourcemap files. + * See Source Maps Integration Guide for your instance for more information. + * + * If not set, the sourcemaps will not be uploaded. The sources will be still processed and ready for manual upload. + */ uploadUrl?: string | URL; + + /** + * Additional upload options. + */ + uploadOptions?: SourceMapUploaderOptions; } From 93b6e2e4afc4e9d8ef07d66b0913256a5b73c965 Mon Sep 17 00:00:00 2001 From: Sebastian Alex Date: Wed, 28 Jun 2023 09:34:53 +0000 Subject: [PATCH 4/5] webpack plugin: changes e2e tests to work with new options --- .../webpack-plugin/tests/e2e/createE2ETest.ts | 135 ++++++------------ tools/webpack-plugin/tests/e2e/helpers.ts | 10 +- 2 files changed, 44 insertions(+), 101 deletions(-) diff --git a/tools/webpack-plugin/tests/e2e/createE2ETest.ts b/tools/webpack-plugin/tests/e2e/createE2ETest.ts index 43c0f5ed..65e03f46 100644 --- a/tools/webpack-plugin/tests/e2e/createE2ETest.ts +++ b/tools/webpack-plugin/tests/e2e/createE2ETest.ts @@ -1,31 +1,11 @@ -import { SourceMapUploader } from '@backtrace/sourcemap-tools'; +import { SourceMapUploader, SourceProcessor } from '@backtrace/sourcemap-tools'; import assert from 'assert'; import fs from 'fs'; import path from 'path'; import webpack from 'webpack'; -import { - asyncWebpack, - expectSourceComment, - expectSourceMapSnippet, - expectSourceSnippet, - expectSuccess, - getFiles, - removeDir, - webpackModeTest, -} from './helpers'; +import { asyncWebpack, expectSuccess, getFiles, removeDir, webpackModeTest } from './helpers'; -interface E2ETestOptions { - testSourceFunction?: boolean; - testSourceComment?: boolean; - testSourceMap?: boolean; - testSourceEval?: boolean; - testSourceMapUpload?: boolean; -} - -export function createE2ETest( - configBuilder: (mode: webpack.Configuration['mode']) => webpack.Configuration, - opts?: E2ETestOptions, -) { +export function createE2ETest(configBuilder: (mode: webpack.Configuration['mode']) => webpack.Configuration) { webpackModeTest((mode) => { function mockUploader() { return jest.spyOn(SourceMapUploader.prototype, 'upload').mockImplementation((_, debugId) => @@ -36,11 +16,21 @@ export function createE2ETest( ); } + function mockProcessor() { + return jest + .spyOn(SourceProcessor.prototype, 'processSourceAndSourceMapFiles') + .mockImplementation(async (_, __, debugId) => debugId ?? 'debugId'); + } + let result: webpack.Stats; let uploadSpy: ReturnType; + let processSpy: ReturnType; beforeAll(async () => { + jest.resetAllMocks(); + uploadSpy = mockUploader(); + processSpy = mockProcessor(); const config = configBuilder(mode); if (config.output?.path) { @@ -52,79 +42,40 @@ export function createE2ETest( result = webpackResult; }, 120000); - if (opts?.testSourceFunction ?? true) { - it('should inject function into emitted source files', async () => { - const outputDir = result.compilation.outputOptions.path; - assert(outputDir); - - const jsFiles = await getFiles(outputDir, /.js$/); - expect(jsFiles.length).toBeGreaterThan(0); - - for (const file of jsFiles) { - const content = await fs.promises.readFile(file, 'utf8'); - await expectSourceSnippet(content); - } - }); - } - - if (opts?.testSourceComment ?? true) { - it('should inject debug ID comment into emitted source files', async () => { - const outputDir = result.compilation.outputOptions.path; - assert(outputDir); - - const jsFiles = await getFiles(outputDir, /.js$/); - expect(jsFiles.length).toBeGreaterThan(0); - - for (const file of jsFiles) { - const content = await fs.promises.readFile(file, 'utf8'); - await expectSourceComment(content); - } - }); - } - - if (opts?.testSourceEval ?? true) { - it('should eval emitted source without syntax errors', async () => { - const outputDir = result.compilation.outputOptions.path; - assert(outputDir); - - const jsFiles = await getFiles(outputDir, /.js$/); - expect(jsFiles.length).toBeGreaterThan(0); - - for (const file of jsFiles) { - const content = await fs.promises.readFile(file, 'utf8'); - expect(() => eval(content)).not.toThrowError(SyntaxError); - } - }); - } - - if (opts?.testSourceMap ?? true) { - it('should inject debug ID into emitted sourcemap files', async () => { - const outputDir = result.compilation.outputOptions.path; - assert(outputDir); + it('should call SourceProcessor for every emitted source file and sourcemap pair', async () => { + const outputDir = result.compilation.outputOptions.path; + assert(outputDir); - const mapFiles = await getFiles(outputDir, /.js.map$/); - expect(mapFiles.length).toBeGreaterThan(0); + const jsFiles = await getFiles(outputDir, /.js$/); + expect(jsFiles.length).toBeGreaterThan(0); - for (const file of mapFiles) { - const content = await fs.promises.readFile(file, 'utf8'); - await expectSourceMapSnippet(content); - } - }); - } + const processedPairs = processSpy.mock.calls.map( + ([p1, p2]) => [path.resolve(p1), path.resolve(p2)] as const, + ); + for (const file of jsFiles) { + const content = await fs.promises.readFile(file, 'utf8'); + const matches = [...content.matchAll(/^\/\/# sourceMappingURL=(.+)$/gm)]; + expect(matches.length).toEqual(1); + const [, sourceMapPath] = matches[0]; + + expect(processedPairs).toContainEqual([ + path.resolve(file), + path.resolve(path.dirname(file), sourceMapPath), + ]); + } + }); - if (opts?.testSourceMapUpload ?? true) { - it('should upload sourcemaps using SourceMapUploader', async () => { - const outputDir = result.compilation.outputOptions.path; - assert(outputDir); + it('should call SourceMapUploader for every emitted sourcemap', async () => { + const outputDir = result.compilation.outputOptions.path; + assert(outputDir); - const mapFiles = await getFiles(outputDir, /.js.map$/); - expect(mapFiles.length).toBeGreaterThan(0); + const mapFiles = await getFiles(outputDir, /.js.map$/); + expect(mapFiles.length).toBeGreaterThan(0); - const uploadedFiles = uploadSpy.mock.calls.map((c) => path.resolve(c[0])); - for (const file of mapFiles) { - expect(uploadedFiles).toContain(path.resolve(file)); - } - }); - } + const uploadedFiles = uploadSpy.mock.calls.map((c) => path.resolve(c[0])); + for (const file of mapFiles) { + expect(uploadedFiles).toContain(path.resolve(file)); + } + }); }); } diff --git a/tools/webpack-plugin/tests/e2e/helpers.ts b/tools/webpack-plugin/tests/e2e/helpers.ts index b13ae6b3..f835a954 100644 --- a/tools/webpack-plugin/tests/e2e/helpers.ts +++ b/tools/webpack-plugin/tests/e2e/helpers.ts @@ -1,4 +1,3 @@ -import { SourceMapUploader } from '@backtrace/sourcemap-tools'; import fs from 'fs'; import path from 'path'; import webpack from 'webpack'; @@ -31,14 +30,7 @@ export function getBaseConfig(config: webpack.Configuration, options?: BaseConfi }, ], }, - plugins: [ - new BacktracePlugin( - options?.pluginOptions ?? { - debugIdGenerator: new TestDebugIdGenerator(), - sourceMapUploader: new SourceMapUploader('http://localhost'), - }, - ), - ], + plugins: [new BacktracePlugin({ uploadUrl: 'https://localhost', ...options?.pluginOptions })], ...config, }; } From cb0b8455ea23fcd0baf584dad286ade750753c70 Mon Sep 17 00:00:00 2001 From: Sebastian Alex Date: Wed, 28 Jun 2023 10:02:41 +0000 Subject: [PATCH 5/5] webpack plugin: change error message on undefined output path --- tools/webpack-plugin/src/BacktracePlugin.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/webpack-plugin/src/BacktracePlugin.ts b/tools/webpack-plugin/src/BacktracePlugin.ts index 2d7fe73a..72952102 100644 --- a/tools/webpack-plugin/src/BacktracePlugin.ts +++ b/tools/webpack-plugin/src/BacktracePlugin.ts @@ -18,7 +18,9 @@ export class BacktracePlugin implements WebpackPluginInstance { compiler.hooks.afterEmit.tapPromise(BacktracePlugin.name, async (compilation) => { const logger = compilation.getLogger(BacktracePlugin.name); if (!compilation.outputOptions.path) { - logger.error('skipping everything because outputOptions.path is not set, a bug?'); + logger.error( + 'Skipping everything because outputOptions.path is not set. If you see this error, please report this to Backtrace.', + ); return; }