Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 3 additions & 3 deletions packages/sveltekit/src/vite/sentryVitePlugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ export async function sentrySvelteKit(options: SentrySvelteKitPluginOptions = {}
const getAdapter = (): Promise<SupportedSvelteKitAdapters> =>
(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<string> | undefined;
const getAdapterOutputDirOnce = (): Promise<string> =>
(adapterOutputDirPromise ??= (async () => getAdapterOutputDir(await getKitConfig(), await getAdapter()))());
Expand Down
30 changes: 29 additions & 1 deletion packages/sveltekit/src/vite/svelteConfig.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -106,7 +107,11 @@ async function getNodeAdapterOutputDir(kitConfig: ResolvedKitConfig): Promise<st
rimraf: () => {},
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: {
Expand All @@ -116,10 +121,33 @@ async function getNodeAdapterOutputDir(kitConfig: ResolvedKitConfig): Promise<st
},
};

// `@sveltejs/adapter-node` v6 deletes its (relative) output dir with `fs.rmSync` before it touches the
// builder, so our no-op `rimraf` doesn't help. Running `adapt()` from an empty temp cwd keeps it from
// wiping the user's real build output. The rm and `writeClient` both run synchronously, so the cwd is
// only changed for the synchronous part of the call and no other code observes it.
const originalCwd = process.cwd();
let sandboxDir: string | undefined;
let adaptResult: unknown;
try {
await nodeAdapter.adapt(adapterBuilder);
sandboxDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sentry-sveltekit-'));
process.chdir(sandboxDir);
adaptResult = nodeAdapter.adapt(adapterBuilder);
} catch {
// We expect the adapter to throw in writeClient!
} finally {
if (process.cwd() !== originalCwd) {
process.chdir(originalCwd);
}
}

try {
await adaptResult;
} catch {
// We expect the adapter to throw in writeClient!
}

if (sandboxDir) {
fs.rmSync(sandboxDir, { recursive: true, force: true });
}

return outputDir;
Expand Down
36 changes: 35 additions & 1 deletion packages/sveltekit/test/vite/svelteConfig.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import type * as FsModule from 'fs';
import * as os from 'os';
import * as path from 'path';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { SupportedSvelteKitAdapters } from '../../src/vite/detectAdapter';
import { getAdapterOutputDir, getHooksFileName, loadSvelteConfig } from '../../src/vite/svelteConfig';

let existsFile: any;

describe('loadSvelteConfig', () => {
vi.mock('fs', () => {
vi.mock('fs', async importOriginal => {
const actual = await importOriginal<typeof FsModule>();
return {
...actual,
existsSync: () => existsFile,
};
});
Expand Down Expand Up @@ -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<typeof FsModule>('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');
Expand Down
Loading