Skip to content
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
122 changes: 98 additions & 24 deletions packages/tanstackstart-react/src/vite/sourceMaps.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { sentryVitePlugin } from '@sentry/bundler-plugins/vite';
import * as path from 'node:path';
import type { Plugin, UserConfig } from 'vite';
import type { BuildEnvironmentOptions, Plugin, ResolvedConfig, UserConfig } from 'vite';
import type { SentryTanstackStartOptions } from './sentryTanstackStart';

type FilesToDeleteAfterUpload = string | string[] | undefined;
Expand Down Expand Up @@ -30,6 +30,12 @@ export function rewriteTanstackStartSource(source: string, _map: unknown, contex
return path.relative(process.cwd(), absolutePath).split(path.sep).join('/');
}

/**
* Tells the plugin that deletes source maps after the upload whether the source maps of an environment are only
* there because Sentry enabled them.
*/
type SourceMapsEnablerApi = { hasSentryEnabledSourceMaps(environmentName: string): boolean };

/**
* A Sentry plugin for adding the @sentry/bundler-plugins/vite to automatically upload source maps to Sentry.
*/
Expand Down Expand Up @@ -62,21 +68,29 @@ export function makeAddSentryVitePlugin(options: SentryTanstackStartOptions): Pl
name: 'sentry-tanstackstart-files-to-delete-after-upload-plugin',
apply: 'build',
enforce: 'post',
config(config) {
const userFilesToDelete = sourcemaps?.filesToDeleteAfterUpload;

// Only auto-delete source maps if the user didn't configure sourcemaps at all
if (typeof userFilesToDelete === 'undefined' && typeof config.build?.sourcemap === 'undefined') {
if (debug) {
// eslint-disable-next-line no-console
console.log(
'[Sentry] Automatically setting `sourcemaps.filesToDeleteAfterUpload: ["./**/*.map"]` to delete generated source maps after they were uploaded to Sentry.',
);
}
resolveFilesToDeleteAfterUpload?.(['./**/*.map']);
} else {
resolveFilesToDeleteAfterUpload?.(userFilesToDelete);
configResolved(config) {
if (sourcemaps?.filesToDeleteAfterUpload !== undefined) {
resolveFilesToDeleteAfterUpload?.(sourcemaps.filesToDeleteAfterUpload);
return;
}

if (!isVite8OrLater(this.meta.viteVersion)) {
// eslint-disable-next-line no-console
console.warn(
`[Sentry] TanStack Start requires Vite 8 or later. With an older Vite, Sentry deletes the source maps it generated only in \`build.outDir\` of each environment, so source maps in other output directories can stay in the build output.`,
);
}

const filesToDelete = getSentryEnabledSourceMapGlobs(config);
if (debug) {
// eslint-disable-next-line no-console
console.log(
filesToDelete.length > 0
? `[Sentry] Automatically setting \`sourcemaps.filesToDeleteAfterUpload: ${JSON.stringify(filesToDelete)}\` to delete generated source maps after they were uploaded to Sentry.`
: '[Sentry] No source maps are deleted after the upload, because Sentry did not enable source map generation for this build.',
);
}
resolveFilesToDeleteAfterUpload?.(filesToDelete);
},
};

Expand Down Expand Up @@ -116,21 +130,72 @@ export function makeAddSentryVitePlugin(options: SentryTanstackStartOptions): Pl
return [configPlugin, ...sentryPlugins];
}

/**
* Globs for the source maps in the output directories that only have source maps because Sentry enabled them. A glob
* such as `./**\/*.map` does not enter dot directories like `.output`, where Nitro puts the client build.
*/
function getSentryEnabledSourceMapGlobs(config: ResolvedConfig): string[] {
const outDirs = Object.entries(config.environments)
.filter(([name]) => hasSentryEnabledSourceMaps(config, name))
.filter(([, environment]) => !hasOutputSourceMapSetting(environment.build))
.flatMap(([, environment]) => getOutputDirs(config.root, environment.build));

return [...new Set(outDirs)].map(outDir => {
Comment thread
s1gr1d marked this conversation as resolved.
const relativeOutDir = path.relative(process.cwd(), outDir).split(path.sep).join('/');
return `${relativeOutDir || '.'}/**/*.map`;
});
}

