Skip to content

Commit 06b4421

Browse files
chargomeclaude
andcommitted
fix(sveltekit): Stop adapter-node 6 output dir probe from deleting the build
To learn the Node adapter's output directory we call its adapt() with a stub builder. adapter-node 6 deletes its output dir with fs.rmSync before it uses the builder, so loading the Vite config (vite dev, svelte-kit sync, ...) wiped the user's build/ folder. Run adapt() from an empty temp cwd so the relative delete hits the temp dir instead. The stub builder also only had the SvelteKit 2 config shape (config.kit.paths), so adapter-node 6 threw before reporting a custom out dir and we fell back to build. Provide the SvelteKit 3 shape (config.paths) as well. Fixes #25186 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent a3dfc27 commit 06b4421

3 files changed

Lines changed: 67 additions & 5 deletions

File tree

‎packages/sveltekit/src/vite/sentryVitePlugins.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ export async function sentrySvelteKit(options: SentrySvelteKitPluginOptions = {}
3838
const getAdapter = (): Promise<SupportedSvelteKitAdapters> =>
3939
(adapterPromise ??= (async () => options.adapter || detectAdapter(await getKitConfig(), options.debug))());
4040

41-
// Side effect: for the Node adapter we invoke `adapter.adapt()` to learn the output directory,
42-
// and `@sveltejs/adapter-node` v6 wipes that directory when it runs. So this must happen once,
43-
// before the build writes anything - never from a late hook like `closeBundle`.
41+
// For the Node adapter we invoke `adapter.adapt()` to learn the output directory. It runs from a temp
42+
// cwd so it can't wipe the real output, but it's still resolved once and before the build writes
43+
// anything, never from a late hook like `closeBundle`, in case an adapter deletes an absolute `out`.
4444
let adapterOutputDirPromise: Promise<string> | undefined;
4545
const getAdapterOutputDirOnce = (): Promise<string> =>
4646
(adapterOutputDirPromise ??= (async () => getAdapterOutputDir(await getKitConfig(), await getAdapter()))());

‎packages/sveltekit/src/vite/svelteConfig.ts‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Builder } from '@sveltejs/kit';
22
import * as fs from 'fs';
3+
import * as os from 'os';
34
import * as path from 'path';
45
import * as url from 'url';
56
import type { SupportedSvelteKitAdapters } from './detectAdapter';
@@ -106,7 +107,11 @@ async function getNodeAdapterOutputDir(kitConfig: ResolvedKitConfig): Promise<st
106107
rimraf: () => {},
107108
mkdirp: () => {},
108109

110+
// SvelteKit 3 passes the kit config directly (`config.paths`), SvelteKit 2 nests it (`config.kit.paths`)
109111
config: {
112+
paths: {
113+
base: kitConfig.paths?.base || '',
114+
},
110115
kit: {
111116
// @ts-expect-error - the builder expects a validated config but for our purpose it's fine to just pass this partial config
112117
paths: {
@@ -116,10 +121,33 @@ async function getNodeAdapterOutputDir(kitConfig: ResolvedKitConfig): Promise<st
116121
},
117122
};
118123

124+
// `@sveltejs/adapter-node` v6 deletes its (relative) output dir with `fs.rmSync` before it touches the
125+
// builder, so our no-op `rimraf` doesn't help. Running `adapt()` from an empty temp cwd keeps it from
126+
// wiping the user's real build output. The rm and `writeClient` both run synchronously, so the cwd is
127+
// only changed for the synchronous part of the call and no other code observes it.
128+
const originalCwd = process.cwd();
129+
let sandboxDir: string | undefined;
130+
let adaptResult: unknown;
119131
try {
120-
await nodeAdapter.adapt(adapterBuilder);
132+
sandboxDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sentry-sveltekit-'));
133+
process.chdir(sandboxDir);
134+
adaptResult = nodeAdapter.adapt(adapterBuilder);
121135
} catch {
122136
// We expect the adapter to throw in writeClient!
137+
} finally {
138+
if (process.cwd() !== originalCwd) {
139+
process.chdir(originalCwd);
140+
}
141+
}
142+
143+
try {
144+
await adaptResult;
145+
} catch {
146+
// We expect the adapter to throw in writeClient!
147+
}
148+
149+
if (sandboxDir) {
150+
fs.rmSync(sandboxDir, { recursive: true, force: true });
123151
}
124152

125153
return outputDir;

‎packages/sveltekit/test/vite/svelteConfig.test.ts‎

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1+
import type * as FsModule from 'fs';
2+
import * as os from 'os';
3+
import * as path from 'path';
14
import { beforeEach, describe, expect, it, vi } from 'vitest';
25
import type { SupportedSvelteKitAdapters } from '../../src/vite/detectAdapter';
36
import { getAdapterOutputDir, getHooksFileName, loadSvelteConfig } from '../../src/vite/svelteConfig';
47

58
let existsFile: any;
69

710
describe('loadSvelteConfig', () => {
8-
vi.mock('fs', () => {
11+
vi.mock('fs', async importOriginal => {
12+
const actual = await importOriginal<typeof FsModule>();
913
return {
14+
...actual,
1015
existsSync: () => existsFile,
1116
};
1217
});
@@ -67,6 +72,35 @@ describe('getAdapterOutputDir', () => {
6772
expect(outputDir).toEqual('customBuildDir');
6873
});
6974

75+
it("doesn't let the Node adapter delete the real output directory", async () => {
76+
const actualFs = await vi.importActual<typeof FsModule>('fs');
77+
const originalCwd = process.cwd();
78+
const projectDir = actualFs.mkdtempSync(path.join(os.tmpdir(), 'sentry-sveltekit-test-'));
79+
actualFs.mkdirSync(path.join(projectDir, 'customOut'));
80+
actualFs.writeFileSync(path.join(projectDir, 'customOut', 'index.js'), '');
81+
82+
// Like `@sveltejs/adapter-node` v6, which removes its output dir itself instead of via `builder.rimraf`
83+
const nodeAdapter = {
84+
name: '@sveltejs/adapter-node',
85+
async adapt(builder: any) {
86+
actualFs.rmSync('customOut', { force: true, recursive: true });
87+
builder.writeClient(`customOut/client${builder.config.paths.base}`);
88+
},
89+
};
90+
91+
process.chdir(projectDir);
92+
try {
93+
const outputDir = await getAdapterOutputDir({ adapter: nodeAdapter }, 'node');
94+
95+
expect(outputDir).toEqual('customOut');
96+
expect(process.cwd()).toEqual(actualFs.realpathSync(projectDir));
97+
expect(actualFs.readdirSync(path.join(projectDir, 'customOut'))).toEqual(['index.js']);
98+
} finally {
99+
process.chdir(originalCwd);
100+
actualFs.rmSync(projectDir, { force: true, recursive: true });
101+
}
102+
});
103+
70104
it('returns the output directory of the Cloudflare adapter', async () => {
71105
const outputDir = await getAdapterOutputDir({ outDir: 'customOutDir' }, 'cloudflare');
72106
expect(outputDir).toEqual('customOutDir/cloudflare');

0 commit comments

Comments
 (0)