From 1912a7e4d99c0825297131dee886e3502aa84c30 Mon Sep 17 00:00:00 2001 From: Sebastian Alex Date: Wed, 21 Jun 2023 09:49:41 +0000 Subject: [PATCH 1/2] webpack-plugin: add test for uploading sourcemaps --- .../webpack-plugin/tests/e2e/createE2ETest.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tools/webpack-plugin/tests/e2e/createE2ETest.ts b/tools/webpack-plugin/tests/e2e/createE2ETest.ts index 698ec401..39ccb42c 100644 --- a/tools/webpack-plugin/tests/e2e/createE2ETest.ts +++ b/tools/webpack-plugin/tests/e2e/createE2ETest.ts @@ -1,5 +1,8 @@ +import { SourceMapUploader } from '@backtrace/sourcemap-tools'; import assert from 'assert'; +import crypto from 'crypto'; import fs from 'fs'; +import path from 'path'; import webpack from 'webpack'; import { asyncWebpack, @@ -17,6 +20,7 @@ interface E2ETestOptions { testSourceComment?: boolean; testSourceMap?: boolean; testSourceEval?: boolean; + testSourceMapUpload?: boolean; } export function createE2ETest( @@ -24,9 +28,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); @@ -95,6 +111,21 @@ export function createE2ETest( await expectSourceMapSnippet(content); } }); + + 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)); + } + }); + } } }); } From e710e94569e64f16e261b570aeb4789b1fdf08ba Mon Sep 17 00:00:00 2001 From: Sebastian Alex Date: Wed, 21 Jun 2023 09:50:05 +0000 Subject: [PATCH 2/2] webpack-plugin: add uploading sourcemaps --- tools/webpack-plugin/src/BacktracePluginV4.ts | 37 ++++++++++- tools/webpack-plugin/src/BacktracePluginV5.ts | 65 +++++++++++++++++-- .../src/models/BacktracePluginOptions.ts | 4 +- tools/webpack-plugin/tests/e2e/helpers.ts | 10 ++- 4 files changed, 106 insertions(+), 10 deletions(-) diff --git a/tools/webpack-plugin/src/BacktracePluginV4.ts b/tools/webpack-plugin/src/BacktracePluginV4.ts index 04ecf814..e541262c 100644 --- a/tools/webpack-plugin/src/BacktracePluginV4.ts +++ b/tools/webpack-plugin/src/BacktracePluginV4.ts @@ -1,17 +1,22 @@ -import { ContentAppender, DebugIdGenerator } from '@backtrace/sourcemap-tools'; +import { ContentAppender, DebugIdGenerator, SourceMapUploader } from '@backtrace/sourcemap-tools'; import crypto from 'crypto'; +import path from 'path'; import { Compiler, WebpackPluginInstance } from 'webpack'; import { BacktraceWebpackSourceGenerator } from './BacktraceWebpackSourceGenerator'; import { BacktracePluginOptions } from './models/BacktracePluginOptions'; export class BacktracePluginV4 implements WebpackPluginInstance { private readonly _sourceGenerator: BacktraceWebpackSourceGenerator; + private readonly _sourceMapUploader?: SourceMapUploader; constructor(public readonly options?: BacktracePluginOptions) { this._sourceGenerator = new BacktraceWebpackSourceGenerator( options?.debugIdGenerator ?? new DebugIdGenerator(), new ContentAppender(), ); + + this._sourceMapUploader = + options?.sourceMapUploader ?? (options?.uploadUrl ? new SourceMapUploader(options.uploadUrl) : undefined); } public apply(compiler: Compiler) { @@ -42,5 +47,35 @@ export class BacktracePluginV4 implements WebpackPluginInstance { 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.'); + } + + for (const key in compilation.assets) { + if (!key.match(/\.(c|m)?jsx?\.map$/)) { + continue; + } + + const sourceKey = key.replace(/.map$/, ''); + const debugId = assetDebugIds.get(sourceKey); + if (!debugId) { + continue; + } + + const sourceMapAsset = compilation.getAsset(key); + if (!sourceMapAsset) { + continue; + } + + const sourceMapPath = path.join(outputPath, sourceMapAsset.name); + await uploader.upload(sourceMapPath, debugId); + } + }); + } } } diff --git a/tools/webpack-plugin/src/BacktracePluginV5.ts b/tools/webpack-plugin/src/BacktracePluginV5.ts index 5ae43047..e130a457 100644 --- a/tools/webpack-plugin/src/BacktracePluginV5.ts +++ b/tools/webpack-plugin/src/BacktracePluginV5.ts @@ -1,18 +1,23 @@ -import { ContentAppender, DebugIdGenerator } from '@backtrace/sourcemap-tools'; +import { ContentAppender, DebugIdGenerator, SourceMapUploader } from '@backtrace/sourcemap-tools'; import crypto from 'crypto'; -import { Compilation, Compiler, WebpackPluginInstance } from 'webpack'; +import path from 'path'; +import { AssetInfo, 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; + private readonly _sourceMapUploader?: SourceMapUploader; constructor(public readonly options?: BacktracePluginOptions) { this._sourceGenerator = new BacktraceWebpackSourceGenerator( options?.debugIdGenerator ?? new DebugIdGenerator(), new ContentAppender(), ); + + this._sourceMapUploader = + options?.sourceMapUploader ?? (options?.uploadUrl ? new SourceMapUploader(options.uploadUrl) : undefined); } public apply(compiler: Compiler) { @@ -87,6 +92,43 @@ 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.'); + } + + for (const key in compilation.assets) { + const asset = compilation.getAsset(key); + if (!asset) { + continue; + } + + const debugId = assetDebugIds.get(key); + if (!debugId) { + continue; + } + + const sourceMapKeys = this.getSourceMapKeys(asset.info); + if (!sourceMapKeys) { + 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); + } + } + }); + } } private injectSourceSnippet(compilation: Compilation, key: string, debugId: string): boolean { @@ -143,15 +185,11 @@ export class BacktracePluginV5 implements WebpackPluginInstance { return false; } - let sourceMapKeys = assetInfo.related?.sourceMap; + const sourceMapKeys = this.getSourceMapKeys(assetInfo); if (!sourceMapKeys) { return false; } - if (!Array.isArray(sourceMapKeys)) { - sourceMapKeys = [sourceMapKeys]; - } - for (const sourceMapKey of sourceMapKeys) { if (processedSourceMaps.has(sourceMapKey)) { continue; @@ -170,4 +208,17 @@ export class BacktracePluginV5 implements WebpackPluginInstance { return true; } + + private getSourceMapKeys(assetInfo: AssetInfo) { + const sourceMapKeys = assetInfo.related?.sourceMap; + if (!sourceMapKeys) { + return undefined; + } + + if (!Array.isArray(sourceMapKeys)) { + return [sourceMapKeys]; + } + + return sourceMapKeys; + } } 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/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, }; }