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
14 changes: 3 additions & 11 deletions packages/bundler-plugin-core/src/build-plugin-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ import {
serializeIgnoreOptions,
stripQueryAndHashFromPath,
} from "./utils";
import { glob } from "glob";
import { defaultRewriteSourcesHook, prepareBundleForDebugIdUpload } from "./debug-id-upload";
import { globFiles } from "./glob";
import { LIB_VERSION } from "./version";

// Module-level guard to prevent duplicate deploy records when multiple bundler plugin
Expand Down Expand Up @@ -678,12 +678,7 @@ export function createSentryBuildPluginManager(

const globResult = await startSpan(
{ name: "glob", scope: sentryScope },
async () =>
await glob(globAssets, {
absolute: true,
nodir: true, // We need individual files for preparation
ignore: options.sourcemaps?.ignore,
})
async () => await globFiles(globAssets, { ignore: options.sourcemaps?.ignore })
);

const debugIdChunkFilePaths = globResult.filter((debugIdChunkFilePath) => {
Expand Down Expand Up @@ -810,10 +805,7 @@ export function createSentryBuildPluginManager(
try {
const filesToDelete = await options.sourcemaps?.filesToDeleteAfterUpload;
if (filesToDelete !== undefined) {
const filePathsToDelete = await glob(filesToDelete, {
absolute: true,
nodir: true,
});
const filePathsToDelete = await globFiles(filesToDelete);

logger.debug(
"Waiting for dependencies on generated files to be freed before deleting..."
Expand Down
8 changes: 8 additions & 0 deletions packages/bundler-plugin-core/src/glob.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { glob } from "glob";

export function globFiles(
patterns: string | string[],
options?: { root?: string; ignore?: string | string[] }
): Promise<string[]> {
return glob(patterns, { absolute: true, nodir: true, ...options });
}
14 changes: 1 addition & 13 deletions packages/bundler-plugin-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import componentNameAnnotatePlugin, {
import SentryCli from "@sentry/cli";
import { logger } from "@sentry/utils";
import * as fs from "fs";
import { glob } from "glob";
import { CodeInjection, containsOnlyImports, stripQueryAndHashFromPath } from "./utils";

/**
Expand Down Expand Up @@ -63,18 +62,7 @@ export function shouldSkipCodeInjection(
return false;
}

export function globFiles(outputDir: string): Promise<string[]> {
return glob(
["/**/*.js", "/**/*.mjs", "/**/*.cjs", "/**/*.js.map", "/**/*.mjs.map", "/**/*.cjs.map"].map(
(q) => `${q}?(\\?*)?(#*)`
), // We want to allow query and hashes strings at the end of files
{
root: outputDir,
absolute: true,
nodir: true,
}
);
}
export { globFiles } from "./glob";

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function createComponentNameAnnotateHooks(
Expand Down
22 changes: 13 additions & 9 deletions packages/bundler-plugin-core/test/build-plugin-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
_resetDeployedReleasesForTesting,
} from "../src/build-plugin-manager";
import fs from "fs";
import { glob } from "glob";
import { globFiles } from "../src/glob";
import { prepareBundleForDebugIdUpload } from "../src/debug-id-upload";

const mockCliExecute = jest.fn();
Expand Down Expand Up @@ -37,10 +37,10 @@ jest.mock("@sentry/core", () => ({
startSpan: jest.fn((options: unknown, callback: () => unknown) => callback()),
}));

jest.mock("glob");
jest.mock("../src/glob");
jest.mock("../src/debug-id-upload");

const mockGlob = glob as jest.MockedFunction<typeof glob>;
const mockGlobFiles = globFiles as jest.MockedFunction<typeof globFiles>;
const mockPrepareBundleForDebugIdUpload = prepareBundleForDebugIdUpload as jest.MockedFunction<
typeof prepareBundleForDebugIdUpload
>;
Expand Down Expand Up @@ -302,7 +302,7 @@ describe("createSentryBuildPluginManager", () => {
})
);
// Should not glob when prepareArtifacts is false
expect(mockGlob).not.toHaveBeenCalled();
expect(mockGlobFiles).not.toHaveBeenCalled();
expect(mockPrepareBundleForDebugIdUpload).not.toHaveBeenCalled();
});

Expand Down Expand Up @@ -340,7 +340,7 @@ describe("createSentryBuildPluginManager", () => {
live: "rejectOnError",
})
);
expect(mockGlob).not.toHaveBeenCalled();
expect(mockGlobFiles).not.toHaveBeenCalled();
expect(mockPrepareBundleForDebugIdUpload).not.toHaveBeenCalled();
});

