diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index a911a89fdd09..f65603565285 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -24,11 +24,13 @@ "@sentry/node": "7.42.0", "@sentry/svelte": "7.42.0", "@sentry/types": "7.42.0", - "@sentry/utils": "7.42.0" + "@sentry/utils": "7.42.0", + "magic-string": "^0.30.0" }, "devDependencies": { - "@sveltejs/kit": "^1.10.0", - "vite": "^4.0.0" + "@sveltejs/kit": "^1.5.0", + "vite": "4.0.0", + "typescript": "^4.9.3" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/sveltekit/src/config/index.ts b/packages/sveltekit/src/config/index.ts new file mode 100644 index 000000000000..5648cfb6bffc --- /dev/null +++ b/packages/sveltekit/src/config/index.ts @@ -0,0 +1 @@ +export { withSentryViteConfig } from './withSentryViteConfig'; diff --git a/packages/sveltekit/src/config/vitePlugins.ts b/packages/sveltekit/src/config/vitePlugins.ts new file mode 100644 index 000000000000..49df160b718a --- /dev/null +++ b/packages/sveltekit/src/config/vitePlugins.ts @@ -0,0 +1,73 @@ +import { logger } from '@sentry/utils'; +import * as fs from 'fs'; +import MagicString from 'magic-string'; +import * as path from 'path'; +import type { Plugin, TransformResult } from 'vite'; + +const serverIndexFilePath = path.join('@sveltejs', 'kit', 'src', 'runtime', 'server', 'index.js'); +const devClientAppFilePath = path.join('generated', 'client', 'app.js'); +const prodClientAppFilePath = path.join('generated', 'client-optimized', 'app.js'); + +/** + * This plugin injects the `Sentry.init` calls from `sentry.(client|server).config.(ts|js)` + * into SvelteKit runtime files. + */ +export const injectSentryInitPlugin: Plugin = { + name: 'sentry-init-injection-plugin', + + // In this hook, we inject the `Sentry.init` calls from `sentry.(client|server).config.(ts|js)` + // into SvelteKit runtime files: For the server, we inject it into the server's `index.js` + // file. For the client, we use the `_app.js` file. + transform(code, id) { + if (id.endsWith(serverIndexFilePath)) { + logger.debug('Injecting Server Sentry.init into', id); + return addSentryConfigFileImport('server', code, id) || code; + } + + if (id.endsWith(devClientAppFilePath) || id.endsWith(prodClientAppFilePath)) { + logger.debug('Injecting Client Sentry.init into', id); + return addSentryConfigFileImport('client', code, id) || code; + } + + return code; + }, + + // This plugin should run as early as possible, + // setting `enforce: 'pre'` ensures that it runs before the built-in vite plugins. + // see: https://vitejs.dev/guide/api-plugin.html#plugin-ordering + enforce: 'pre', +}; + +function addSentryConfigFileImport( + platform: 'server' | 'client', + originalCode: string, + entryFileId: string, +): TransformResult | undefined { + const projectRoot = process.cwd(); + const sentryConfigFilename = getUserConfigFile(projectRoot, platform); + + if (!sentryConfigFilename) { + logger.error(`Could not find sentry.${platform}.config.(ts|js) file.`); + return undefined; + } + + const filePath = path.join(path.relative(path.dirname(entryFileId), projectRoot), sentryConfigFilename); + const importStmt = `\nimport "${filePath}";`; + + const ms = new MagicString(originalCode); + ms.append(importStmt); + + return { code: ms.toString(), map: ms.generateMap() }; +} + +function getUserConfigFile(projectDir: string, platform: 'server' | 'client'): string | undefined { + const possibilities = [`sentry.${platform}.config.ts`, `sentry.${platform}.config.js`]; + + for (const filename of possibilities) { + if (fs.existsSync(path.resolve(projectDir, filename))) { + return filename; + } + } + + throw new Error(`Cannot find '${possibilities[0]}' or '${possibilities[1]}' in '${projectDir}'.`); +} diff --git a/packages/sveltekit/src/config/withSentryViteConfig.ts b/packages/sveltekit/src/config/withSentryViteConfig.ts new file mode 100644 index 000000000000..a5fabf3241ee --- /dev/null +++ b/packages/sveltekit/src/config/withSentryViteConfig.ts @@ -0,0 +1,52 @@ +import type { UserConfig, UserConfigExport } from 'vite'; + +import { injectSentryInitPlugin } from './vitePlugins'; + +/** + * This function adds Sentry-specific configuration to your Vite config. + * Pass your config to this function and make sure the return value is exported + * from your `vite.config.js` file. + * + * Note: If you're already wrapping your config with another wrapper, + * for instance with `defineConfig` from vitest, make sure + * that the Sentry wrapper is the outermost one. + * + * @param originalConfig your original vite config + * + * @returns a vite config with Sentry-specific configuration added to it. + */ +export function withSentryViteConfig(originalConfig: UserConfigExport): UserConfigExport { + if (typeof originalConfig === 'function') { + return function (this: unknown, ...viteConfigFunctionArgs: unknown[]): UserConfig | Promise { + const userViteConfigObject = originalConfig.apply(this, viteConfigFunctionArgs); + if (userViteConfigObject instanceof Promise) { + return userViteConfigObject.then(userConfig => addSentryConfig(userConfig)); + } + return addSentryConfig(userViteConfigObject); + }; + } else if (originalConfig instanceof Promise) { + return originalConfig.then(userConfig => addSentryConfig(userConfig)); + } + return addSentryConfig(originalConfig); +} + +function addSentryConfig(originalConfig: UserConfig): UserConfig { + const config = { + ...originalConfig, + plugins: originalConfig.plugins ? [injectSentryInitPlugin, ...originalConfig.plugins] : [injectSentryInitPlugin], + }; + + const mergedDevServerFileSystemConfig: UserConfig['server'] = { + fs: { + ...(config.server && config.server.fs), + allow: [...((config.server && config.server.fs && config.server.fs.allow) || []), '.'], + }, + }; + + config.server = { + ...config.server, + ...mergedDevServerFileSystemConfig, + }; + + return config; +} diff --git a/packages/sveltekit/src/index.server.ts b/packages/sveltekit/src/index.server.ts index 9bdd72ae4f02..acedc021218b 100644 --- a/packages/sveltekit/src/index.server.ts +++ b/packages/sveltekit/src/index.server.ts @@ -1,4 +1,5 @@ export * from './server'; +export * from './config'; // This file is the main entrypoint on the server and/or when the package is `require`d diff --git a/packages/sveltekit/src/index.types.ts b/packages/sveltekit/src/index.types.ts index f2eccf6ffb92..d4c7a9e52c25 100644 --- a/packages/sveltekit/src/index.types.ts +++ b/packages/sveltekit/src/index.types.ts @@ -4,6 +4,7 @@ // Some of the exports collide, which is not allowed, unless we redifine the colliding // exports in this file - which we do below. export * from './client'; +export * from './config'; export * from './server'; import type { Integration, Options, StackParser } from '@sentry/types'; diff --git a/packages/sveltekit/test/config/vitePlugins.test.ts b/packages/sveltekit/test/config/vitePlugins.test.ts new file mode 100644 index 000000000000..9e7c3ec2d788 --- /dev/null +++ b/packages/sveltekit/test/config/vitePlugins.test.ts @@ -0,0 +1,58 @@ +import * as fs from 'fs'; + +import { injectSentryInitPlugin } from '../../src/config/vitePlugins'; + +describe('injectSentryInitPlugin', () => { + it('has its basic properties set', () => { + expect(injectSentryInitPlugin.name).toBe('sentry-init-injection-plugin'); + expect(injectSentryInitPlugin.enforce).toBe('pre'); + expect(typeof injectSentryInitPlugin.transform).toBe('function'); + }); + + describe('tansform', () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + + it('transforms the server index file', () => { + const code = 'foo();'; + const id = '/node_modules/@sveltejs/kit/src/runtime/server/index.js'; + + // @ts-ignore -- transform is definitely defined and callable. Seems like TS doesn't know that. + const result = injectSentryInitPlugin.transform(code, id); + + expect(result.code).toMatch(/foo\(\);\n.*import ".*sentry\.server\.config\.ts";/gm); + expect(result.map).toBeDefined(); + }); + + it('transforms the client index file (dev server)', () => { + const code = 'foo();'; + const id = '.svelte-kit/generated/client/app.js'; + + // @ts-ignore -- transform is definitely defined and callable. Seems like TS doesn't know that. + const result = injectSentryInitPlugin.transform(code, id); + + expect(result.code).toMatch(/foo\(\);\n.*import ".*sentry\.client\.config\.ts";/gm); + expect(result.map).toBeDefined(); + }); + + it('transforms the client index file (prod build)', () => { + const code = 'foo();'; + const id = '.svelte-kit/generated/client-optimized/app.js'; + + // @ts-ignore -- transform is definitely defined and callable. Seems like TS doesn't know that. + const result = injectSentryInitPlugin.transform(code, id); + + expect(result.code).toMatch(/foo\(\);\n.*import ".*sentry\.client\.config\.ts";/gm); + expect(result.map).toBeDefined(); + }); + + it("doesn't transform other files", () => { + const code = 'foo();'; + const id = './src/routes/+page.ts'; + + // @ts-ignore -- transform is definitely defined and callable. Seems like TS doesn't know that. + const result = injectSentryInitPlugin.transform(code, id); + + expect(result).toBe(code); + }); + }); +}); diff --git a/packages/sveltekit/test/config/withSentryViteConfig.test.ts b/packages/sveltekit/test/config/withSentryViteConfig.test.ts new file mode 100644 index 000000000000..1ed7cf5d3d51 --- /dev/null +++ b/packages/sveltekit/test/config/withSentryViteConfig.test.ts @@ -0,0 +1,118 @@ +import type { Plugin, UserConfig } from 'vite'; + +import { withSentryViteConfig } from '../../src/config/withSentryViteConfig'; + +describe('withSentryViteConfig', () => { + const originalConfig = { + plugins: [{ name: 'foo' }], + server: { + fs: { + allow: ['./bar'], + }, + }, + test: { + include: ['src/**/*.{test,spec}.{js,ts}'], + }, + }; + + it('takes a POJO Vite config and returns the sentrified version', () => { + const sentrifiedConfig = withSentryViteConfig(originalConfig); + + expect(typeof sentrifiedConfig).toBe('object'); + + const plugins = (sentrifiedConfig as UserConfig).plugins as Plugin[]; + + expect(plugins).toHaveLength(2); + expect(plugins[0].name).toBe('sentry-init-injection-plugin'); + expect(plugins[1].name).toBe('foo'); + + expect((sentrifiedConfig as UserConfig).server?.fs?.allow).toStrictEqual(['./bar', '.']); + + expect((sentrifiedConfig as any).test).toEqual(originalConfig.test); + }); + + it('takes a Vite config Promise and returns the sentrified version', async () => { + const sentrifiedConfig = await withSentryViteConfig(Promise.resolve(originalConfig)); + + expect(typeof sentrifiedConfig).toBe('object'); + + const plugins = (sentrifiedConfig as UserConfig).plugins as Plugin[]; + + expect(plugins).toHaveLength(2); + expect(plugins[0].name).toBe('sentry-init-injection-plugin'); + expect(plugins[1].name).toBe('foo'); + + expect((sentrifiedConfig as UserConfig).server?.fs?.allow).toStrictEqual(['./bar', '.']); + + expect((sentrifiedConfig as any).test).toEqual(originalConfig.test); + }); + + it('takes a function returning a Vite config and returns the sentrified version', () => { + const sentrifiedConfigFunction = withSentryViteConfig(_env => { + return originalConfig; + }); + const sentrifiedConfig = + typeof sentrifiedConfigFunction === 'function' && sentrifiedConfigFunction({ command: 'build', mode: 'test' }); + + expect(typeof sentrifiedConfig).toBe('object'); + + const plugins = (sentrifiedConfig as UserConfig).plugins as Plugin[]; + + expect(plugins).toHaveLength(2); + expect(plugins[0].name).toBe('sentry-init-injection-plugin'); + expect(plugins[1].name).toBe('foo'); + + expect((sentrifiedConfig as UserConfig).server?.fs?.allow).toStrictEqual(['./bar', '.']); + + expect((sentrifiedConfig as any).test).toEqual(originalConfig.test); + }); + + it('takes a function returning a Vite config promise and returns the sentrified version', async () => { + const sentrifiedConfigFunction = withSentryViteConfig(_env => { + return Promise.resolve(originalConfig); + }); + const sentrifiedConfig = + typeof sentrifiedConfigFunction === 'function' && + (await sentrifiedConfigFunction({ command: 'build', mode: 'test' })); + + expect(typeof sentrifiedConfig).toBe('object'); + + const plugins = (sentrifiedConfig as UserConfig).plugins as Plugin[]; + + expect(plugins).toHaveLength(2); + expect(plugins[0].name).toBe('sentry-init-injection-plugin'); + expect(plugins[1].name).toBe('foo'); + + expect((sentrifiedConfig as UserConfig).server?.fs?.allow).toStrictEqual(['./bar', '.']); + + expect((sentrifiedConfig as any).test).toEqual(originalConfig.test); + }); + + it('adds the vite plugin if no plugins are present', () => { + const sentrifiedConfig = withSentryViteConfig({ + test: { + include: ['src/**/*.{test,spec}.{js,ts}'], + }, + } as UserConfig); + + expect(typeof sentrifiedConfig).toBe('object'); + + const plugins = (sentrifiedConfig as UserConfig).plugins as Plugin[]; + + expect(plugins).toHaveLength(1); + expect(plugins[0].name).toBe('sentry-init-injection-plugin'); + }); + + it('adds the vite plugin and server config to an empty vite config', () => { + const sentrifiedConfig = withSentryViteConfig({}); + + expect(typeof sentrifiedConfig).toBe('object'); + + const plugins = (sentrifiedConfig as UserConfig).plugins as Plugin[]; + + expect(plugins).toHaveLength(1); + expect(plugins[0].name).toBe('sentry-init-injection-plugin'); + + expect((sentrifiedConfig as UserConfig).server?.fs?.allow).toStrictEqual(['.']); + }); +}); diff --git a/yarn.lock b/yarn.lock index 77998803e637..803d62922290 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4205,10 +4205,10 @@ dependencies: highlight.js "^9.15.6" -"@sveltejs/kit@^1.10.0": - version "1.10.0" - resolved "https://registry.yarnpkg.com/@sveltejs/kit/-/kit-1.10.0.tgz#17d3565e5903f6d2c0730197fd875c2cf921ad01" - integrity sha512-0P35zHrByfbF3Ym3RdQL+RvzgsCDSyO3imSwuZ67XAD5HoCQFF3a8Mhh0V3sObz3rc5aJd4Qn82UpAihJqZ6gQ== +"@sveltejs/kit@^1.5.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@sveltejs/kit/-/kit-1.11.0.tgz#23f233c351e5956356ba6f3206e40637c5f5dbda" + integrity sha512-PwViZcMoLgEU/jhLoSyjf5hSrHS67wvSm0ifBo4prP9irpGa5HuPOZeVDTL5tPDSBoKxtdYi1zlGdoiJfO86jA== dependencies: "@sveltejs/vite-plugin-svelte" "^2.0.0" "@types/cookie" "^0.5.1" @@ -12057,7 +12057,7 @@ esbuild@0.13.8: esbuild-windows-64 "0.13.8" esbuild-windows-arm64 "0.13.8" -esbuild@^0.16.14: +esbuild@^0.16.3: version "0.16.17" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.16.17.tgz#fc2c3914c57ee750635fee71b89f615f25065259" integrity sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg== @@ -21687,7 +21687,7 @@ postcss@^8.1.10, postcss@^8.1.7, postcss@^8.2.15: picocolors "^1.0.0" source-map-js "^1.0.2" -postcss@^8.2.4, postcss@^8.3.5, postcss@^8.3.7, postcss@^8.4.21: +postcss@^8.2.4, postcss@^8.3.5, postcss@^8.3.7, postcss@^8.4.19: version "8.4.21" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.21.tgz#c639b719a57efc3187b13a1d765675485f4134f4" integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg== @@ -23284,7 +23284,7 @@ rollup@^2.45.1: optionalDependencies: fsevents "~2.3.2" -rollup@^3.10.0: +rollup@^3.7.0: version "3.18.0" resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.18.0.tgz#2354ba63ba66d6a09c652c3ea0dbcd9dad72bbde" integrity sha512-J8C6VfEBjkvYPESMQYxKHxNOh4A5a3FlP+0BETGo34HEcE4eTlgCrO2+eWzlu2a/sHs2QUkZco+wscH7jhhgWg== @@ -25983,7 +25983,7 @@ typescript@^3.9.5, typescript@^3.9.7: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.9.tgz#e69905c54bc0681d0518bd4d587cc6f2d0b1a674" integrity sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w== -typescript@^4.9.4: +typescript@^4.9.3, typescript@^4.9.4: version "4.9.5" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== @@ -26518,15 +26518,15 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" -vite@^4.0.0: - version "4.1.4" - resolved "https://registry.yarnpkg.com/vite/-/vite-4.1.4.tgz#170d93bcff97e0ebc09764c053eebe130bfe6ca0" - integrity sha512-3knk/HsbSTKEin43zHu7jTwYWv81f8kgAL99G5NWBcA1LKvtvcVAC4JjBH1arBunO9kQka+1oGbrMKOjk4ZrBg== +vite@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/vite/-/vite-4.0.0.tgz#b81b88349a06b2faaa53ae14cf96c942548e3454" + integrity sha512-ynad+4kYs8Jcnn8J7SacS9vAbk7eMy0xWg6E7bAhS1s79TK+D7tVFGXVZ55S7RNLRROU1rxoKlvZ/qjaB41DGA== dependencies: - esbuild "^0.16.14" - postcss "^8.4.21" + esbuild "^0.16.3" + postcss "^8.4.19" resolve "^1.22.1" - rollup "^3.10.0" + rollup "^3.7.0" optionalDependencies: fsevents "~2.3.2"