Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 2 additions & 1 deletion packages/bundler-plugin-core/src/build-plugin-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,8 @@ export function createSentryBuildPluginManager(
tmpUploadFolder,
chunkIndex,
logger,
options.sourcemaps?.rewriteSources ?? defaultRewriteSourcesHook
options.sourcemaps?.rewriteSources ?? defaultRewriteSourcesHook,
options.sourcemaps?.resolveSourceMap
);
}
);
Expand Down
86 changes: 43 additions & 43 deletions packages/bundler-plugin-core/src/debug-id-upload.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import fs from "fs";
import path from "path";
import * as url from "url"
import * as util from "util";
import { promisify } from "util";
import { SentryBuildPluginManager } from "./build-plugin-manager";
import { Logger } from "./logger";

interface RewriteSourcesHook {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(source: string, map: any): string;
}
import { ResolveSourceMapHook, RewriteSourcesHook } from "./types";

interface DebugIdUploadPluginOptions {
sentryBuildPluginManager: SentryBuildPluginManager;
Expand All @@ -27,7 +24,8 @@ export async function prepareBundleForDebugIdUpload(
uploadFolder: string,
chunkIndex: number,
logger: Logger,
rewriteSourcesHook: RewriteSourcesHook
rewriteSourcesHook: RewriteSourcesHook,
resolveSourceMapHook: ResolveSourceMapHook | undefined
) {
let bundleContent;
try {
Expand Down Expand Up @@ -60,7 +58,8 @@ export async function prepareBundleForDebugIdUpload(
const writeSourceMapFilePromise = determineSourceMapPathFromBundle(
bundleFilePath,
bundleContent,
logger
logger,
resolveSourceMapHook
).then(async (sourceMapPath) => {
if (sourceMapPath) {
await prepareSourceMapForDebugIdUpload(
Expand Down Expand Up @@ -114,61 +113,62 @@ function addDebugIdToBundleSource(bundleSource: string, debugId: string): string
*
* @returns the path to the bundle's source map or `undefined` if none could be found.
*/
async function determineSourceMapPathFromBundle(
export async function determineSourceMapPathFromBundle(
bundlePath: string,
bundleSource: string,
logger: Logger
logger: Logger,
resolveSourceMapHook: ResolveSourceMapHook | undefined
): Promise<string | undefined> {
// 1. try to find source map at `sourceMappingURL` location
const sourceMappingUrlMatch = bundleSource.match(/^\s*\/\/# sourceMappingURL=(.*)$/m);
if (sourceMappingUrlMatch) {
const sourceMappingUrl = path.normalize(sourceMappingUrlMatch[1] as string);
const sourceMappingUrl = sourceMappingUrlMatch ? sourceMappingUrlMatch[1] as string : undefined;

let isUrl;
let isSupportedUrl;
const searchLocations: string[] = [];

if (resolveSourceMapHook) {
const customPath = await resolveSourceMapHook(bundlePath, sourceMappingUrl);
if (customPath) {
searchLocations.push(customPath);
}
}

// 1. try to find source map at `sourceMappingURL` location
if (sourceMappingUrl) {
let parsedUrl: URL | undefined;
try {
const url = new URL(sourceMappingUrl);
isUrl = true;
isSupportedUrl = url.protocol === "file:";
parsedUrl = new URL(sourceMappingUrl);
} catch {
isUrl = false;
isSupportedUrl = false;
// noop
}

let absoluteSourceMapPath;
if (isSupportedUrl) {
absoluteSourceMapPath = sourceMappingUrl;
} else if (isUrl) {
// noop
if (parsedUrl && parsedUrl.protocol === "file:") {
searchLocations.push(url.fileURLToPath(sourceMappingUrl));
} else if (parsedUrl) {
// noop, non-file urls don't translate to a local sourcemap file
} else if (path.isAbsolute(sourceMappingUrl)) {
absoluteSourceMapPath = sourceMappingUrl;
searchLocations.push(path.normalize(sourceMappingUrl))
} else {
absoluteSourceMapPath = path.join(path.dirname(bundlePath), sourceMappingUrl);
}

if (absoluteSourceMapPath) {
try {
// Check if the file actually exists
await util.promisify(fs.access)(absoluteSourceMapPath);
return absoluteSourceMapPath;
} catch (e) {
// noop
}
searchLocations.push(path.normalize(path.join(path.dirname(bundlePath), sourceMappingUrl)));
}
}

// 2. try to find source map at path adjacent to chunk source, but with `.map` appended
try {
const adjacentSourceMapFilePath = bundlePath + ".map";
await util.promisify(fs.access)(adjacentSourceMapFilePath);
return adjacentSourceMapFilePath;
} catch (e) {
// noop
searchLocations.push(bundlePath + ".map")

for (const searchLocation of searchLocations) {
try {
await util.promisify(fs.access)(searchLocation);
logger.debug(`Source map found for bundle \`${bundlePath}\`: \`${searchLocation}\``)
return searchLocation;
} catch (e) {
// noop
}
}

// This is just a debug message because it can be quite spammy for some frameworks
logger.debug(
`Could not determine source map path for bundle: ${bundlePath} - Did you turn on source map generation in your bundler?`
`Could not determine source map path for bundle: \`${bundlePath}\`` +
` - Did you turn on source map generation in your bundler?` +
` (Attempted paths: ${searchLocations.map(e => `\`${e}\``).join(", ")})`
);
return undefined;
}
Expand Down
18 changes: 16 additions & 2 deletions packages/bundler-plugin-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,17 @@ export interface Options {
*
* Defaults to making all sources relative to `process.cwd()` while building.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
rewriteSources?: (source: string, map: any) => string;
rewriteSources?: RewriteSourcesHook;

/**
* Hook to customize source map file resolution.
*
* The hook is called with the absolute path of the build artifact and the value of the `//# sourceMappingURL=`
* comment, if present. The hook should then return an absolute path indicating where to find the artifact's
* corresponding `.map` file. If no path is returned or the returned path doesn't exist, the standard source map
* resolution process will be used.
*/
resolveSourceMap?: ResolveSourceMapHook;

/**
* A glob or an array of globs that specifies the build artifacts that should be deleted after the artifact upload to Sentry has been completed.
Expand Down Expand Up @@ -356,6 +365,11 @@ export interface Options {
};
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type RewriteSourcesHook = (source: string, map: any) => string

export type ResolveSourceMapHook = (artifactPath: string, sourceMappingUrl: string | undefined) => string | undefined | Promise<string | undefined>

export interface ModuleMetadata {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
"use strict";
console.log("wow!");

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
"use strict";
console.log("wow!");

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

164 changes: 164 additions & 0 deletions packages/bundler-plugin-core/test/sentry/resolve-source-maps.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import * as path from "path";
import * as fs from "fs";
import * as url from "url";
import { determineSourceMapPathFromBundle } from "../../src/debug-id-upload";
import { createLogger } from "../../src/logger";

const logger = createLogger({ prefix: "[resolve-source-maps-test]", silent: false, debug: false });
const fixtureDir = path.resolve(__dirname, "../fixtures/resolve-source-maps");

const adjacentBundlePath = path.join(fixtureDir, "adjacent-sourcemap/index.js");
const adjacentSourceMapPath = path.join(fixtureDir, "adjacent-sourcemap/index.js.map");
const adjacentBundleContent = fs.readFileSync(adjacentBundlePath, "utf-8");

const separateBundlePath = path.join(fixtureDir, "separate-directory/bundles/index.js");
const separateSourceMapPath = path.join(fixtureDir, "separate-directory/sourcemaps/index.js.map");
const separateBundleContent = fs.readFileSync(separateBundlePath, "utf-8");

const sourceMapUrl = "https://sourcemaps.example.com/foo/index.js.map"

function srcMappingUrl(url: string): string {
return `\n//# sourceMappingURL=${url}`
}

describe("Resolve source maps", () => {
it("should resolve source maps next to bundles", async () => {
expect(
await determineSourceMapPathFromBundle(
adjacentBundlePath,
adjacentBundleContent,
logger,
undefined
)
).toEqual(adjacentSourceMapPath);
});

it("shouldn't resolve source maps in separate directories", async () => {
expect(
await determineSourceMapPathFromBundle(
separateBundlePath,
separateBundleContent,
logger,
undefined
)
).toBeUndefined();
});

describe("sourceMappingURL resolution", () => {
it("should resolve source maps when sourceMappingURL is a file URL", async () => {
expect(
await determineSourceMapPathFromBundle(
separateBundlePath,
separateBundleContent + srcMappingUrl(url.pathToFileURL(separateSourceMapPath).href),
logger,
undefined
)
).toEqual(separateSourceMapPath);
});

it("shouldn't resolve source maps when sourceMappingURL is a non-file URL", async () => {
expect(
await determineSourceMapPathFromBundle(
separateBundlePath,
separateBundleContent + srcMappingUrl(sourceMapUrl),
logger,
undefined
)
).toBeUndefined();
});

it("should resolve source maps when sourceMappingURL is an absolute path", async () => {
expect(
await determineSourceMapPathFromBundle(
separateBundlePath,
separateBundleContent + srcMappingUrl(separateSourceMapPath),
logger,
undefined
)
).toEqual(separateSourceMapPath);
});

it("should resolve source maps when sourceMappingURL is a relative path", async () => {
expect(
await determineSourceMapPathFromBundle(
separateBundlePath,
separateBundleContent + srcMappingUrl(path.relative(path.dirname(separateBundlePath), separateSourceMapPath)),
logger,
undefined
)
).toEqual(separateSourceMapPath);
});
});

describe("resolveSourceMap hook", () => {
it("should resolve source maps when a resolveSourceMap hook is provided", async () => {
expect(
await determineSourceMapPathFromBundle(
separateBundlePath,
separateBundleContent + srcMappingUrl(sourceMapUrl),
logger,
() => separateSourceMapPath
)
).toEqual(separateSourceMapPath);
});

it("should pass the correct values to the resolveSourceMap hook", async () => {
const hook = jest.fn(() => separateSourceMapPath)
expect(
await determineSourceMapPathFromBundle(
separateBundlePath,
separateBundleContent + srcMappingUrl(sourceMapUrl),
logger,
hook
)
).toEqual(separateSourceMapPath);
expect(hook.mock.calls[0]).toEqual([separateBundlePath, sourceMapUrl])
});

it("should pass the correct values to the resolveSourceMap hook when no sourceMappingURL is present", async () => {
const hook = jest.fn(() => separateSourceMapPath)
expect(
await determineSourceMapPathFromBundle(
separateBundlePath,
separateBundleContent,
logger,
hook
)
).toEqual(separateSourceMapPath);
expect(hook.mock.calls[0]).toEqual([separateBundlePath, undefined])
});

it("should prefer resolveSourceMap result over heuristic results", async () => {
expect(
await determineSourceMapPathFromBundle(
adjacentBundlePath,
adjacentBundleContent,
logger,
() => separateSourceMapPath
)
).toEqual(separateSourceMapPath);
});

it("should fall back when the resolveSourceMap hook returns undefined", async () => {
expect(
await determineSourceMapPathFromBundle(
adjacentBundlePath,
adjacentBundleContent,
logger,
() => undefined
)
).toEqual(adjacentSourceMapPath);
});

it("should fall back when the resolveSourceMap hook returns a non-existent path", async () => {
expect(
await determineSourceMapPathFromBundle(
adjacentBundlePath,
adjacentBundleContent,
logger,
() => path.join(fixtureDir, "non-existent.js.map")
)
).toEqual(adjacentSourceMapPath);
});
});
});
Loading
Loading