Skip to content

Commit f0fbdeb

Browse files
authored
fix(v10/nuxt): Delete public source maps before Nitro lists its public assets (#25284)
Backport of #25267 Fixes #25242
1 parent 07414f7 commit f0fbdeb

5 files changed

Lines changed: 227 additions & 2 deletions

File tree

‎dev-packages/test-utils/src/index.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ export {
1717
export { getPlaywrightConfig } from './playwright-config';
1818
export { createBasicSentryServer, createTestServer } from './server';
1919

20+
export { startNodeServer } from './node-server';
21+
export type { NodeServerOptions, NodeServer } from './node-server';
22+
2023
export { startMockSentryServer } from './mock-sentry-server';
2124
export type { MockSentryServerOptions, MockSentryServer } from './mock-sentry-server';
2225
export * from './sourcemap-upload-utils';
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { spawn } from 'node:child_process';
2+
3+
export interface NodeServerOptions {
4+
/** Path to the server entry, e.g. `.output/server/index.mjs`. */
5+
entry: string;
6+
port: number;
7+
/** How long to wait for the first response. Defaults to 30 seconds. */
8+
timeoutMs?: number;
9+
}
10+
11+
export interface NodeServer {
12+
baseUrl: string;
13+
/** Stops the server and resolves once its process has exited. */
14+
close: () => Promise<void>;
15+
}
16+
17+
/**
18+
* Starts a built Node server and resolves once it answers HTTP requests. The server gets the port via `PORT`.
19+
*
20+
* Rejects if the server exits or does not answer before the timeout. Its output is printed only then.
21+
*/
22+
export async function startNodeServer({ entry, port, timeoutMs = 30_000 }: NodeServerOptions): Promise<NodeServer> {
23+
const baseUrl = `http://localhost:${port}`;
24+
const child = spawn(process.execPath, [entry], { env: { ...process.env, PORT: String(port) } });
25+
26+
let output = '';
27+
child.stdout.on('data', chunk => (output += chunk));
28+
child.stderr.on('data', chunk => (output += chunk));
29+
30+
const close = async (): Promise<void> => {
31+
if (child.exitCode !== null || child.signalCode !== null) {
32+
return;
33+
}
34+
const exited = new Promise(resolve => child.once('exit', resolve));
35+
child.kill();
36+
await exited;
37+
};
38+
39+
const deadline = Date.now() + timeoutMs;
40+
while (Date.now() < deadline) {
41+
if (child.exitCode !== null) {
42+
throw new Error(`Server ${entry} exited with code ${child.exitCode} before it answered:\n${output}`);
43+
}
44+
45+
try {
46+
await fetch(baseUrl);
47+
return { baseUrl, close };
48+
} catch {
49+
await new Promise(resolve => setTimeout(resolve, 250));
50+
}
51+
}
52+
53+
await close();
54+
throw new Error(`Server ${entry} did not answer on ${baseUrl} within ${timeoutMs}ms:\n${output}`);
55+
}

‎packages/nuxt/src/vite/sourceMapDeletion.ts‎

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { createSentryBuildPluginManager, type Options } from '@sentry/bundler-plugins/core';
1+
import { createSentryBuildPluginManager, globFiles, type Options } from '@sentry/bundler-plugins/core';
2+
import type { Nitro } from 'nitropack';
3+
import * as fs from 'node:fs';
4+
import * as path from 'node:path';
25

36
export function withoutSourceMapDeletion(options: Options): Options {
47
return {
@@ -32,3 +35,62 @@ export async function deleteSourceMapsAfterBuild(options: Options): Promise<void
3235

3336
await sentryBuildPluginManager.deleteArtifacts();
3437
}
38+
39+
/**
40+
* Adds a plugin to Nitro's build that deletes the matching files in Nitro's public dir with `deletePublicSourceMaps`.
41+
*/
42+
export function addPublicSourceMapDeletionPlugin(nitro: Nitro, getOptions: () => Options): void {
43+
const publicDir = nitro.options.output.publicDir;
44+
const rollupConfig = (nitro.options.rollupConfig ??= { output: {} });
45+
46+
rollupConfig.plugins = [
47+
rollupConfig.plugins,
48+
{
49+
name: 'sentry-nuxt-public-source-map-deletion',
50+
// By now the client build is in the public dir, and Nitro has not yet listed its files.
51+
async buildStart() {
52+
await deletePublicSourceMaps(getOptions(), publicDir);
53+
},
54+
},
55+
];
56+
}
57+
58+
/**
59+
* Deletes the files in `publicDir` that match `filesToDeleteAfterUpload`. Nitro answers a request for a file it
60+
* listed at build time but cannot read with a 500. Files outside `publicDir` are deleted in `close`.
61+
*/
62+
export async function deletePublicSourceMaps(options: Options, publicDir: string): Promise<void> {
63+
const filesToDeleteAfterUpload = await options.sourcemaps?.filesToDeleteAfterUpload;
64+
65+
if (filesToDeleteAfterUpload === undefined) {
66+
return;
67+
}
68+
69+
const files = await globFiles(filesToDeleteAfterUpload);
70+
const publicFiles = files.filter(file => isInDirectory(file, publicDir));
71+
72+
await Promise.all(
73+
publicFiles.map(async file => {
74+
logDebug(options, `Deleting ${file} before Nitro bundles the server.`);
75+
// `deleteSourceMapsAfterBuild` tries again in `close`.
76+
await fs.promises.rm(file, { force: true }).catch(error => logDebug(options, `Could not delete ${file}.`, error));
77+
}),
78+
);
79+
}
80+
81+
function logDebug(options: Options, ...messages: unknown[]): void {
82+
if (options.debug) {
83+
// eslint-disable-next-line no-console
84+
console.log('[Sentry]', ...messages);
85+
}
86+
}
87+
88+
function isInDirectory(file: string, directory: string): boolean {
89+
const relativePath = path.relative(directory, file);
90+
return (
91+
relativePath !== '' &&
92+
relativePath !== '..' &&
93+
!relativePath.startsWith(`..${path.sep}`) &&
94+
!path.isAbsolute(relativePath)
95+
);
96+
}

‎packages/nuxt/src/vite/sourceMaps.ts‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import { sentryVitePlugin, type SentryVitePluginOptions } from '@sentry/vite-plu
44
import type { NitroConfig } from 'nitropack';
55
import type { Plugin } from 'vite';
66
import type { SentryNuxtModuleOptions } from '../common/types';
7-
import { deleteSourceMapsAfterBuild, withoutSourceMapDeletion } from './sourceMapDeletion';
7+
import {
8+
addPublicSourceMapDeletionPlugin,
9+
deleteSourceMapsAfterBuild,
10+
withoutSourceMapDeletion,
11+
} from './sourceMapDeletion';
812
import { validateSourceMapsOptionsPlugin } from './sentryVitePlugin';
913

1014
/**
@@ -122,6 +126,12 @@ export function setupSourceMaps(
122126
}
123127
});
124128

129+
nuxt.hook('nitro:init', nitro => {
130+
if (sourceMapsEnabled && !nitro.options.dev && !nuxt.options?._prepare) {
131+
addPublicSourceMapDeletionPlugin(nitro, () => getPluginOptions(moduleOptions, shouldDeleteFilesFallback));
132+
}
133+
});
134+
125135
nuxt.hook('close', async () => {
126136
if (sourceMapsEnabled && !nuxt.options.dev && !nuxt.options?._prepare) {
127137
await deleteSourceMapsAfterBuild(getPluginOptions(moduleOptions, shouldDeleteFilesFallback));
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import * as fs from 'fs';
2+
import * as os from 'os';
3+
import * as path from 'path';
4+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5+
import { deletePublicSourceMaps } from '../../src/vite/sourceMapDeletion';
6+
7+
const BUILD_FILES = [
8+
'.output/public/_nuxt/entry.DxB3kLmA.js',
9+
'.output/public/_nuxt/entry.DxB3kLmA.js.map',
10+
'.output/public/_nuxt/pages/products.Cq81zR4t.js.map',
11+
'.output/public-assets/_nuxt/entry.DxB3kLmA.js.map',
12+
'.output/server/chunks/nitro/nitro.mjs.map',
13+
'.nuxt/dist/server/_nuxt/index-B2kR7mXp.mjs.map',
14+
];
15+
16+
let rootDir: string;
17+
let publicDir: string;
18+
19+
/** Glob patterns need forward slashes, also on Windows. */
20+
function toGlob(relativePattern: string): string {
21+
return path.join(rootDir, relativePattern).split(path.sep).join('/');
22+
}
23+
24+
function remainingFiles(): string[] {
25+
return BUILD_FILES.filter(file => fs.existsSync(path.join(rootDir, file)));
26+
}
27+
28+
describe('deletePublicSourceMaps', () => {
29+
beforeEach(() => {
30+
rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sentry-nuxt-source-map-deletion-'));
31+
publicDir = path.join(rootDir, '.output', 'public');
32+
33+
for (const file of BUILD_FILES) {
34+
fs.mkdirSync(path.dirname(path.join(rootDir, file)), { recursive: true });
35+
fs.writeFileSync(path.join(rootDir, file), '');
36+
}
37+
});
38+
39+
afterEach(() => {
40+
fs.rmSync(rootDir, { recursive: true, force: true });
41+
});
42+
43+
it('deletes the matched files in the public dir and keeps the matched files outside of it', async () => {
44+
await deletePublicSourceMaps(
45+
{
46+
sourcemaps: {
47+
filesToDeleteAfterUpload: [toGlob('.*/**/public*/**/*.map'), toGlob('.*/**/server/**/*.map')],
48+
},
49+
},
50+
publicDir,
51+
);
52+
53+
expect(remainingFiles()).toEqual([
54+
'.output/public/_nuxt/entry.DxB3kLmA.js',
55+
'.output/public-assets/_nuxt/entry.DxB3kLmA.js.map',
56+
'.output/server/chunks/nitro/nitro.mjs.map',
57+
'.nuxt/dist/server/_nuxt/index-B2kR7mXp.mjs.map',
58+
]);
59+
});
60+
61+
it('keeps the files in the public dir that no pattern matches', async () => {
62+
await deletePublicSourceMaps(
63+
{ sourcemaps: { filesToDeleteAfterUpload: toGlob('.output/public/_nuxt/pages/*.map') } },
64+
publicDir,
65+
);
66+
67+
expect(remainingFiles()).toEqual([
68+
'.output/public/_nuxt/entry.DxB3kLmA.js',
69+
'.output/public/_nuxt/entry.DxB3kLmA.js.map',
70+
'.output/public-assets/_nuxt/entry.DxB3kLmA.js.map',
71+
'.output/server/chunks/nitro/nitro.mjs.map',
72+
'.nuxt/dist/server/_nuxt/index-B2kR7mXp.mjs.map',
73+
]);
74+
});
75+
76+
it('resolves a promised list of patterns', async () => {
77+
await deletePublicSourceMaps(
78+
{ sourcemaps: { filesToDeleteAfterUpload: Promise.resolve([toGlob('.output/public/**/*.map')]) } },
79+
publicDir,
80+
);
81+
82+
expect(remainingFiles()).toEqual([
83+
'.output/public/_nuxt/entry.DxB3kLmA.js',
84+
'.output/public-assets/_nuxt/entry.DxB3kLmA.js.map',
85+
'.output/server/chunks/nitro/nitro.mjs.map',
86+
'.nuxt/dist/server/_nuxt/index-B2kR7mXp.mjs.map',
87+
]);
88+
});
89+
90+
it('deletes nothing when no deletion patterns are set', async () => {
91+
await deletePublicSourceMaps({ sourcemaps: {} }, publicDir);
92+
93+
expect(remainingFiles()).toEqual(BUILD_FILES);
94+
});
95+
});

0 commit comments

Comments
 (0)