diff --git a/packages/sveltekit/src/vite/sentryVitePlugins.ts b/packages/sveltekit/src/vite/sentryVitePlugins.ts index 9052560af04b..d4bf6ead91ec 100644 --- a/packages/sveltekit/src/vite/sentryVitePlugins.ts +++ b/packages/sveltekit/src/vite/sentryVitePlugins.ts @@ -38,9 +38,9 @@ export async function sentrySvelteKit(options: SentrySvelteKitPluginOptions = {} const getAdapter = (): Promise => (adapterPromise ??= (async () => options.adapter || detectAdapter(await getKitConfig(), options.debug))()); - // Side effect: for the Node adapter we invoke `adapter.adapt()` to learn the output directory, - // and `@sveltejs/adapter-node` v6 wipes that directory when it runs. So this must happen once, - // before the build writes anything - never from a late hook like `closeBundle`. + // For the Node adapter we invoke `adapter.adapt()` to learn the output directory. It runs from a temp + // cwd so it can't wipe the real output, but it's still resolved once and before the build writes + // anything, never from a late hook like `closeBundle`, in case an adapter deletes an absolute `out`. let adapterOutputDirPromise: Promise | undefined; const getAdapterOutputDirOnce = (): Promise => (adapterOutputDirPromise ??= (async () => getAdapterOutputDir(await getKitConfig(), await getAdapter()))()); diff --git a/packages/sveltekit/src/vite/svelteConfig.ts b/packages/sveltekit/src/vite/svelteConfig.ts index 3941dc3dbc59..7151ba053f4c 100644 --- a/packages/sveltekit/src/vite/svelteConfig.ts +++ b/packages/sveltekit/src/vite/svelteConfig.ts @@ -1,5 +1,6 @@ import type { Builder } from '@sveltejs/kit'; import * as fs from 'fs'; +import * as os from 'os'; import * as path from 'path'; import * as url from 'url'; import type { SupportedSvelteKitAdapters } from './detectAdapter'; @@ -106,7 +107,11 @@ async function getNodeAdapterOutputDir(kitConfig: ResolvedKitConfig): Promise {}, mkdirp: () => {}, + // SvelteKit 3 passes the kit config directly (`config.paths`), SvelteKit 2 nests it (`config.kit.paths`) config: { + paths: { + base: kitConfig.paths?.base || '', + }, kit: { // @ts-expect-error - the builder expects a validated config but for our purpose it's fine to just pass this partial config paths: { @@ -116,10 +121,33 @@ async function getNodeAdapterOutputDir(kitConfig: ResolvedKitConfig): Promise { - vi.mock('fs', () => { + vi.mock('fs', async importOriginal => { + const actual = await importOriginal(); return { + ...actual, existsSync: () => existsFile, }; }); @@ -67,6 +72,35 @@ describe('getAdapterOutputDir', () => { expect(outputDir).toEqual('customBuildDir'); }); + it("doesn't let the Node adapter delete the real output directory", async () => { + const actualFs = await vi.importActual('fs'); + const originalCwd = process.cwd(); + const projectDir = actualFs.mkdtempSync(path.join(os.tmpdir(), 'sentry-sveltekit-test-')); + actualFs.mkdirSync(path.join(projectDir, 'customOut')); + actualFs.writeFileSync(path.join(projectDir, 'customOut', 'index.js'), ''); + + // Like `@sveltejs/adapter-node` v6, which removes its output dir itself instead of via `builder.rimraf` + const nodeAdapter = { + name: '@sveltejs/adapter-node', + async adapt(builder: any) { + actualFs.rmSync('customOut', { force: true, recursive: true }); + builder.writeClient(`customOut/client${builder.config.paths.base}`); + }, + }; + + process.chdir(projectDir); + try { + const outputDir = await getAdapterOutputDir({ adapter: nodeAdapter }, 'node'); + + expect(outputDir).toEqual('customOut'); + expect(process.cwd()).toEqual(actualFs.realpathSync(projectDir)); + expect(actualFs.readdirSync(path.join(projectDir, 'customOut'))).toEqual(['index.js']); + } finally { + process.chdir(originalCwd); + actualFs.rmSync(projectDir, { force: true, recursive: true }); + } + }); + it('returns the output directory of the Cloudflare adapter', async () => { const outputDir = await getAdapterOutputDir({ outDir: 'customOutDir' }, 'cloudflare'); expect(outputDir).toEqual('customOutDir/cloudflare');