-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(react-router): Add build-time config (#15406)
- Adds a vite plugin for react router that handles: - Updating sourcemap settings - Release injection - Telemetry Data - Adds a sentryOnBuildEnd hook that handles: - Creating releases - DebugId injection - Uploading sourcemaps - Deleting sourcemaps after upload closes #15188 --------- Co-authored-by: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com>
- Loading branch information
Showing
15 changed files
with
1,630 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
export * from './server'; | ||
export * from './vite'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
113 changes: 113 additions & 0 deletions
113
packages/react-router/src/vite/buildEnd/handleOnBuildEnd.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import { rm } from 'node:fs/promises'; | ||
import type { Config } from '@react-router/dev/dist/config'; | ||
import SentryCli from '@sentry/cli'; | ||
import { glob } from 'glob'; | ||
import type { SentryReactRouterBuildOptions } from '../types'; | ||
|
||
type BuildEndHook = NonNullable<Config['buildEnd']>; | ||
|
||
function getSentryConfig(viteConfig: unknown): SentryReactRouterBuildOptions { | ||
if (!viteConfig || typeof viteConfig !== 'object' || !('sentryConfig' in viteConfig)) { | ||
// eslint-disable-next-line no-console | ||
console.error('[Sentry] sentryConfig not found - it needs to be passed to vite.config.ts'); | ||
} | ||
|
||
return (viteConfig as { sentryConfig: SentryReactRouterBuildOptions }).sentryConfig; | ||
} | ||
|
||
/** | ||
* 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, | ||
* and optionally deletes the source map files after upload. | ||
*/ | ||
export const sentryOnBuildEnd: BuildEndHook = async ({ reactRouterConfig, viteConfig }) => { | ||
const { | ||
authToken, | ||
org, | ||
project, | ||
release, | ||
sourceMapsUploadOptions = { enabled: true }, | ||
debug = false, | ||
} = getSentryConfig(viteConfig); | ||
|
||
const cliInstance = new SentryCli(null, { | ||
authToken, | ||
org, | ||
project, | ||
}); | ||
// check if release should be created | ||
if (release?.name) { | ||
try { | ||
await cliInstance.releases.new(release.name); | ||
} catch (error) { | ||
// eslint-disable-next-line no-console | ||
console.error('[Sentry] Could not create release', error); | ||
} | ||
} | ||
|
||
if (sourceMapsUploadOptions?.enabled ?? (true && viteConfig.build.sourcemap !== false)) { | ||
// inject debugIds | ||
try { | ||
await cliInstance.execute(['sourcemaps', 'inject', reactRouterConfig.buildDirectory], debug); | ||
} catch (error) { | ||
// eslint-disable-next-line no-console | ||
console.error('[Sentry] Could not inject debug ids', error); | ||
} | ||
|
||
// upload sourcemaps | ||
try { | ||
await cliInstance.releases.uploadSourceMaps(release?.name || 'undefined', { | ||
include: [ | ||
{ | ||
paths: [reactRouterConfig.buildDirectory], | ||
}, | ||
], | ||
}); | ||
} catch (error) { | ||
// eslint-disable-next-line no-console | ||
console.error('[Sentry] Could not upload sourcemaps', error); | ||
} | ||
} | ||
// delete sourcemaps after upload | ||
let updatedFilesToDeleteAfterUpload = sourceMapsUploadOptions?.filesToDeleteAfterUpload; | ||
// set a default value no option was set | ||
if (typeof sourceMapsUploadOptions?.filesToDeleteAfterUpload === 'undefined') { | ||
updatedFilesToDeleteAfterUpload = [`${reactRouterConfig.buildDirectory}/**/*.map`]; | ||
if (debug) { | ||
// eslint-disable-next-line no-console | ||
console.info( | ||
`[Sentry] Automatically setting \`sourceMapsUploadOptions.filesToDeleteAfterUpload: ${JSON.stringify( | ||
updatedFilesToDeleteAfterUpload, | ||
)}\` to delete generated source maps after they were uploaded to Sentry.`, | ||
); | ||
} | ||
} | ||
if (updatedFilesToDeleteAfterUpload) { | ||
try { | ||
const filePathsToDelete = await glob(updatedFilesToDeleteAfterUpload, { | ||
absolute: true, | ||
nodir: true, | ||
}); | ||
if (debug) { | ||
filePathsToDelete.forEach(filePathToDelete => { | ||
// eslint-disable-next-line no-console | ||
console.info(`Deleting asset after upload: ${filePathToDelete}`); | ||
}); | ||
} | ||
await Promise.all( | ||
filePathsToDelete.map(filePathToDelete => | ||
rm(filePathToDelete, { force: true }).catch((e: unknown) => { | ||
if (debug) { | ||
// This is allowed to fail - we just don't do anything | ||
// eslint-disable-next-line no-console | ||
console.debug(`An error occurred while attempting to delete asset: ${filePathToDelete}`, e); | ||
} | ||
}), | ||
), | ||
); | ||
} catch (error) { | ||
// eslint-disable-next-line no-console | ||
console.error('Error deleting files after sourcemap upload:', error); | ||
} | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export { sentryReactRouter } from './plugin'; | ||
export { sentryOnBuildEnd } from './buildEnd/handleOnBuildEnd'; | ||
export type { SentryReactRouterBuildOptions } from './types'; |
37 changes: 37 additions & 0 deletions
37
packages/react-router/src/vite/makeCustomSentryVitePlugins.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { sentryVitePlugin } from '@sentry/vite-plugin'; | ||
import { type Plugin } from 'vite'; | ||
import type { SentryReactRouterBuildOptions } from './types'; | ||
|
||
/** | ||
* Create a custom subset of sentry's vite plugins | ||
*/ | ||
export async function makeCustomSentryVitePlugins(options: SentryReactRouterBuildOptions): Promise<Plugin[]> { | ||
const { debug, unstable_sentryVitePluginOptions, bundleSizeOptimizations, authToken, org, project, telemetry } = | ||
options; | ||
|
||
const sentryVitePlugins = sentryVitePlugin({ | ||
authToken: authToken ?? process.env.SENTRY_AUTH_TOKEN, | ||
bundleSizeOptimizations, | ||
debug: debug ?? false, | ||
org: org ?? process.env.SENTRY_ORG, | ||
project: project ?? process.env.SENTRY_PROJECT, | ||
telemetry: telemetry ?? true, | ||
_metaOptions: { | ||
telemetry: { | ||
metaFramework: 'react-router', | ||
}, | ||
}, | ||
// will be handled in buildEnd hook | ||
sourcemaps: { | ||
disable: true, | ||
}, | ||
...unstable_sentryVitePluginOptions, | ||
}) as Plugin[]; | ||
|
||
// only use a subset of the plugins as all upload and file deletion tasks will be handled in the buildEnd hook | ||
return [ | ||
...sentryVitePlugins.filter(plugin => { | ||
return ['sentry-telemetry-plugin', 'sentry-vite-release-injection-plugin'].includes(plugin.name); | ||
}), | ||
]; | ||
} |
83 changes: 83 additions & 0 deletions
83
packages/react-router/src/vite/makeEnableSourceMapsPlugin.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import { consoleSandbox } from '@sentry/core'; | ||
import type { Plugin, UserConfig } from 'vite'; | ||
import type { SentryReactRouterBuildOptions } from './types'; | ||
|
||
/** | ||
* A Sentry plugin for React Router to enable "hidden" source maps if they are unset. | ||
*/ | ||
export function makeEnableSourceMapsPlugin(options: SentryReactRouterBuildOptions): Plugin { | ||
return { | ||
name: 'sentry-react-router-update-source-map-setting', | ||
apply: 'build', | ||
enforce: 'post', | ||
config(viteConfig) { | ||
return { | ||
...viteConfig, | ||
build: { | ||
...viteConfig.build, | ||
sourcemap: getUpdatedSourceMapSettings(viteConfig, options), | ||
}, | ||
}; | ||
}, | ||
}; | ||
} | ||
|
||
/** There are 3 ways to set up source map generation | ||
* | ||
* 1. User explicitly disabled source maps | ||
* - 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'). | ||
* - 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) | ||
* | ||
* --> only exported for testing | ||
*/ | ||
export function getUpdatedSourceMapSettings( | ||
viteConfig: UserConfig, | ||
sentryPluginOptions?: SentryReactRouterBuildOptions, | ||
): boolean | 'inline' | 'hidden' { | ||
viteConfig.build = viteConfig.build || {}; | ||
|
||
const viteSourceMap = viteConfig?.build?.sourcemap; | ||
let updatedSourceMapSetting = viteSourceMap; | ||
|
||
const settingKey = 'vite.build.sourcemap'; | ||
|
||
if (viteSourceMap === false) { | ||
updatedSourceMapSetting = viteSourceMap; | ||
|
||
consoleSandbox(() => { | ||
// 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 if (viteSourceMap && ['hidden', 'inline', true].includes(viteSourceMap)) { | ||
updatedSourceMapSetting = viteSourceMap; | ||
|
||
if (sentryPluginOptions?.debug) { | ||
consoleSandbox(() => { | ||
// 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'; | ||
|
||
consoleSandbox(() => { | ||
// 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 updatedSourceMapSetting; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import type { ConfigEnv } from 'vite'; | ||
import { type Plugin } from 'vite'; | ||
import { makeCustomSentryVitePlugins } from './makeCustomSentryVitePlugins'; | ||
import { makeEnableSourceMapsPlugin } from './makeEnableSourceMapsPlugin'; | ||
import type { SentryReactRouterBuildOptions } from './types'; | ||
|
||
/** | ||
* A Vite plugin for Sentry that handles source map uploads and bundle size optimizations. | ||
* | ||
* @param options - Configuration options for the Sentry Vite plugin | ||
* @param viteConfig - The Vite user config object | ||
* @returns An array of Vite plugins | ||
*/ | ||
export async function sentryReactRouter( | ||
options: SentryReactRouterBuildOptions = {}, | ||
config: ConfigEnv, | ||
): Promise<Plugin[]> { | ||
const plugins: Plugin[] = []; | ||
|
||
if (process.env.NODE_ENV !== 'development' && config.command === 'build' && config.mode !== 'development') { | ||
plugins.push(makeEnableSourceMapsPlugin(options)); | ||
plugins.push(...(await makeCustomSentryVitePlugins(options))); | ||
} | ||
|
||
return plugins; | ||
} |
Oops, something went wrong.