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
34 changes: 34 additions & 0 deletions packages/nuxt/src/vite/sourceMapDeletion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { createSentryBuildPluginManager, type Options } from '@sentry/bundler-plugins/core';

export function withoutSourceMapDeletion(options: Options): Options {
return {
...options,
sourcemaps: {
...options.sourcemaps,
filesToDeleteAfterUpload: undefined,
},
};
}

export async function deleteSourceMapsAfterBuild(options: Options): Promise<void> {
const filesToDeleteAfterUpload = await options.sourcemaps?.filesToDeleteAfterUpload;

if (filesToDeleteAfterUpload === undefined) {
return;
}

const deletionOptions: Options = {
...options,
sourcemaps: {
...options.sourcemaps,
filesToDeleteAfterUpload,
},
};

const sentryBuildPluginManager = createSentryBuildPluginManager(deletionOptions, {
buildTool: 'nuxt',
loggerPrefix: '[Sentry Nuxt]',
});

await sentryBuildPluginManager.deleteArtifacts();
}
11 changes: 9 additions & 2 deletions packages/nuxt/src/vite/sourceMaps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { sentryVitePlugin, type SentryVitePluginOptions } from '@sentry/bundler-
import type { NitroConfig } from 'nitropack';
import type { Plugin } from 'vite';
import type { SentryNuxtModuleOptions } from '../common/types';
import { deleteSourceMapsAfterBuild, withoutSourceMapDeletion } from './sourceMapDeletion';
import { validateSourceMapsOptionsPlugin } from './sentryVitePlugin';

/**
Expand Down Expand Up @@ -71,7 +72,7 @@ export function setupSourceMaps(
[
validateSourceMapsOptionsPlugin({ nuxt, moduleOptions, sourceMapsEnabled }),
// Vite plugin is added on the client and server side (plugin runs for both builds)
...sentryVitePlugin(getPluginOptions(moduleOptions, shouldDeleteFilesFallback)),
...sentryVitePlugin(withoutSourceMapDeletion(getPluginOptions(moduleOptions, shouldDeleteFilesFallback))),
],
{ dev: false, build: true }, // Only add source map plugin during build
);
Expand All @@ -98,10 +99,16 @@ export function setupSourceMaps(
// Add Sentry plugin
// Runs only on server-side (Nitro)
nitroConfig.rollupConfig.plugins.push(
sentryRollupPlugin(getPluginOptions(moduleOptions, shouldDeleteFilesFallback)),
sentryRollupPlugin(withoutSourceMapDeletion(getPluginOptions(moduleOptions, shouldDeleteFilesFallback))),
);
}
});

nuxt.hook('close', async () => {
if (sourceMapsEnabled && !nuxt.options.dev && !nuxt.options?._prepare) {
await deleteSourceMapsAfterBuild(getPluginOptions(moduleOptions, shouldDeleteFilesFallback));
}
Comment on lines +107 to +110

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Source maps are incorrectly deleted when sourcemaps.disable is set to 'disable-upload', preventing manual uploads.
Severity: MEDIUM

Suggested Fix

Update the condition in the close hook to also check if moduleOptions.sourcemaps?.disable is not equal to 'disable-upload'. This will ensure that the source map deletion logic is skipped when this specific setting is used, aligning the behavior with its intended purpose.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: packages/nuxt/src/vite/sourceMaps.ts#L107-L110

Potential issue: When the `sourcemaps.disable` option is set to the string
`'disable-upload'`, the source maps are still deleted after the build process concludes.
The check `moduleOptions.sourcemaps?.disable !== true` in the `close` hook evaluates to
true in this case, which incorrectly triggers the source map deletion logic. This
prevents users from manually uploading the source maps, which is the intended purpose of
the `'disable-upload'` setting. The `deleteArtifacts` function proceeds to delete the
files without any further checks on the `disable` option's value.

Did we get this right? 👍 / 👎 to inform future reviews.

});
}

/**
Expand Down
144 changes: 125 additions & 19 deletions packages/nuxt/test/vite/sourceMaps-nuxtHooks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,25 @@ function createMockNuxt(options: {
}

describe('setupSourceMaps hooks', () => {
const defaultFilesToDeleteAfterUpload = [
'.*/**/public/**/*.map',
'.*/**/server/**/*.map',
'.*/**/output/**/*.map',
'.*/**/function/**/*.map',
];

