Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => {
"sentry-cli-mock.json": "["release","create","CURRENT_SHA","--project","fake-project"],
["release","set-commits","CURRENT_SHA","--auto"],
["release","finalize","CURRENT_SHA"],
["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"],
",
}
`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => {
"sentry-cli-mock.json": "["release","create","CURRENT_SHA","--project","fake-project"],
["release","set-commits","CURRENT_SHA","--auto"],
["release","finalize","CURRENT_SHA"],
["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"],
",
}
`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => {
",
"sentry-cli-mock.json": "["release","set-commits","CURRENT_SHA","--auto"],
["release","finalize","CURRENT_SHA"],
["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"],
",
}
`);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as esbuild from "esbuild";
import { sentryEsbuildPlugin } from "@sentry/bundler-plugins/esbuild";
import { sentryConfig } from "../configs/sourcemaps-assets-auto.config.js";

await esbuild.build({
entryPoints: ["./src/entry1.js", "./src/entry2.js"],
bundle: true,
outdir: "./out/sourcemaps-assets-auto",
minify: false,
format: "esm",
splitting: true,
chunkNames: "[name]",
plugins: [sentryEsbuildPlugin(sentryConfig)],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { expect } from "vitest";
import { test } from "./utils";

test(import.meta.url, ({ runBundler, readOutputFiles }) => {
runBundler({ SENTRY_TEST_RECORD_UPLOADED_SOURCES: "1" });
const files = readOutputFiles();

expect(Object.keys(files).sort()).toMatchInlineSnapshot(`
[
"chunk.js",
"chunk.js.map",
"entry1.js",
"entry2.js",
"entry2.js.map",
"sentry-cli-mock.json",
"sentry-uploaded-sources.json",
]
`);
expect(JSON.parse(files["sentry-uploaded-sources.json"]!)).toMatchInlineSnapshot(`
[
[
"sentry-debug-id-stub:_sentry-debug-id-injection-stub?sentry-module-id=00000000-0000-0000-0000-000000000000",
"../../src/entry1.js",
"../../src/entry1.js",
],
]
`);
expect(files["sentry-cli-mock.json"]).toMatchInlineSnapshot(`
"["release","create","CURRENT_SHA","--project","fake-project"],
["release","set-commits","CURRENT_SHA","--auto"],
["release","finalize","CURRENT_SHA"],
["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"],
"
`);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as esbuild from "esbuild";
import { sentryEsbuildPlugin } from "@sentry/bundler-plugins/esbuild";
import { sentryConfig } from "../configs/sourcemaps-ignore-auto.config.js";

await esbuild.build({
entryPoints: ["./src/entry1.js", "./src/entry2.js"],
bundle: true,
outdir: "./out/sourcemaps-ignore-auto",
minify: false,
format: "esm",
splitting: true,
chunkNames: "[name]",
plugins: [sentryEsbuildPlugin(sentryConfig)],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { expect } from "vitest";
import { test } from "./utils";

test(import.meta.url, ({ runBundler, readOutputFiles }) => {
runBundler({ SENTRY_TEST_RECORD_UPLOADED_SOURCES: "1" });
const files = readOutputFiles();

expect(Object.keys(files).sort()).toMatchInlineSnapshot(`
[
"chunk.js",
"chunk.js.map",
"entry1.js",
"entry2.js",
"entry2.js.map",
"sentry-cli-mock.json",
"sentry-uploaded-sources.json",
]
`);
expect(JSON.parse(files["sentry-uploaded-sources.json"]!)).toMatchInlineSnapshot(`
[
[
"sentry-debug-id-stub:_sentry-debug-id-injection-stub?sentry-module-id=00000000-0000-0000-0000-000000000000",
"../../src/entry1.js",
"../../src/entry1.js",
],
]
`);
expect(files["sentry-cli-mock.json"]).toMatchInlineSnapshot(`
"["release","create","CURRENT_SHA","--project","fake-project"],
["release","set-commits","CURRENT_SHA","--auto"],
["release","finalize","CURRENT_SHA"],
["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"],
"
`);
});
5 changes: 5 additions & 0 deletions packages/bundler-plugins/src/core/debug-id-upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,11 @@ function hasSpecCompliantDebugId(bundleSource: string): boolean {
return SPEC_LAST_DEBUG_ID_REGEX.test(bundleSource);
}

/** Whether the bundle carries a debug ID, injected by the plugin or as a `//# debugId=` comment. */
export function hasDebugId(bundleSource: string): boolean {
return determineDebugIdFromBundleSource(bundleSource) !== undefined || hasSpecCompliantDebugId(bundleSource);
}

