diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json index 416639132e92..000d5840580e 100644 --- a/dev-packages/browser-integration-tests/package.json +++ b/dev-packages/browser-integration-tests/package.json @@ -77,6 +77,6 @@ "@sentry/rrweb-types": "^2.45.0", "@sentry/wasm": "11.10.0", "@types/node": "^20.19.0", - "glob": "^13.0.6" + "glob": "^14.0.3" } } diff --git a/dev-packages/e2e-tests/test-applications/react-router-sourcemaps/assert-build.ts b/dev-packages/e2e-tests/test-applications/react-router-sourcemaps/assert-build.ts index 2bc4986220d4..ca6acfb5a6aa 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-sourcemaps/assert-build.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-sourcemaps/assert-build.ts @@ -111,4 +111,12 @@ for (const chunk of chunks) { assert.ok(crossCheckedChunks > 0, 'Expected at least one uploaded chunk to cross-check debug IDs against'); console.log(`${crossCheckedChunks} chunk(s) ship a debug ID that was uploaded\n`); +const remainingClientMaps = fs + .readdirSync(CLIENT_ASSETS_DIR, { recursive: true }) + .filter(file => file.endsWith('.map')); +assert.deepEqual(remainingClientMaps, [], 'Expected client source maps to be deleted after upload'); + +const remainingServerMaps = fs.readdirSync('build/server', { recursive: true }).filter(file => file.endsWith('.map')); +assert.ok(remainingServerMaps.length > 0, 'Expected server source maps outside the deletion pattern to be preserved'); + console.log('All react-router source map assertions passed!'); diff --git a/packages/bundler-plugins/package.json b/packages/bundler-plugins/package.json index 3bb53c945278..60df96e18f71 100644 --- a/packages/bundler-plugins/package.json +++ b/packages/bundler-plugins/package.json @@ -117,7 +117,7 @@ "dependencies": { "@sentry/core": "11.10.0", "dotenv": "^17.4.2", - "glob": "^13.0.6", + "glob": "^14.0.3", "magic-string": "~0.30.8 || ^1.0.0", "oxc-parser": "^0.152.0", "sentry": "^0.45.0", diff --git a/packages/bundler-plugins/test/core/glob.test.ts b/packages/bundler-plugins/test/core/glob.test.ts index 74583dd01c4e..ffbc776fecc6 100644 --- a/packages/bundler-plugins/test/core/glob.test.ts +++ b/packages/bundler-plugins/test/core/glob.test.ts @@ -140,6 +140,15 @@ describe('globFiles', () => { expect(result).toEqual([]); }); + it.skipIf(process.platform === 'win32')('matches files with a query or hash suffix', async () => { + const files = await Promise.all([touch('a.js?v=1'), touch('a.js.map#hash'), touch('b.mjs?v=1#hash')]); + await touch('c.css?v=1'); + + const result = await globFiles(JS_AND_MAP_PATTERNS, { root: tmpDir }); + result.sort(); + expect(result).toEqual(files.sort()); + }); + it('works in nested subdirectories', async () => { const files = await Promise.all([touch('src', 'deep', 'a.js'), touch('src', 'deep', 'a.js.map')]); diff --git a/packages/react-router/package.json b/packages/react-router/package.json index 07d576d599b0..a97bb7ee240e 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -64,7 +64,6 @@ "@sentry/server-runtime-injection": "11.10.0", "@sentry/server-utils": "11.10.0", "@sentry/bundler-plugins": "11.10.0", - "glob": "^13.0.6", "sentry": "^0.44.0" }, "devDependencies": { diff --git a/packages/react-router/src/vite/buildEnd/handleOnBuildEnd.ts b/packages/react-router/src/vite/buildEnd/handleOnBuildEnd.ts index a4cf0403e248..0f83b4553ecb 100644 --- a/packages/react-router/src/vite/buildEnd/handleOnBuildEnd.ts +++ b/packages/react-router/src/vite/buildEnd/handleOnBuildEnd.ts @@ -1,8 +1,9 @@ import { rm } from 'node:fs/promises'; +import * as path from 'node:path'; import type { Config } from '@react-router/dev/config'; import { createSentrySDK } from 'sentry'; +import { globFiles } from '@sentry/bundler-plugins/core'; import type { SentryVitePluginOptions } from '@sentry/bundler-plugins/vite'; -import { glob } from 'glob'; import type { SentryReactRouterBuildOptions } from '../types'; type BuildEndHook = NonNullable; @@ -24,6 +25,11 @@ function getSentryConfig(viteConfig: unknown): SentryReactRouterBuildOptions { return (viteConfig as { sentryConfig: SentryReactRouterBuildOptions }).sentryConfig; } +/** Glob patterns need forward slashes, but React Router resolves `buildDirectory` with the platform separator. */ +function toGlobPath(directory: string): string { + return directory.split(path.sep).join('/'); +} + /** * A build end hook that handles Sentry release creation and source map uploads. * It creates a new Sentry release if configured, uploads source maps to Sentry, @@ -123,7 +129,7 @@ export const sentryOnBuildEnd: BuildEndHook = async ({ reactRouterConfig, viteCo // set a default value no option was set if (typeof updatedFilesToDeleteAfterUpload === 'undefined') { - updatedFilesToDeleteAfterUpload = [`${reactRouterConfig.buildDirectory}/**/*.map`]; + updatedFilesToDeleteAfterUpload = [`${toGlobPath(reactRouterConfig.buildDirectory)}/**/*.map`]; debug && // eslint-disable-next-line no-console console.info( @@ -134,10 +140,7 @@ export const sentryOnBuildEnd: BuildEndHook = async ({ reactRouterConfig, viteCo } if (updatedFilesToDeleteAfterUpload) { try { - const filePathsToDelete = await glob(updatedFilesToDeleteAfterUpload, { - absolute: true, - nodir: true, - }); + const filePathsToDelete = await globFiles(updatedFilesToDeleteAfterUpload); if (debug) { filePathsToDelete.forEach(filePathToDelete => { // eslint-disable-next-line no-console diff --git a/packages/react-router/test/vite/buildEnd/handleOnBuildEnd.test.ts b/packages/react-router/test/vite/buildEnd/handleOnBuildEnd.test.ts index 560552ed278d..548fea3b7145 100644 --- a/packages/react-router/test/vite/buildEnd/handleOnBuildEnd.test.ts +++ b/packages/react-router/test/vite/buildEnd/handleOnBuildEnd.test.ts @@ -1,6 +1,7 @@ import { createSentrySDK } from 'sentry'; import { rm } from 'node:fs/promises'; -import { glob } from 'glob'; +import type * as path from 'node:path'; +import { globFiles } from '@sentry/bundler-plugins/core'; import type { ResolvedConfig } from 'vite'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { sentryOnBuildEnd } from '../../../src/vite/buildEnd/handleOnBuildEnd'; @@ -11,7 +12,7 @@ vi.mock('node:fs/promises', () => { const rm = vi.fn().mockResolvedValue(undefined); return { rm, default: { rm } }; }); -vi.mock('glob'); +vi.mock('@sentry/bundler-plugins/core', () => ({ globFiles: vi.fn() })); type TestConfig = ResolvedConfig & { sentryConfig: SentryReactRouterBuildOptions; @@ -62,7 +63,7 @@ describe('sentryOnBuildEnd', () => { vi.mocked(createSentrySDK) .mockReset() .mockReturnValue(mockSentrySdkInstance as unknown as ReturnType); - vi.mocked(glob).mockResolvedValue(['/build/file1.map', '/build/file2.map']); + vi.mocked(globFiles).mockResolvedValue(['/build/file1.map', '/build/file2.map']); vi.mocked(rm).mockResolvedValue(undefined); }); @@ -126,10 +127,7 @@ describe('sentryOnBuildEnd', () => { directory: '/build', release: '1.2.3', }); - expect(glob).toHaveBeenCalledWith(['./build/custom/**/*.map'], { - absolute: true, - nodir: true, - }); + expect(globFiles).toHaveBeenCalledWith(['./build/custom/**/*.map']); }); it('should upload source maps when enabled', async () => { @@ -196,7 +194,7 @@ describe('sentryOnBuildEnd', () => { expect(mockSentrySdkInstance.sourcemap.inject).not.toHaveBeenCalled(); expect(mockSentrySdkInstance.sourcemap.upload).not.toHaveBeenCalled(); - expect(glob).not.toHaveBeenCalled(); + expect(globFiles).not.toHaveBeenCalled(); expect(rm).not.toHaveBeenCalled(); }); @@ -219,7 +217,37 @@ describe('sentryOnBuildEnd', () => { expect(mockSentrySdkInstance.sourcemap.inject).toHaveBeenCalledWith({ directory: '/build' }); expect(mockSentrySdkInstance.sourcemap.upload).not.toHaveBeenCalled(); - expect(glob).not.toHaveBeenCalled(); + expect(globFiles).not.toHaveBeenCalled(); + }); + + it('deletes the source maps in the build directory by default', async () => { + // @ts-expect-error - mocking the React config + await sentryOnBuildEnd(defaultConfig); + + expect(globFiles).toHaveBeenCalledWith(['/build/**/*.map']); + expect(rm).toHaveBeenCalledWith('/build/file1.map', { force: true }); + expect(rm).toHaveBeenCalledWith('/build/file2.map', { force: true }); + }); + + it('builds the default deletion pattern with forward slashes on Windows', async () => { + vi.doMock('node:path', async importOriginal => ({ + ...(await importOriginal()), + sep: '\\', + })); + const { sentryOnBuildEnd: sentryOnBuildEndWin32 } = await import('../../../src/vite/buildEnd/handleOnBuildEnd'); + const { globFiles: globFilesWin32 } = await import('@sentry/bundler-plugins/core'); + vi.mocked(globFilesWin32).mockResolvedValue([]); + + const config = { + ...defaultConfig, + reactRouterConfig: { ...defaultConfig.reactRouterConfig, buildDirectory: 'C:\\app\\build' }, + }; + + // @ts-expect-error - mocking the React config + await sentryOnBuildEndWin32(config); + + expect(globFilesWin32).toHaveBeenCalledWith(['C:/app/build/**/*.map']); + vi.doUnmock('node:path'); }); it('should delete custom files after upload when specified', async () => { @@ -239,10 +267,7 @@ describe('sentryOnBuildEnd', () => { // @ts-expect-error - mocking the React config await sentryOnBuildEnd(config); - expect(glob).toHaveBeenCalledWith('/custom/**/*.map', { - absolute: true, - nodir: true, - }); + expect(globFiles).toHaveBeenCalledWith('/custom/**/*.map'); }); it('should handle errors during release creation gracefully', async () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6ac487eaedde..adb34c67fe56 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -344,8 +344,8 @@ importers: specifier: ^20.19.0 version: 20.19.43 glob: - specifier: ^13.0.6 - version: 13.0.6 + specifier: ^14.0.3 + version: 14.0.3 dev-packages/bun-integration-tests: dependencies: @@ -1163,8 +1163,8 @@ importers: specifier: ^17.4.2 version: 17.4.2 glob: - specifier: ^13.0.6 - version: 13.0.6 + specifier: ^14.0.3 + version: 14.0.3 magic-string: specifier: ~0.30.8 || ^1.0.0 version: 0.30.21 @@ -1977,9 +1977,6 @@ importers: '@sentry/server-utils': specifier: 11.10.0 version: link:../server-utils - glob: - specifier: ^13.0.6 - version: 13.0.6 sentry: specifier: ^0.44.0 version: 0.44.0 @@ -5805,6 +5802,9 @@ packages: resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} + '@isaacs/nonrecursive-glob-match@1.0.3': + resolution: {integrity: sha512-SI7kyKHq4XXDRozw+r9GlRZkia1ZkFrpLW98r2IC2Bpr8YkFGE3fvjzmxCG0x78A++69HmDNrDvSMYBZmHGpKA==} + '@istanbuljs/load-nyc-config@1.1.0': resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} engines: {node: '>=8'} @@ -10169,6 +10169,10 @@ packages: brace-expansion@2.1.7: resolution: {integrity: sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==} + brace-expansion@5.0.12: + resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} + engines: {node: 20 || >=22} + brace-expansion@5.0.8: resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==} engines: {node: 20 || >=22} @@ -12566,6 +12570,10 @@ packages: resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} engines: {node: 18 || 20 || >=22} + glob@14.0.3: + resolution: {integrity: sha512-A1vjIwC3qpdCso45Iw127gnp7sPhqgY8lUQhDrArIs+GbmfaaVPFuoyy7WyLT9EPZCRZ7j7aLdwUhQmHWesEow==} + engines: {node: 18 || 20 || >=22} + glob@5.0.15: resolution: {integrity: sha512-c9IPMazfRITpmAAKi22dK1VKxGDX9ehhqfABDriL/lzO92xcUKEJPQHrVA/2YHSNFB4iFlykVmWvwo48nr3OxA==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me @@ -14422,6 +14430,10 @@ packages: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} + minimatch@11.0.3: + resolution: {integrity: sha512-iny/40PEGkNJQ5/Pv6z3nm6FbRAby32GeZRjCUJk2WnNDbgPSTcdI+DDvLqsgBdD4cD5sMtMheqPPnehCCa8Nw==} + engines: {node: 18 || 20 || >=22} + minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} @@ -15601,6 +15613,10 @@ packages: resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} engines: {node: 18 || 20 || >=22} + path-scurry@3.0.0: + resolution: {integrity: sha512-1pTbQqH1wIwMi/eleQ5leFTTCkzFsPcgXNjwOA8YGGW/W4/kHT3dssE7Z6NH+zuqNLOzOB0uaD61yl7cAcVhgg==} + engines: {node: 18 || 20 || >=22} + path-to-regexp@0.1.12: resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} @@ -24073,6 +24089,8 @@ snapshots: dependencies: minipass: 7.1.3 + '@isaacs/nonrecursive-glob-match@1.0.3': {} + '@istanbuljs/load-nyc-config@1.1.0': dependencies: camelcase: 5.3.1 @@ -29097,6 +29115,10 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@5.0.12: + dependencies: + balanced-match: 4.0.3 + brace-expansion@5.0.8: dependencies: balanced-match: 4.0.3 @@ -31977,6 +31999,12 @@ snapshots: minipass: 7.1.3 path-scurry: 2.0.2 + glob@14.0.3: + dependencies: + minimatch: 11.0.3 + minipass: 7.1.3 + path-scurry: 3.0.0 + glob@5.0.15: dependencies: inflight: 1.0.6 @@ -34310,6 +34338,11 @@ snapshots: dependencies: brace-expansion: 5.0.8 + minimatch@11.0.3: + dependencies: + '@isaacs/nonrecursive-glob-match': 1.0.3 + brace-expansion: 5.0.12 + minimatch@3.1.5: dependencies: brace-expansion: 1.1.21 @@ -36244,6 +36277,11 @@ snapshots: lru-cache: 11.4.0 minipass: 7.1.3 + path-scurry@3.0.0: + dependencies: + lru-cache: 11.4.0 + minipass: 7.1.3 + path-to-regexp@0.1.12: {} path-to-regexp@1.9.0: