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..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 } 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'; @@ -87,7 +87,19 @@ 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` + ); + } + + rimraf(target); + }, mkdirp, copy, @@ -263,6 +275,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.startsWith('..') && !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;