From a68237b3ceb135160715675f715e51dfc5eae64a Mon Sep 17 00:00:00 2001 From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com> Date: Thu, 30 Jul 2026 19:45:17 -0400 Subject: [PATCH 1/3] fix: error when an adapter tries to delete the SvelteKit build output --- .changeset/hot-jokes-refuse.md | 5 ++++ packages/kit/src/core/adapt/builder.js | 29 +++++++++++++++++++-- packages/kit/src/core/adapt/builder.spec.js | 11 ++++++++ packages/kit/src/exports/public.d.ts | 2 +- packages/kit/types/index.d.ts | 2 +- 5 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 .changeset/hot-jokes-refuse.md diff --git a/.changeset/hot-jokes-refuse.md b/.changeset/hot-jokes-refuse.md new file mode 100644 index 000000000000..171778f14fa6 --- /dev/null +++ b/.changeset/hot-jokes-refuse.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: error when an adapter tries to delete the SvelteKit build output diff --git a/packages/kit/src/core/adapt/builder.js b/packages/kit/src/core/adapt/builder.js index 2515f20dc29d..815b8602c491 100644 --- a/packages/kit/src/core/adapt/builder.js +++ b/packages/kit/src/core/adapt/builder.js @@ -6,7 +6,7 @@ import { loadEnv } from 'vite'; import * as devalue from 'devalue'; import { createReadStream, createWriteStream, existsSync, statSync } from 'node:fs'; -import { extname, resolve, join, dirname, relative } from 'node:path'; +import { extname, resolve, join, dirname, relative, sep, isAbsolute } from 'node:path'; import { pipeline } from 'node:stream'; import { promisify, styleText } from 'node:util'; import zlib from 'node:zlib'; @@ -87,7 +87,22 @@ export function create_builder({ return { log, - rimraf, + /** @param {string} dir */ + rimraf(dir) { + const target = resolve(dir); + const output = resolve(config.kit.outDir, 'output'); + + if (contains(output, target) || contains(target, output)) { + throw new Error( + `Cannot delete ${target}, because it overlaps with ${output}, which SvelteKit copies the build output from. ` + + 'Change the directory your adapter writes to (`publish` in netlify.toml, `out` for adapter-node, ' + + '`pages`/`assets` for adapter-static, `assets.directory` or `pages_build_output_dir` in your Wrangler config) ' + + 'so that the two do not overlap.' + ); + } + + rimraf(target); + }, mkdirp, copy, @@ -263,6 +278,16 @@ export function create_builder({ }; } +/** + * Whether `b` is `a` or lives inside it + * @param {string} a + * @param {string} b + */ +function contains(a, b) { + const path = relative(a, b); + return path === '' || (path !== '..' && !path.startsWith(`..${sep}`) && !isAbsolute(path)); +} + /** * @param {string} file * @param {'gz' | 'br'} format diff --git a/packages/kit/src/core/adapt/builder.spec.js b/packages/kit/src/core/adapt/builder.spec.js index 2e255854ff68..592c5d640c37 100644 --- a/packages/kit/src/core/adapt/builder.spec.js +++ b/packages/kit/src/core/adapt/builder.spec.js @@ -56,6 +56,17 @@ test('copy files', () => { rmSync(dest, { force: true, recursive: true }); }); +test('refuses to delete the build output', () => { + const outDir = join(import.meta.dirname, 'fixtures/basic/.svelte-kit'); + + // @ts-expect-error - we don't need the whole config for this test + const builder = create_builder({ config: { kit: { outDir } }, route_data: [] }); + + expect(() => builder.rimraf(join(outDir, 'output/client'))).toThrow('Cannot delete'); + expect(() => builder.rimraf(outDir)).toThrow('Cannot delete'); + expect(() => builder.rimraf(join(outDir, 'netlify-tmp'))).not.toThrow(); +}); + test('compress files', async () => { // @ts-expect-error - we don't need the whole config for this test const builder = create_builder({ diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index fb1409b2a6be..5b91b70fc476 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -110,7 +110,7 @@ type UnpackValidationError = export interface Builder { /** Print messages to the console. `log.info` and `log.minor` are silent unless Vite's `logLevel` is `info`. */ log: Logger; - /** Remove `dir` and all its contents. */ + /** Remove `dir` and all its contents. Throws if `dir` overlaps with `${config.kit.outDir}/output`, which `writeClient`, `writeServer` and `writePrerendered` copy from. */ rimraf: (dir: string) => void; /** Create `dir` and any required parent directories. */ mkdirp: (dir: string) => void; diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 405b9b79033d..16e6f5c119bd 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -85,7 +85,7 @@ declare module '@sveltejs/kit' { export interface Builder { /** Print messages to the console. `log.info` and `log.minor` are silent unless Vite's `logLevel` is `info`. */ log: Logger; - /** Remove `dir` and all its contents. */ + /** Remove `dir` and all its contents. Throws if `dir` overlaps with `${config.kit.outDir}/output`, which `writeClient`, `writeServer` and `writePrerendered` copy from. */ rimraf: (dir: string) => void; /** Create `dir` and any required parent directories. */ mkdirp: (dir: string) => void; From 52d65f975ba3497423cf8d48b4fff6d4023d24cf Mon Sep 17 00:00:00 2001 From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com> Date: Thu, 30 Jul 2026 19:55:13 -0400 Subject: [PATCH 2/3] shorten error message --- packages/kit/src/core/adapt/builder.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/kit/src/core/adapt/builder.js b/packages/kit/src/core/adapt/builder.js index 815b8602c491..71bf6f84e7a9 100644 --- a/packages/kit/src/core/adapt/builder.js +++ b/packages/kit/src/core/adapt/builder.js @@ -94,10 +94,7 @@ export function create_builder({ if (contains(output, target) || contains(target, output)) { throw new Error( - `Cannot delete ${target}, because it overlaps with ${output}, which SvelteKit copies the build output from. ` + - 'Change the directory your adapter writes to (`publish` in netlify.toml, `out` for adapter-node, ' + - '`pages`/`assets` for adapter-static, `assets.directory` or `pages_build_output_dir` in your Wrangler config) ' + - 'so that the two do not overlap.' + `Cannot delete ${target}, because it overlaps with ${output}, which SvelteKit copies the build output from` ); } From 265bc04c579a364bb4894b96ceac25e28d27481c Mon Sep 17 00:00:00 2001 From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com> Date: Thu, 30 Jul 2026 20:00:46 -0400 Subject: [PATCH 3/3] simplify overlap check --- packages/kit/src/core/adapt/builder.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kit/src/core/adapt/builder.js b/packages/kit/src/core/adapt/builder.js index 71bf6f84e7a9..68624ea66667 100644 --- a/packages/kit/src/core/adapt/builder.js +++ b/packages/kit/src/core/adapt/builder.js @@ -6,7 +6,7 @@ import { loadEnv } from 'vite'; import * as devalue from 'devalue'; import { createReadStream, createWriteStream, existsSync, statSync } from 'node:fs'; -import { extname, resolve, join, dirname, relative, sep, isAbsolute } from 'node:path'; +import { extname, resolve, join, dirname, relative, isAbsolute } from 'node:path'; import { pipeline } from 'node:stream'; import { promisify, styleText } from 'node:util'; import zlib from 'node:zlib'; @@ -282,7 +282,7 @@ export function create_builder({ */ function contains(a, b) { const path = relative(a, b); - return path === '' || (path !== '..' && !path.startsWith(`..${sep}`) && !isAbsolute(path)); + return !path.startsWith('..') && !isAbsolute(path); } /**