/** `output.dir` takes precedence over `build.outDir`. Nitro sets only `output.dir` for its server build. */
function getOutputDirs(root: string, build: ResolvedConfig['environments'][string]['build']): string[] {
// Vite 7 has no `rolldownOptions`.
const outputs = [build.rolldownOptions?.output ?? {}].flat();
return outputs.map(output => path.resolve(root, output.dir ?? build.outDir));
}

/** `output.sourcemap` of the bundler config overrides `build.sourcemap` in the build output. */
function hasOutputSourceMapSetting(build: Pick<BuildEnvironmentOptions, 'rolldownOptions'> | undefined): boolean {
return [build?.rolldownOptions?.output ?? []].flat().some(output => output.sourcemap !== undefined);
}

/** Vite tells plugins its version since Vite 7. */
function isVite8OrLater(viteVersion: string | undefined): boolean {
return Number(viteVersion?.split('.')[0]) >= 8;
}

function hasSentryEnabledSourceMaps(config: ResolvedConfig, environmentName: string): boolean {
return config.plugins.some(plugin =>
(plugin.api as Partial<SourceMapsEnablerApi> | undefined)?.hasSentryEnabledSourceMaps?.(environmentName),
);
}

/**
* A Sentry plugin for TanStack Start React to enable "hidden" source maps if they are unset.
*/
export function makeEnableSourceMapsVitePlugin(options: SentryTanstackStartOptions): Plugin[] {
let enabledSourceMaps = false;
let environmentsWithSourceMapSetting: string[] = [];

const api: SourceMapsEnablerApi = {
hasSentryEnabledSourceMaps: environmentName =>
enabledSourceMaps && !environmentsWithSourceMapSetting.includes(environmentName),
};

return [
{
name: 'sentry-tanstackstart-react-source-maps',
apply: 'build',
enforce: 'post',
api,
config(viteConfig) {
return {
build: {
sourcemap: getUpdatedSourceMapSettings(viteConfig, options),
},
};
const sourcemap = getUpdatedSourceMapSettings(viteConfig, options);
enabledSourceMaps = sourcemap !== viteConfig.build?.sourcemap;
// The setting of an environment overrides the top-level setting, e.g. `nitro/vite` sets it for its environments.
environmentsWithSourceMapSetting = Object.entries(viteConfig.environments ?? {})
.filter(([, environment]) => environment?.build?.sourcemap !== undefined)
.map(([name]) => name);

return { build: { sourcemap } };
},
},
];
Expand All @@ -142,19 +207,19 @@ export function makeEnableSourceMapsVitePlugin(options: SentryTanstackStartOptio
* - keep this setting (emit a warning that errors won't be unminified in Sentry)
* - We won't upload anything
*
* 2. Users enabled source map generation (true, 'hidden', 'inline').
* 2. Users enabled source map generation (true, 'hidden', 'inline') or set `build.rolldownOptions.output.sourcemap`.
* - keep this setting (don't do anything - like deletion - besides uploading)
*
* 3. Users didn't set source maps generation
* - we enable 'hidden' source maps generation
* - configure `filesToDeleteAfterUpload` to delete all .map files (we emit a log about this)
* - configure `filesToDeleteAfterUpload` to delete these .map files after the upload (we emit a log about this)
*
* --> only exported for testing
* @internal Only exported for testing.
*/
export function getUpdatedSourceMapSettings(
viteConfig: UserConfig,
sentryPluginOptions?: SentryTanstackStartOptions,
): boolean | 'inline' | 'hidden' {
): boolean | 'inline' | 'hidden' | undefined {
viteConfig.build = viteConfig.build || {};

const viteUserSourceMapSetting = viteConfig.build?.sourcemap;
Expand Down Expand Up @@ -183,6 +248,15 @@ export function getUpdatedSourceMapSettings(
}

return viteUserSourceMapSetting;
} else if (hasOutputSourceMapSetting(viteConfig.build)) {
if (debug) {
// eslint-disable-next-line no-console
console.log(
'[Sentry] We discovered `vite.build.rolldownOptions.output.sourcemap`, which overrides `vite.build.sourcemap`. Sentry will keep this source map setting.',
);
}

return undefined;
}

// If the user did not specify a source map setting, we enable 'hidden' by default
Expand Down
Loading
Loading