Expand All @@ -359,7 +359,7 @@ describe("createSentryBuildPluginManager", () => {
await manager.uploadSourcemaps([".next"], { prepareArtifacts: false });

expect(mockCliUploadSourceMaps).not.toHaveBeenCalled();
expect(mockGlob).not.toHaveBeenCalled();
expect(mockGlobFiles).not.toHaveBeenCalled();
expect(mockPrepareBundleForDebugIdUpload).not.toHaveBeenCalled();
});

Expand All @@ -378,14 +378,18 @@ describe("createSentryBuildPluginManager", () => {
await manager.uploadSourcemaps([".next"]);

expect(mockCliUploadSourceMaps).not.toHaveBeenCalled();
expect(mockGlob).not.toHaveBeenCalled();
expect(mockGlobFiles).not.toHaveBeenCalled();
expect(mockPrepareBundleForDebugIdUpload).not.toHaveBeenCalled();
});

it("prepares into temp folder and uploads when prepareArtifacts is true (default)", async () => {
mockCliUploadSourceMaps.mockResolvedValue(undefined);

mockGlob.mockResolvedValue(["/app/dist/a.js", "/app/dist/a.js.map", "/app/dist/other.txt"]);
mockGlobFiles.mockResolvedValue([
"/app/dist/a.js",
"/app/dist/a.js.map",
"/app/dist/other.txt",
]);

jest.spyOn(fs.promises, "mkdtemp").mockResolvedValue("/tmp/sentry-upload-xyz");
jest.spyOn(fs.promises, "readdir").mockResolvedValue(["a.js", "a.js.map"] as never);
Expand Down Expand Up @@ -472,7 +476,7 @@ describe("createSentryBuildPluginManager", () => {
describe("uploadSourcemaps with multiple projects", () => {
beforeEach(() => {
jest.clearAllMocks();
mockGlob.mockResolvedValue(["/path/to/bundle.js"]);
mockGlobFiles.mockResolvedValue(["/path/to/bundle.js"]);
mockPrepareBundleForDebugIdUpload.mockResolvedValue(undefined);
mockCliUploadSourceMaps.mockResolvedValue(undefined);

Expand Down
161 changes: 161 additions & 0 deletions packages/bundler-plugin-core/test/glob.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
import { globFiles } from "../src/glob";

let tmpDir: string;

beforeEach(async () => {
tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "glob-test-"));
});

afterEach(async () => {
await fs.promises.rm(tmpDir, { recursive: true, force: true });
});

/** Helper: create a file (and any parent dirs) under tmpDir. */
async function touch(...segments: string[]): Promise<string> {
const filePath = path.join(tmpDir, ...segments);
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
await fs.promises.writeFile(filePath, "");
return filePath;
}

describe("globFiles", () => {
describe("core behavior", () => {
it("returns absolute paths", async () => {
await touch("a.js");
const result = await globFiles(path.join(tmpDir, "**/*.js"));
expect(result).toHaveLength(1);
expect(path.isAbsolute(result[0]!)).toBe(true);
});

it("excludes directories (nodir)", async () => {
// Create a directory that matches the glob pattern
await fs.promises.mkdir(path.join(tmpDir, "subdir.js"), { recursive: true });
await touch("real.js");
const result = await globFiles(path.join(tmpDir, "**/*.js"));
expect(result).toEqual([path.join(tmpDir, "real.js")]);
});

it("returns [] for no matches", async () => {
await touch("a.txt");
const result = await globFiles(path.join(tmpDir, "**/*.js"));
expect(result).toEqual([]);
});

it("returns [] for nonexistent pattern path", async () => {
const result = await globFiles(path.join(tmpDir, "nonexistent/**/*.js"));
expect(result).toEqual([]);
});

it("matches deeply nested files", async () => {
const filePath = await touch("a", "b", "c", "deep.js");
const result = await globFiles(path.join(tmpDir, "**/*.js"));
expect(result).toEqual([filePath]);
});

it("works with a string pattern", async () => {
await touch("single.js");
const result = await globFiles(path.join(tmpDir, "*.js"));
expect(result).toHaveLength(1);
});

it("works with an array of patterns", async () => {
const jsFile = await touch("a.js");
const mapFile = await touch("a.js.map");
await touch("b.css");

const result = await globFiles([
path.join(tmpDir, "**/*.js"),
path.join(tmpDir, "**/*.js.map"),
]);
result.sort();
expect(result).toEqual([jsFile, mapFile].sort());
});
});

describe("root option", () => {
it("scopes results to root directory", async () => {
await touch("file.js");
// Patterns starting with / are resolved relative to root
const result = await globFiles("/**/*.js", { root: tmpDir });
expect(result).toEqual([path.join(tmpDir, "file.js")]);
});
});

describe("ignore option", () => {
it("excludes files matching ignore string pattern", async () => {
await touch("keep.js");
await touch("node_modules", "dep.js");

const result = await globFiles(path.join(tmpDir, "**/*.js"), {
ignore: path.join(tmpDir, "node_modules/**"),
});
expect(result).toEqual([path.join(tmpDir, "keep.js")]);
});

it("excludes files matching ignore array patterns", async () => {
await touch("keep.js");
await touch("node_modules", "dep.js");
await touch("dist", "bundle.js");

const result = await globFiles(path.join(tmpDir, "**/*.js"), {
ignore: [path.join(tmpDir, "node_modules/**"), path.join(tmpDir, "dist/**")],
});
expect(result).toEqual([path.join(tmpDir, "keep.js")]);
});
});

describe("rollup JS/map patterns", () => {
const JS_AND_MAP_PATTERNS = [
"/**/*.js",
"/**/*.mjs",
"/**/*.cjs",
"/**/*.js.map",
"/**/*.mjs.map",
"/**/*.cjs.map",
].map((q) => `${q}?(\\?*)?(#*)`);

it("matches .js, .mjs, .cjs and their .map variants", async () => {
const files = await Promise.all([
touch("a.js"),
touch("b.mjs"),
touch("c.cjs"),
touch("a.js.map"),
touch("b.mjs.map"),
touch("c.cjs.map"),
]);

const result = await globFiles(JS_AND_MAP_PATTERNS, { root: tmpDir });
result.sort();
expect(result).toEqual(files.sort());
});

it("does NOT match .css, .ts, .json, etc.", async () => {
await touch("style.css");
await touch("types.ts");
await touch("data.json");
await touch("readme.md");

const result = await globFiles(JS_AND_MAP_PATTERNS, { root: tmpDir });
expect(result).toEqual([]);
});

it("works in nested subdirectories", async () => {
const files = await Promise.all([
touch("src", "deep", "a.js"),
touch("src", "deep", "a.js.map"),
]);

const result = await globFiles(JS_AND_MAP_PATTERNS, { root: tmpDir });
result.sort();
expect(result).toEqual(files.sort());
});

it("returns [] for empty directory", async () => {
const result = await globFiles(JS_AND_MAP_PATTERNS, { root: tmpDir });
expect(result).toEqual([]);
});
});
});
10 changes: 9 additions & 1 deletion packages/rollup-plugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,15 @@ export function _rollupPluginInternal(
if (sourcemapsEnabled && options.sourcemaps?.disable !== "disable-upload") {
if (outputOptions.dir) {
const outputDir = outputOptions.dir;
const buildArtifacts = await globFiles(outputDir);
const JS_AND_MAP_PATTERNS = [
"/**/*.js",
"/**/*.mjs",
"/**/*.cjs",
"/**/*.js.map",
"/**/*.mjs.map",
"/**/*.cjs.map",
].map((q) => `${q}?(\\?*)?(#*)`); // We want to allow query and hash strings at the end of files
const buildArtifacts = await globFiles(JS_AND_MAP_PATTERNS, { root: outputDir });
await upload(buildArtifacts);
} else if (outputOptions.file) {
await upload([outputOptions.file]);
Expand Down
Loading