function addDebugIdToBundleSource(bundleSource: string, debugId: string): string {
if (hasSpecCompliantDebugId(bundleSource)) {
return bundleSource.replace(SPEC_LAST_DEBUG_ID_REGEX, `//# debugId=${debugId}`);
Expand Down
2 changes: 1 addition & 1 deletion packages/bundler-plugins/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export {
} from './utils';
export { createSentryBuildPluginManager } from './build-plugin-manager';
export type { SentryBuildPluginManager, UploadStaging } from './build-plugin-manager';
export { createDebugIdUploadFunction, addDebugIdToEmittedArtifacts, stampDebugId } from './debug-id-upload';
export { createDebugIdUploadFunction, addDebugIdToEmittedArtifacts, hasDebugId, stampDebugId } from './debug-id-upload';
export { decideSourceMapHandling } from './sourcemap-settings';
export type { SourceMapDecision, SourceMapKind } from './sourcemap-settings';
export type { UploadFilter } from './upload-filter';
83 changes: 72 additions & 11 deletions packages/bundler-plugins/src/esbuild/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Options } from '../core';
import type { Options, SourceMapKind, UploadFilter } from '../core';
import {
createSentryBuildPluginManager,
generateReleaseInjectorCode,
Expand All @@ -7,8 +7,10 @@ import {
createDebugIdUploadFunction,
CodeInjection,
addDebugIdToEmittedArtifacts,
hasDebugId,
isJsFile,
} from '../core';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { createRequire } from 'node:module';
import { randomUUID } from 'node:crypto';
Expand Down Expand Up @@ -48,13 +50,33 @@ interface EsbuildOnEndArgs {
};
}

type EsbuildSourceMapSetting = boolean | 'inline' | 'external' | 'linked' | 'both' | undefined;

interface EsbuildInitialOptions {
bundle?: boolean;
inject?: string[];
metafile?: boolean;
define?: Record<string, string>;
write?: boolean;
absWorkingDir?: string;
sourcemap?: EsbuildSourceMapSetting;
}

/** Maps esbuild's `sourcemap` setting onto the plugin's source map kinds. */
function sourceMapKindFromEsbuildSetting(setting: EsbuildSourceMapSetting): SourceMapKind {
switch (setting) {
case undefined:
return 'unset';
case false:
return 'off';
case 'external':
return 'hidden';
case 'inline':
case 'both':
return 'inline';
default:
return 'visible';
}
}

interface EsbuildPluginBuild {
Expand Down Expand Up @@ -280,6 +302,13 @@ export function sentryEsbuildPlugin(userOptions: Options = {}): any {
});
}

const sourceMapDecision = sentryBuildPluginManager.sourceMaps.decide(
sourceMapKindFromEsbuildSetting(initialOptions.sourcemap),
);
if (sourceMapDecision.enableHidden) {
initialOptions.sourcemap = 'external';
}

// Create release and optionally upload
const freeGlobalDependencyOnBuildArtifacts = createDependencyOnBuildArtifacts();
const upload = createDebugIdUploadFunction({ sentryBuildPluginManager });
Expand All @@ -290,27 +319,38 @@ export function sentryEsbuildPlugin(userOptions: Options = {}): any {
await sentryBuildPluginManager.createRelease();

if (sourcemapsEnabled) {
const buildArtifacts = result.metafile ? Object.keys(result.metafile.outputs) : [];
// The metafile lists outputs relative to `absWorkingDir`, not to the process.
const outputDir = initialOptions.absWorkingDir ?? process.cwd();
const buildArtifacts = result.metafile
? Object.keys(result.metafile.outputs).map(output => path.resolve(outputDir, output))
: [];

if (options.sourcemaps?.disable !== 'disable-upload') {
await upload(buildArtifacts);
// The upload globs the paths, and glob reads backslashes as escapes.
const { uploaded } = await upload(buildArtifacts.map(artifact => artifact.split(path.sep).join('/')));
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved

// esbuild writes before `onEnd` runs, so the maps are deleted from disk
// afterwards instead of being dropped from the output like in other bundlers.
if (sourceMapDecision.deleteAfterUpload && initialOptions.write !== false) {
const { shouldUpload } = sentryBuildPluginManager.sourceMaps;
const sourceMaps = await getUploadedSourceMaps(buildArtifacts, shouldUpload);

if (uploaded) {
await Promise.all(sourceMaps.map(sourceMap => fs.promises.rm(sourceMap, { force: true })));
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
} else if (sourceMaps.length > 0) {
logger.error('The source maps were not uploaded, so they stay in the build output.');
}
}
} else if (initialOptions.write === false) {
logger.debug('Build output is not written to disk. Skipping debug ID injection into build artifacts.');
} else {
// The upload routine (which stamps debug IDs into temp copies of the artifacts) is
// skipped with `disable-upload`. esbuild has no hook to modify outputs before they are
// written, so the emitted artifacts get stamped on disk instead.
const outputDir = initialOptions.absWorkingDir ?? process.cwd();
await Promise.all(
buildArtifacts
.filter(isJsFile)
.map(bundle =>
addDebugIdToEmittedArtifacts(
path.resolve(outputDir, bundle),
logger,
options.sourcemaps?.resolveSourceMap,
),
),
.map(bundle => addDebugIdToEmittedArtifacts(bundle, logger, options.sourcemaps?.resolveSourceMap)),
);
}
}
Expand All @@ -326,3 +366,24 @@ export function sentryEsbuildPlugin(userOptions: Options = {}): any {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default sentryEsbuildPlugin;
export type { Options as SentryEsbuildPluginOptions } from '../core';

/**
* The maps of the chunks the upload took: those that pass `sourcemaps.assets` and `ignore` and
* carry a debug ID. Shared chunks and CSS get none, so their maps stay in the build output.
*/
async function getUploadedSourceMaps(buildArtifacts: string[], shouldUpload: UploadFilter): Promise<string[]> {
const sourceMaps = new Set(buildArtifacts.filter(artifact => artifact.endsWith('.map')));
const uploaded: string[] = [];

for (const artifact of buildArtifacts) {
if (!isJsFile(artifact) || !sourceMaps.has(`${artifact}.map`) || !shouldUpload(artifact)) {
continue;
}
const code = await fs.promises.readFile(artifact, 'utf8').catch(() => '');
if (hasDebugId(code)) {
uploaded.push(`${artifact}.map`);
}
}

return uploaded;
}
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
Loading
Loading