Skip to content

feat(sveltekit): Streamline build logs #17306

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Aug 7, 2025
Merged
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
185 changes: 82 additions & 103 deletions packages/sveltekit/src/vite/sourceMaps.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable max-lines */
import { consoleSandbox, escapeStringForRegex, uuid4 } from '@sentry/core';
import { escapeStringForRegex, uuid4 } from '@sentry/core';
import { getSentryRelease } from '@sentry/node';
import type { SentryVitePluginOptions } from '@sentry/vite-plugin';
import { sentryVitePlugin } from '@sentry/vite-plugin';
Expand Down Expand Up @@ -27,6 +27,8 @@ type Sorcery = {
// and we only want to generate a uuid once in case we have to fall back to it.
const releaseName = detectSentryRelease();

type FilesToDeleteAfterUpload = string | string[] | undefined;

/**
* Creates a new Vite plugin that uses the unplugin-based Sentry Vite plugin to create
* releases and upload source maps to Sentry.
Expand Down Expand Up @@ -60,8 +62,12 @@ export async function makeCustomSentryVitePlugins(options?: CustomSentryVitePlug
},
};

const { promise: filesToDeleteAfterUpload, resolve: resolveFilesToDeleteAfterUpload } =
createFilesToDeleteAfterUploadPromise();
let _resolveFilesToDeleteAfterUpload:
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if we should create a utility that does this, but then does a node version check to run Promise.withResolvers

| undefined
| ((value: FilesToDeleteAfterUpload | Promise<FilesToDeleteAfterUpload>) => void);
const filesToDeleteAfterUploadPromise = new Promise<FilesToDeleteAfterUpload>(resolve => {
_resolveFilesToDeleteAfterUpload = resolve;
});

const mergedOptions = {
...defaultPluginOptions,
Expand All @@ -72,7 +78,7 @@ export async function makeCustomSentryVitePlugins(options?: CustomSentryVitePlug
},
sourcemaps: {
...options?.sourcemaps,
filesToDeleteAfterUpload,
filesToDeleteAfterUpload: filesToDeleteAfterUploadPromise,
},
};

Expand Down Expand Up @@ -100,7 +106,7 @@ export async function makeCustomSentryVitePlugins(options?: CustomSentryVitePlug
);

// resolving filesToDeleteAfterUpload here, because we return the original deletion plugin which awaits the promise
resolveFilesToDeleteAfterUpload(undefined);
_resolveFilesToDeleteAfterUpload?.(undefined);

return sentryPlugins;
}
Expand All @@ -113,7 +119,7 @@ export async function makeCustomSentryVitePlugins(options?: CustomSentryVitePlug
);

// resolving filesToDeleteAfterUpload here, because we return the original deletion plugin which awaits the promise
resolveFilesToDeleteAfterUpload(undefined);
_resolveFilesToDeleteAfterUpload?.(undefined);

return sentryPlugins;
}
Expand All @@ -126,7 +132,7 @@ export async function makeCustomSentryVitePlugins(options?: CustomSentryVitePlug
);

// resolving filesToDeleteAfterUpload here, because we return the original deletion plugin which awaits the promise
resolveFilesToDeleteAfterUpload(undefined);
_resolveFilesToDeleteAfterUpload?.(undefined);

return sentryPlugins;
}
Expand All @@ -153,64 +159,40 @@ export async function makeCustomSentryVitePlugins(options?: CustomSentryVitePlug
name: 'sentry-sveltekit-update-source-map-setting-plugin',
apply: 'build', // only apply this plugin at build time
config: async (config: UserConfig) => {
const settingKey = 'build.sourcemap';

const { updatedSourceMapSetting, previousSourceMapSetting } = getUpdatedSourceMapSetting(config);

const userProvidedFilesToDeleteAfterUpload = await options?.sourcemaps?.filesToDeleteAfterUpload;

if (previousSourceMapSetting === 'unset') {
consoleSandbox(() => {
// eslint-disable-next-line no-console
console.log(`[Sentry] Enabled source map generation in the build options with \`${settingKey}: "hidden"\`.`);
});

if (userProvidedFilesToDeleteAfterUpload) {
resolveFilesToDeleteAfterUpload(userProvidedFilesToDeleteAfterUpload);
} else {
// Including all hidden (`.*`) directories by default so that folders like .vercel,
// .netlify, etc are also cleaned up. Additionally, we include the adapter output
// dir which could be a non-hidden directory, like `build` for the Node adapter.
const defaultFileDeletionGlob = ['./.*/**/*.map', `./${adapterOutputDir}/**/*.map`];

consoleSandbox(() => {
// eslint-disable-next-line no-console
console.warn(
`[Sentry] Automatically setting \`sourceMapsUploadOptions.sourcemaps.filesToDeleteAfterUpload: [${defaultFileDeletionGlob
.map(file => `"${file}"`)
.join(', ')}]\` to delete generated source maps after they were uploaded to Sentry.`,
);
});
return {
...config,
build: {
...config.build,
sourcemap: _getUpdatedSourceMapSettings(config, options),
},
};
},
};

