-
-
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(sveltekit): Add Sentry Vite Plugin to upload source maps (#7811)
Add a customized version of the Sentry Vite plugin to the SvelteKit SDK. Rework the SDK's build API: * Instead of having a super plugin that adds our other plugins (this doesn't work), we now export a factory function that returns an array of plugins. [SvelteKit also creates its plugins](https://github.com/Lms24/kit/blob/f7de9556319f652cabb89dd6f17b21e25326759c/packages/kit/src/exports/vite/index.js#L114-L143) this way. * The currently only plugin in this array is the customized Vite plugin. The customized Vite plugin differs from the Vite plugin as follows: * It only runs on builds (not on the dev server) * It tries to run as late as possible by setting `enforce: 'post'` * It uses the `closeBundle` hook instead of the `writeBundle` hook to upload source maps. * This is because the SvelteKit adapters also run only at closeBundle but luckily before our plugin * It uses the `configure` hook to enable source map generation * It flattens source maps before uploading them * We to flatten them (actually [`sorcery`](https://github.com/Rich-Harris/sorcery) does) to work around [weird source maps generation behaviour](sveltejs/kit#9608).
- Loading branch information
Showing
10 changed files
with
470 additions
and
71 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
export { sentrySvelteKitPlugin } from './sentrySvelteKitPlugin'; | ||
export { sentrySvelteKit } from './sentryVitePlugins'; |
This file was deleted.
Oops, something went wrong.
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,57 @@ | ||
import type { SentryVitePluginOptions } from '@sentry/vite-plugin'; | ||
import type { Plugin } from 'vite'; | ||
|
||
import { makeCustomSentryVitePlugin } from './sourceMaps'; | ||
|
||
type SourceMapsUploadOptions = { | ||
/** | ||
* If this flag is `true`, the Sentry plugins will automatically upload source maps to Sentry. | ||
* Defaults to `true`. | ||
*/ | ||
autoUploadSourceMaps?: boolean; | ||
|
||
/** | ||
* Options for the Sentry Vite plugin to customize and override the release creation and source maps upload process. | ||
* See [Sentry Vite Plugin Options](https://github.com/getsentry/sentry-javascript-bundler-plugins/tree/main/packages/vite-plugin#configuration) for a detailed description. | ||
*/ | ||
sourceMapsUploadOptions?: Partial<SentryVitePluginOptions>; | ||
}; | ||
|
||
export type SentrySvelteKitPluginOptions = { | ||
/** | ||
* If this flag is `true`, the Sentry plugins will log some useful debug information. | ||
* Defaults to `false`. | ||
*/ | ||
debug?: boolean; | ||
} & SourceMapsUploadOptions; | ||
|
||
const DEFAULT_PLUGIN_OPTIONS: SentrySvelteKitPluginOptions = { | ||
autoUploadSourceMaps: true, | ||
debug: false, | ||
}; | ||
|
||
/** | ||
* Vite Plugins for the Sentry SvelteKit SDK, taking care of creating | ||
* Sentry releases and uploading source maps to Sentry. | ||
* | ||
* Sentry adds a few additional properties to your Vite config. | ||
* Make sure, it is registered before the SvelteKit plugin. | ||
*/ | ||
export function sentrySvelteKit(options: SentrySvelteKitPluginOptions = {}): Plugin[] { | ||
const mergedOptions = { | ||
...DEFAULT_PLUGIN_OPTIONS, | ||
...options, | ||
}; | ||
|
||
const sentryPlugins = []; | ||
|
||
if (mergedOptions.autoUploadSourceMaps) { | ||
const pluginOptions = { | ||
...mergedOptions.sourceMapsUploadOptions, | ||
debug: mergedOptions.debug, // override the plugin's debug flag with the one from the top-level options | ||
}; | ||
sentryPlugins.push(makeCustomSentryVitePlugin(pluginOptions)); | ||
} | ||
|
||
return sentryPlugins; | ||
} |
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,151 @@ | ||
import type { SentryVitePluginOptions } from '@sentry/vite-plugin'; | ||
import { sentryVitePlugin } from '@sentry/vite-plugin'; | ||
import * as fs from 'fs'; | ||
import * as path from 'path'; | ||
// @ts-ignore -sorcery has no types :( | ||
// eslint-disable-next-line import/default | ||
import * as sorcery from 'sorcery'; | ||
import type { Plugin } from 'vite'; | ||
|
||
const DEFAULT_PLUGIN_OPTIONS: SentryVitePluginOptions = { | ||
// TODO: Read these values from the node adapter somehow as the out dir can be changed in the adapter options | ||
include: ['build/server', 'build/client'], | ||
}; | ||
|
||
// sorcery has no types, so these are some basic type definitions: | ||
type Chain = { | ||
write(): Promise<void>; | ||
apply(): Promise<void>; | ||
}; | ||
type Sorcery = { | ||
load(filepath: string): Promise<Chain>; | ||
}; | ||
|
||
type SentryVitePluginOptionsOptionalInclude = Omit<SentryVitePluginOptions, 'include'> & { | ||
include?: SentryVitePluginOptions['include']; | ||
}; | ||
|
||
/** | ||
* Creates a new Vite plugin that uses the unplugin-based Sentry Vite plugin to create | ||
* releases and upload source maps to Sentry. | ||
* | ||
* Because the unplugin-based Sentry Vite plugin doesn't work ootb with SvelteKit, | ||
* we need to add some additional stuff to make source maps work: | ||
* | ||
* - the `config` hook needs to be added to generate source maps | ||
* - the `configResolved` hook tells us when to upload source maps. | ||
* We only want to upload once at the end, given that SvelteKit makes multiple builds | ||
* - the `closeBundle` hook is used to flatten server source maps, which at the moment is necessary for SvelteKit. | ||
* After the maps are flattened, they're uploaded to Sentry as in the original plugin. | ||
* see: https://github.com/sveltejs/kit/discussions/9608 | ||
* | ||
* @returns the custom Sentry Vite plugin | ||
*/ | ||
export function makeCustomSentryVitePlugin(options?: SentryVitePluginOptionsOptionalInclude): Plugin { | ||
const mergedOptions = { | ||
...DEFAULT_PLUGIN_OPTIONS, | ||
...options, | ||
}; | ||
const sentryPlugin: Plugin = sentryVitePlugin(mergedOptions); | ||
|
||
const { debug } = mergedOptions; | ||
const { buildStart, resolveId, transform, renderChunk } = sentryPlugin; | ||
|
||
let upload = true; | ||
|
||
const customPlugin: Plugin = { | ||
name: 'sentry-vite-plugin-custom', | ||
apply: 'build', // only apply this plugin at build time | ||
enforce: 'post', | ||
|
||
// These hooks are copied from the original Sentry Vite plugin. | ||
// They're mostly responsible for options parsing and release injection. | ||
buildStart, | ||
resolveId, | ||
renderChunk, | ||
transform, | ||
|
||
// Modify the config to generate source maps | ||
config: config => { | ||
// eslint-disable-next-line no-console | ||
debug && console.log('[Source Maps Plugin] Enabeling source map generation'); | ||
return { | ||
...config, | ||
build: { | ||
...config.build, | ||
sourcemap: true, | ||
}, | ||
}; | ||
}, | ||
|
||
configResolved: config => { | ||
// The SvelteKit plugins trigger additional builds within the main (SSR) build. | ||
// We just need a mechanism to upload source maps only once. | ||
// `config.build.ssr` is `true` for that first build and `false` in the other ones. | ||
// Hence we can use it as a switch to upload source maps only once in main build. | ||
if (!config.build.ssr) { | ||
upload = false; | ||
} | ||
}, | ||
|
||
// We need to start uploading source maps later than in the original plugin | ||
// because SvelteKit is still doing some stuff at closeBundle. | ||
closeBundle: () => { | ||
if (!upload) { | ||
return; | ||
} | ||
|
||
// TODO: Read the out dir from the node adapter somehow as it can be changed in the adapter options | ||
const outDir = path.resolve(process.cwd(), 'build'); | ||
|
||
const jsFiles = getFiles(outDir).filter(file => file.endsWith('.js')); | ||
// eslint-disable-next-line no-console | ||
debug && console.log('[Source Maps Plugin] Flattening source maps'); | ||
|
||
jsFiles.forEach(async file => { | ||
try { | ||
await (sorcery as Sorcery).load(file).then(async chain => { | ||
if (!chain) { | ||
// We end up here, if we don't have a source map for the file. | ||
// This is fine, as we're not interested in files w/o source maps. | ||
return; | ||
} | ||
// This flattens the source map | ||
await chain.apply(); | ||
// Write it back to the original file | ||
await chain.write(); | ||
}); | ||
} catch (e) { | ||
// Sometimes sorcery fails to flatten the source map. While this isn't ideal, it seems to be mostly | ||
// happening in Kit-internal files which is fine as they're not in-app. | ||
// This mostly happens when sorcery tries to resolve a source map while flattening that doesn't exist. | ||
const isKnownError = e instanceof Error && e.message.includes('ENOENT: no such file or directory, open'); | ||
if (debug && !isKnownError) { | ||
// eslint-disable-next-line no-console | ||
console.error('[Source Maps Plugin] error while flattening', file, e); | ||
} | ||
} | ||
}); | ||
|
||
// @ts-ignore - this hook exists on the plugin! | ||
sentryPlugin.writeBundle(); | ||
}, | ||
}; | ||
|
||
return customPlugin; | ||
} | ||
|
||
function getFiles(dir: string): string[] { | ||
if (!fs.existsSync(dir)) { | ||
return []; | ||
} | ||
const dirents = fs.readdirSync(dir, { withFileTypes: true }); | ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
// @ts-ignore | ||
const files: string[] = dirents.map(dirent => { | ||
const resFileOrDir = path.resolve(dir, dirent.name); | ||
return dirent.isDirectory() ? getFiles(resFileOrDir) : resFileOrDir; | ||
}); | ||
|
||
return Array.prototype.concat(...files); | ||
} |
12 changes: 0 additions & 12 deletions
12
packages/sveltekit/test/vite/sentrySvelteKitPlugin.test.ts
This file was deleted.
Oops, something went wrong.
41 changes: 41 additions & 0 deletions
41
packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.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,41 @@ | ||
import { vi } from 'vitest'; | ||
|
||
import { sentrySvelteKit } from '../../src/vite/sentryVitePlugins'; | ||
import * as sourceMaps from '../../src/vite/sourceMaps'; | ||
|
||
describe('sentryVite()', () => { | ||
it('returns an array of Vite plugins', () => { | ||
const plugins = sentrySvelteKit(); | ||
expect(plugins).toBeInstanceOf(Array); | ||
expect(plugins).toHaveLength(1); | ||
}); | ||
|
||
it('returns the custom sentry source maps plugin by default', () => { | ||
const plugins = sentrySvelteKit(); | ||
const plugin = plugins[0]; | ||
expect(plugin.name).toEqual('sentry-vite-plugin-custom'); | ||
}); | ||
|
||
it("doesn't return the custom sentry source maps plugin if autoUploadSourcemaps is `false`", () => { | ||
const plugins = sentrySvelteKit({ autoUploadSourceMaps: false }); | ||
expect(plugins).toHaveLength(0); | ||
}); | ||
|
||
it('passes user-specified vite pugin options to the custom sentry source maps plugin', () => { | ||
const makePluginSpy = vi.spyOn(sourceMaps, 'makeCustomSentryVitePlugin'); | ||
const plugins = sentrySvelteKit({ | ||
debug: true, | ||
sourceMapsUploadOptions: { | ||
include: ['foo.js'], | ||
ignore: ['bar.js'], | ||
}, | ||
}); | ||
const plugin = plugins[0]; | ||
expect(plugin.name).toEqual('sentry-vite-plugin-custom'); | ||
expect(makePluginSpy).toHaveBeenCalledWith({ | ||
debug: true, | ||
ignore: ['bar.js'], | ||
include: ['foo.js'], | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.