Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
8 changes: 5 additions & 3 deletions packages/sveltekit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Comment thread
Lms24 marked this conversation as resolved.
"vite": "4.0.0",
"typescript": "^4.9.3"
},
"scripts": {
"build": "run-p build:transpile build:types",
Expand Down
1 change: 1 addition & 0 deletions packages/sveltekit/src/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { withSentryViteConfig } from './withSentryViteConfig';
73 changes: 73 additions & 0 deletions packages/sveltekit/src/config/vitePlugins.ts
Original file line number Diff line number Diff line change
@@ -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';

/**
* 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) {
const serverIndexFilePath = path.join('@sveltejs', 'kit', 'src', 'runtime', 'server', 'index.js');
const devClientAppFilePath = path.join('.svelte-kit', 'generated', 'client', 'app.js');
const prodClientAppFilePath = path.join('.svelte-kit', 'generated', 'client-optimized', 'app.js');
Comment thread
AbhiPrasad marked this conversation as resolved.
Outdated

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}'.`);
}
52 changes: 52 additions & 0 deletions packages/sveltekit/src/config/withSentryViteConfig.ts
Original file line number Diff line number Diff line change
@@ -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.
Comment thread
Lms24 marked this conversation as resolved.
*
* @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<UserConfig> {
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) || []), '.'],
Comment thread
AbhiPrasad marked this conversation as resolved.
},
};

config.server = {
...config.server,
...mergedDevServerFileSystemConfig,
};

return config;
}
1 change: 1 addition & 0 deletions packages/sveltekit/src/index.server.ts
Original file line number Diff line number Diff line change
@@ -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

Expand Down
1 change: 1 addition & 0 deletions packages/sveltekit/src/index.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
58 changes: 58 additions & 0 deletions packages/sveltekit/test/config/vitePlugins.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
117 changes: 117 additions & 0 deletions packages/sveltekit/test/config/withSentryViteConfig.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { UserConfig, Plugin } from 'vite';
import { withSentryViteConfig } from '../../src/config/withSentryViteConfig';

describe('withSentryViteConfig', () => {
const originalConfig = {
plugins: [{ name: 'foo' }],
Comment thread
Lms24 marked this conversation as resolved.
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(['.']);
});
});
Loading