// In case we enabled source maps and users didn't specify a glob patter to delete, we set a default pattern:
resolveFilesToDeleteAfterUpload(defaultFileDeletionGlob);
}
const filesToDeleteAfterUploadConfigPlugin: Plugin = {
name: 'sentry-sveltekit-files-to-delete-after-upload-setting-plugin',
apply: 'build', // only apply this plugin at build time
config: (config: UserConfig) => {
const originalFilesToDeleteAfterUpload = options?.sourcemaps?.filesToDeleteAfterUpload;

return {
...config,
build: { ...config.build, sourcemap: updatedSourceMapSetting },
};
}
if (typeof originalFilesToDeleteAfterUpload === 'undefined' && typeof config.build?.sourcemap === 'undefined') {
// Including all hidden (`.*`) directories by default so that folders like .vercel,
// .netlify, etc are also cleaned up. Additionally, we include the adapter output
// dir which could be a non-hidden directory, like `build` for the Node adapter.
const defaultFileDeletionGlob = ['./.*/**/*.map', `./${adapterOutputDir}/**/*.map`];

if (previousSourceMapSetting === 'disabled') {
consoleSandbox(() => {
// eslint-disable-next-line no-console
console.warn(
`[Sentry] Parts of source map generation are currently disabled in your Vite configuration (\`${settingKey}: false\`). This setting is either a default setting or was explicitly set in your configuration. Sentry won't override this setting. Without source maps, code snippets on the Sentry Issues page will remain minified. To show unminified code, enable source maps in \`${settingKey}\` (e.g. by setting them to \`hidden\`).`,
debug &&
// eslint-disable-next-line no-console
console.info(
`[Sentry] Automatically setting \`sourceMapsUploadOptions.sourcemaps.filesToDeleteAfterUpload: [${defaultFileDeletionGlob
.map(file => `"${file}"`)
.join(', ')}]\` to delete generated source maps after they were uploaded to Sentry.`,
);
});
} else if (previousSourceMapSetting === 'enabled') {
if (mergedOptions?.debug) {
consoleSandbox(() => {
// eslint-disable-next-line no-console
console.log(
`[Sentry] We discovered you enabled source map generation in your Vite configuration (\`${settingKey}\`). Sentry will keep this source map setting. This will un-minify the code snippet on the Sentry Issue page.`,
);
});
}
}

resolveFilesToDeleteAfterUpload(userProvidedFilesToDeleteAfterUpload);
_resolveFilesToDeleteAfterUpload?.(defaultFileDeletionGlob);
} else {
_resolveFilesToDeleteAfterUpload?.(originalFilesToDeleteAfterUpload);
}

return config;
},
Expand Down Expand Up @@ -390,18 +372,14 @@ export async function makeCustomSentryVitePlugins(options?: CustomSentryVitePlug
return [
...unchangedSentryVitePlugins,
sourceMapSettingsPlugin,
filesToDeleteAfterUploadConfigPlugin,
customReleaseManagementPlugin,
customDebugIdUploadPlugin,
customFileDeletionPlugin,
];
}

/**
* Whether the user enabled (true, 'hidden', 'inline') or disabled (false) source maps
*/
type UserSourceMapSetting = 'enabled' | 'disabled' | 'unset' | undefined;

/** There are 3 ways to set up source map generation (https://github.com/getsentry/sentry-javascript/issues/13993)
/** There are 3 ways to set up source map generation (https://github.com/getsentry/sentry-j avascript/issues/13993)
*
* 1. User explicitly disabled source maps
* - keep this setting (emit a warning that errors won't be unminified in Sentry)
Expand All @@ -416,30 +394,50 @@ type UserSourceMapSetting = 'enabled' | 'disabled' | 'unset' | undefined;
*
* --> only exported for testing
*/
export function getUpdatedSourceMapSetting(viteConfig: {
build?: {
sourcemap?: boolean | 'inline' | 'hidden';
};
}): { updatedSourceMapSetting: boolean | 'inline' | 'hidden'; previousSourceMapSetting: UserSourceMapSetting } {
export function _getUpdatedSourceMapSettings(
viteConfig: UserConfig,
sentryPluginOptions?: CustomSentryVitePluginOptions,
): boolean | 'inline' | 'hidden' {
viteConfig.build = viteConfig.build || {};

const originalSourcemapSetting = viteConfig.build.sourcemap;
const viteSourceMap = viteConfig?.build?.sourcemap;
let updatedSourceMapSetting = viteSourceMap;

if (originalSourcemapSetting === false) {
return {
previousSourceMapSetting: 'disabled',
updatedSourceMapSetting: originalSourcemapSetting,
};
}
const settingKey = 'build.sourcemap';
const debug = sentryPluginOptions?.debug;

if (viteSourceMap === false) {
updatedSourceMapSetting = viteSourceMap;

if (debug) {
// Longer debug message with more details
// eslint-disable-next-line no-console
console.warn(
`[Sentry] Source map generation is currently disabled in your Vite configuration (\`${settingKey}: false \`). This setting is either a default setting or was explicitly set in your configuration. Sentry won't override this setting. Without source maps, code snippets on the Sentry Issues page will remain minified. To show unminified code, enable source maps in \`${settingKey}\` (e.g. by setting them to \`hidden\`).`,
);
} else {
// eslint-disable-next-line no-console
console.warn('[Sentry] Source map generation is disabled in your Vite configuration.');
}
} else if (viteSourceMap && ['hidden', 'inline', true].includes(viteSourceMap)) {
updatedSourceMapSetting = viteSourceMap;

debug &&
// eslint-disable-next-line no-console
console.log(
`[Sentry] We discovered \`${settingKey}\` is set to \`${viteSourceMap.toString()}\`. Sentry will keep this source map setting. This will un-minify the code snippet on the Sentry Issue page.`,
);
} else {
updatedSourceMapSetting = 'hidden';

if (originalSourcemapSetting && ['hidden', 'inline', true].includes(originalSourcemapSetting)) {
return { previousSourceMapSetting: 'enabled', updatedSourceMapSetting: originalSourcemapSetting };
debug &&
// eslint-disable-next-line no-console
console.log(
`[Sentry] Enabled source map generation in the build options with \`${settingKey}: 'hidden'\`. The source maps will be deleted after they were uploaded to Sentry.`,
);
}

return {
previousSourceMapSetting: 'unset',
updatedSourceMapSetting: 'hidden',
};
return updatedSourceMapSetting;
}

function getFiles(dir: string): string[] {
Expand Down Expand Up @@ -475,22 +473,3 @@ function detectSentryRelease(): string {

return release;
}

/**
* Creates a deferred promise that can be resolved/rejected by calling the
* `resolve` or `reject` function.
* Inspired by: https://stackoverflow.com/a/69027809
*/
function createFilesToDeleteAfterUploadPromise(): {
promise: Promise<string | string[] | undefined>;
resolve: (value: string | string[] | undefined) => void;
reject: (reason?: unknown) => void;
} {
let resolve!: (value: string | string[] | undefined) => void;
let reject!: (reason?: unknown) => void;
const promise = new Promise<string | string[] | undefined>((res, rej) => {
resolve = res;
reject = rej;
});
return { resolve, reject, promise };
}
7 changes: 4 additions & 3 deletions packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ describe('sentrySvelteKit()', () => {

expect(plugins).toBeInstanceOf(Array);
// 1 auto instrument plugin + 5 source maps plugins
expect(plugins).toHaveLength(8);
expect(plugins).toHaveLength(9);
});

it('returns the custom sentry source maps upload plugin, unmodified sourcemaps plugins and the auto-instrument plugin by default', async () => {
Expand All @@ -56,6 +56,7 @@ describe('sentrySvelteKit()', () => {
'sentry-vite-release-injection-plugin',
'sentry-vite-debug-id-injection-plugin',
'sentry-sveltekit-update-source-map-setting-plugin',
'sentry-sveltekit-files-to-delete-after-upload-setting-plugin',
// custom release plugin:
'sentry-sveltekit-release-management-plugin',
// custom source maps plugin:
Expand Down Expand Up @@ -86,7 +87,7 @@ describe('sentrySvelteKit()', () => {
it("doesn't return the auto instrument plugin if autoInstrument is `false`", async () => {
const plugins = await getSentrySvelteKitPlugins({ autoInstrument: false });
const pluginNames = plugins.map(plugin => plugin.name);
expect(plugins).toHaveLength(7);
expect(plugins).toHaveLength(8);
expect(pluginNames).not.toContain('sentry-upload-source-maps');
});

Expand Down Expand Up @@ -184,7 +185,7 @@ describe('sentrySvelteKit()', () => {
// just to ignore the source maps plugin:
autoUploadSourceMaps: false,
});
const plugin = plugins[0];
const plugin = plugins[0]!;

expect(plugin.name).toEqual('sentry-auto-instrumentation');
expect(makePluginSpy).toHaveBeenCalledWith({
Expand Down
Loading
Loading