diff --git a/packages/sveltekit/src/vite/detectAdapter.ts b/packages/sveltekit/src/vite/detectAdapter.ts new file mode 100644 index 000000000000..95e26dfc9f6d --- /dev/null +++ b/packages/sveltekit/src/vite/detectAdapter.ts @@ -0,0 +1,62 @@ +import type { Package } from '@sentry/types'; +import * as fs from 'fs'; +import * as path from 'path'; + +/** + * Supported @sveltejs/adapters-[adapter] SvelteKit adapters + */ +export type SupportedSvelteKitAdapters = 'node' | 'auto' | 'vercel' | 'other'; + +/** + * Tries to detect the used adapter for SvelteKit by looking at the dependencies. + * returns the name of the adapter or 'other' if no supported adapter was found. + */ +export async function detectAdapter(debug?: boolean): Promise { + const pkgJson = await loadPackageJson(); + + const allDependencies = pkgJson ? { ...pkgJson.dependencies, ...pkgJson.devDependencies } : {}; + + let adapter: SupportedSvelteKitAdapters = 'other'; + if (allDependencies['@sveltejs/adapter-vercel']) { + adapter = 'vercel'; + } else if (allDependencies['@sveltejs/adapter-node']) { + adapter = 'node'; + } else if (allDependencies['@sveltejs/adapter-auto']) { + adapter = 'auto'; + } + + if (debug) { + if (adapter === 'other') { + // eslint-disable-next-line no-console + console.warn( + "[Sentry SvelteKit Plugin] Couldn't detect SvelteKit adapter. Please set the 'adapter' option manually.", + ); + } else { + // eslint-disable-next-line no-console + console.log(`[Sentry SvelteKit Plugin] Detected SvelteKit ${adapter} adapter`); + } + } + + return adapter; +} + +/** + * Imports the pacakge.json file and returns the parsed JSON object. + */ +async function loadPackageJson(): Promise { + const pkgFile = path.join(process.cwd(), 'package.json'); + + try { + if (!fs.existsSync(pkgFile)) { + throw new Error(`File ${pkgFile} doesn't exist}`); + } + + const pkgJsonContent = (await fs.promises.readFile(pkgFile, 'utf-8')).toString(); + + return JSON.parse(pkgJsonContent); + } catch (e) { + // eslint-disable-next-line no-console + console.warn("[Sentry SvelteKit Plugin] Couldn't load package.json:", e); + return undefined; + } +} diff --git a/packages/sveltekit/src/vite/sentryVitePlugins.ts b/packages/sveltekit/src/vite/sentryVitePlugins.ts index 1f983d7a9653..e78a25adea10 100644 --- a/packages/sveltekit/src/vite/sentryVitePlugins.ts +++ b/packages/sveltekit/src/vite/sentryVitePlugins.ts @@ -3,6 +3,8 @@ import type { Plugin } from 'vite'; import type { AutoInstrumentSelection } from './autoInstrument'; import { makeAutoInstrumentationPlugin } from './autoInstrument'; +import type { SupportedSvelteKitAdapters } from './detectAdapter'; +import { detectAdapter } from './detectAdapter'; import { makeCustomSentryVitePlugin } from './sourceMaps'; type SourceMapsUploadOptions = { @@ -39,6 +41,24 @@ export type SentrySvelteKitPluginOptions = { * @default false. */ debug?: boolean; + + /** + * Specify which SvelteKit adapter you're using. + * By default, the SDK will attempt auto-detect the used adapter at build time and apply the + * correct config for source maps upload or auto-instrumentation. + * + * Currently, the SDK supports the following adapters: + * - node (@sveltejs/adapter-node) + * - auto (@sveltejs/adapter-auto) only Vercel + * - vercel (@sveltejs/adapter-auto) only Serverless functions, no edge runtime + * + * Set this option, if the SDK detects the wrong adapter or you want to use an adapter + * that is not in this list. If you specify 'other', you'll most likely need to configure + * source maps upload yourself. + * + * @default {} the SDK attempts to auto-detect the used adapter at build time + */ + adapter?: SupportedSvelteKitAdapters; } & SourceMapsUploadOptions & AutoInstrumentOptions; @@ -59,6 +79,7 @@ export async function sentrySvelteKit(options: SentrySvelteKitPluginOptions = {} const mergedOptions = { ...DEFAULT_PLUGIN_OPTIONS, ...options, + adapter: options.adapter || (await detectAdapter(options.debug || false)), }; const sentryPlugins: Plugin[] = []; diff --git a/packages/sveltekit/test/vite/detectAdapter.test.ts b/packages/sveltekit/test/vite/detectAdapter.test.ts new file mode 100644 index 000000000000..7f4e05a1a44b --- /dev/null +++ b/packages/sveltekit/test/vite/detectAdapter.test.ts @@ -0,0 +1,79 @@ +import { vi } from 'vitest'; + +import { detectAdapter } from '../../src/vite/detectAdapter'; + +let existsFile = true; +const pkgJson = { + dependencies: {}, +}; +describe('detectAdapter', () => { + beforeEach(() => { + existsFile = true; + vi.clearAllMocks(); + pkgJson.dependencies = {}; + }); + + vi.mock('fs', () => { + return { + existsSync: () => existsFile, + promises: { + readFile: () => { + return Promise.resolve(JSON.stringify(pkgJson)); + }, + }, + }; + }); + + const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + + it.each(['auto', 'vercel', 'node'])( + 'returns the adapter name (adapter %s) and logs it to the console', + async adapter => { + pkgJson.dependencies[`@sveltejs/adapter-${adapter}`] = '1.0.0'; + const detectedAdapter = await detectAdapter(true); + expect(detectedAdapter).toEqual(adapter); + expect(consoleLogSpy).toHaveBeenCalledTimes(1); + expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining(`Detected SvelteKit ${adapter} adapter`)); + }, + ); + + it('returns "other" if no supported adapter was found', async () => { + pkgJson.dependencies['@sveltejs/adapter-netlify'] = '1.0.0'; + const detectedAdapter = await detectAdapter(); + expect(detectedAdapter).toEqual('other'); + }); + + it('logs a warning if in debug mode and no supported adapter was found', async () => { + pkgJson.dependencies['@sveltejs/adapter-netlify'] = '1.0.0'; + const detectedAdapter = await detectAdapter(true); + expect(detectedAdapter).toEqual('other'); + expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining("Couldn't detect SvelteKit adapter")); + }); + + it('returns "other" if package.json isnt available and emits a warning log', async () => { + existsFile = false; + const detectedAdapter = await detectAdapter(); + expect(detectedAdapter).toEqual('other'); + + expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy).toHaveBeenCalledWith( + expect.stringContaining("Couldn't load package.json"), + expect.any(Error), + ); + }); + + it('prefers all other adapters over adapter auto', async () => { + pkgJson.dependencies['@sveltejs/adapter-auto'] = '1.0.0'; + pkgJson.dependencies['@sveltejs/adapter-vercel'] = '1.0.0'; + pkgJson.dependencies['@sveltejs/adapter-node'] = '1.0.0'; + + const detectedAdapter = await detectAdapter(); + expect(detectedAdapter).toEqual('vercel'); + + delete pkgJson.dependencies['@sveltejs/adapter-vercel']; + const detectedAdapter2 = await detectAdapter(); + expect(detectedAdapter2).toEqual('node'); + }); +}); diff --git a/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts b/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts index 963844bdef70..026d347d777d 100644 --- a/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts +++ b/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts @@ -17,7 +17,14 @@ vi.mock('fs', async () => { }; }); -describe('sentryVite()', () => { +vi.spyOn(console, 'log').mockImplementation(() => { + /* noop */ +}); +vi.spyOn(console, 'warn').mockImplementation(() => { + /* noop */ +}); + +describe('sentrySvelteKit()', () => { it('returns an array of Vite plugins', async () => { const plugins = await sentrySvelteKit(); diff --git a/packages/types/src/package.ts b/packages/types/src/package.ts index c44d66e62950..15a61f3668d3 100644 --- a/packages/types/src/package.ts +++ b/packages/types/src/package.ts @@ -2,4 +2,6 @@ export interface Package { name: string; version: string; + dependencies?: Record; + devDependencies?: Record; }