const mockSentryVitePlugin = vi.fn(() => [{ name: 'sentry-vite-plugin' }]);
const mockSentryRollupPlugin = vi.fn(() => ({ name: 'sentry-rollup-plugin' }));
const mockDeleteArtifacts = vi.fn().mockResolvedValue(undefined);
const mockCreateSentryBuildPluginManager = vi.fn(() => ({ deleteArtifacts: mockDeleteArtifacts }));

const consoleLogSpy = vi.spyOn(console, 'log');
const consoleWarnSpy = vi.spyOn(console, 'warn');

beforeAll(() => {
vi.doMock('@sentry/bundler-plugins/core', () => ({
createSentryBuildPluginManager: mockCreateSentryBuildPluginManager,
}));
vi.doMock('@sentry/bundler-plugins/vite', () => ({
sentryVitePlugin: mockSentryVitePlugin,
}));
Expand All @@ -65,6 +77,7 @@ describe('setupSourceMaps hooks', () => {
afterAll(() => {
consoleLogSpy.mockRestore();
consoleWarnSpy.mockRestore();
vi.doUnmock('@sentry/bundler-plugins/core');
vi.doUnmock('@sentry/bundler-plugins/vite');
vi.doUnmock('@sentry/bundler-plugins/rollup');
});
Expand All @@ -74,6 +87,8 @@ describe('setupSourceMaps hooks', () => {
consoleWarnSpy.mockClear();
mockSentryVitePlugin.mockClear();
mockSentryRollupPlugin.mockClear();
mockCreateSentryBuildPluginManager.mockClear();
mockDeleteArtifacts.mockClear();
});

describe('vite plugin registration', () => {
Expand Down Expand Up @@ -163,14 +178,7 @@ describe('setupSourceMaps hooks', () => {
});

describe('shouldDeleteFilesFallback passed to getPluginOptions in Vite plugin', () => {
const defaultFilesToDeleteAfterUpload = [
'.*/**/public/**/*.map',
'.*/**/server/**/*.map',
'.*/**/output/**/*.map',
'.*/**/function/**/*.map',
];

it('sentryVitePlugin is called with fallback filesToDeleteAfterUpload when source maps are unset', async () => {
it('does not pass fallback deletion patterns to the Vite plugin', async () => {
const { setupSourceMaps } = await import('../../src/vite/sourceMaps');
const mockNuxt = createMockNuxt({
_prepare: false,
Expand All @@ -181,13 +189,28 @@ describe('setupSourceMaps hooks', () => {

setupSourceMaps({ debug: false }, mockNuxt as unknown as Nuxt, mockAddVitePlugin);

expect(mockSentryVitePlugin).toHaveBeenCalledWith(
expect.objectContaining({
sourcemaps: expect.objectContaining({
filesToDeleteAfterUpload: defaultFilesToDeleteAfterUpload,
}),
}),
);
expect(mockSentryVitePlugin).toHaveBeenCalledWith({
applicationKey: undefined,
org: undefined,
project: undefined,
authToken: undefined,
telemetry: true,
url: undefined,
headers: undefined,
debug: false,
silent: false,
errorHandler: undefined,
bundleSizeOptimizations: undefined,
release: { name: undefined },
_metaOptions: { telemetry: { metaFramework: 'nuxt' } },
sourcemaps: {
disable: undefined,
assets: undefined,
ignore: undefined,
filesToDeleteAfterUpload: undefined,
rewriteSources: expect.any(Function),
},
});
});

it('sentryRollupPlugin is called without filesToDeleteAfterUpload when source maps are explicitly enabled', async () => {
Expand All @@ -205,10 +228,93 @@ describe('setupSourceMaps hooks', () => {
const nitroConfig = { rollupConfig: { plugins: [] as unknown[], output: {} }, dev: false };
await mockNuxt.triggerHook('nitro:config', nitroConfig);

const pluginOptions = (mockSentryRollupPlugin?.mock?.calls?.[0] as unknown[])?.[0] as {
sourcemaps?: { filesToDeleteAfterUpload?: string[] };
};
expect(pluginOptions?.sourcemaps?.filesToDeleteAfterUpload).toBeUndefined();
expect(mockSentryRollupPlugin).toHaveBeenCalledWith(
expect.objectContaining({
sourcemaps: expect.objectContaining({ filesToDeleteAfterUpload: undefined }),
}),
);
});
});

describe('close hook', () => {
it('deletes source maps after the build using fallback patterns', async () => {
const { setupSourceMaps } = await import('../../src/vite/sourceMaps');
const mockNuxt = createMockNuxt({
_prepare: false,
dev: false,
sourcemap: { client: undefined, server: undefined },
});
const { mockAddVitePlugin } = createMockAddVitePlugin();

setupSourceMaps({ debug: false }, mockNuxt as unknown as Nuxt, mockAddVitePlugin);
await mockNuxt.triggerHook('modules:done');
await mockNuxt.triggerHook('close');

expect(mockCreateSentryBuildPluginManager).toHaveBeenCalledWith(
expect.objectContaining({
sourcemaps: expect.objectContaining({ filesToDeleteAfterUpload: defaultFilesToDeleteAfterUpload }),
}),
{ buildTool: 'nuxt', loggerPrefix: '[Sentry Nuxt]' },
);
expect(mockDeleteArtifacts).toHaveBeenCalledTimes(1);
});

it('uses user-provided deletion patterns after the build', async () => {
const { setupSourceMaps } = await import('../../src/vite/sourceMaps');
const mockNuxt = createMockNuxt({
_prepare: false,
dev: false,
sourcemap: { client: true, server: true },
});
const { mockAddVitePlugin } = createMockAddVitePlugin();
const filesToDeleteAfterUpload = ['.output/**/*.map'];

setupSourceMaps({ sourcemaps: { filesToDeleteAfterUpload } }, mockNuxt as unknown as Nuxt, mockAddVitePlugin);
await mockNuxt.triggerHook('modules:done');
await mockNuxt.triggerHook('close');

expect(mockCreateSentryBuildPluginManager).toHaveBeenCalledWith(
expect.objectContaining({
sourcemaps: expect.objectContaining({ filesToDeleteAfterUpload }),
}),
{ buildTool: 'nuxt', loggerPrefix: '[Sentry Nuxt]' },
);
expect(mockDeleteArtifacts).toHaveBeenCalledTimes(1);
});

it('does not create a manager when deletion is not configured', async () => {
const { setupSourceMaps } = await import('../../src/vite/sourceMaps');
const mockNuxt = createMockNuxt({
_prepare: false,
dev: false,
sourcemap: { client: true, server: true },
});
const { mockAddVitePlugin } = createMockAddVitePlugin();

setupSourceMaps({}, mockNuxt as unknown as Nuxt, mockAddVitePlugin);
await mockNuxt.triggerHook('modules:done');
await mockNuxt.triggerHook('close');

expect(mockCreateSentryBuildPluginManager).not.toHaveBeenCalled();
expect(mockDeleteArtifacts).not.toHaveBeenCalled();
});

it.each([
{ label: 'prepare mode', nuxtOptions: { _prepare: true, dev: false } },
{ label: 'dev mode', nuxtOptions: { _prepare: false, dev: true } },
])('does not delete source maps in $label', async ({ nuxtOptions }) => {
const { setupSourceMaps } = await import('../../src/vite/sourceMaps');
const mockNuxt = createMockNuxt(nuxtOptions);
const { mockAddVitePlugin } = createMockAddVitePlugin();

setupSourceMaps(
{ sourcemaps: { filesToDeleteAfterUpload: ['.output/**/*.map'] } },
mockNuxt as unknown as Nuxt,
mockAddVitePlugin,
);
await mockNuxt.triggerHook('close');

expect(mockCreateSentryBuildPluginManager).not.toHaveBeenCalled();
});
});

Expand Down
Loading