import test from "node:test"; import assert from "node:assert/strict"; import { mkdtempSync, mkdirSync, writeFileSync, symlinkSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { resolveUploadDir } from "../pages-output-dir.js"; /** A site tree on disk. `dirs` are created empty; `files` are written. */ function tree(files: Record, dirs: string[] = []): string { const dir = mkdtempSync(join(tmpdir(), "pages-out-")); for (const d of dirs) mkdirSync(join(dir, d), { recursive: true }); for (const [rel, body] of Object.entries(files)) writeFileSync(join(dir, rel), body); return dir; } test("no wrangler config at the root resolves to the site root", async () => { const dir = tree({ "index.html": "" }); assert.deepEqual(await resolveUploadDir(dir), { uploadDir: dir, source: "site-root" }); }); test("a config declaring no output dir resolves to the site root", async () => { const dir = tree({ "wrangler.toml": 'name = "kit-data"\n' }); assert.deepEqual(await resolveUploadDir(dir), { uploadDir: dir, source: "site-root" }); }); test("a declared toml output dir resolves to that subdirectory", async () => { const dir = tree({ "wrangler.toml": 'name = "gls"\npages_build_output_dir = "public"\n' }, [ "public", ]); assert.deepEqual(await resolveUploadDir(dir), { uploadDir: join(dir, "public"), source: "wrangler.toml", declared: "public", }); }); test('a declared "." resolves to the site root and still names its config', async () => { const dir = tree({ "wrangler.toml": 'pages_build_output_dir = "."\n' }); assert.deepEqual(await resolveUploadDir(dir), { uploadDir: dir, source: "wrangler.toml", declared: ".", }); }); test("a single-quoted toml value is read", async () => { const dir = tree({ "wrangler.toml": "pages_build_output_dir = 'dist'\n" }, ["dist"]); assert.equal((await resolveUploadDir(dir)).uploadDir, join(dir, "dist")); }); test("a commented-out toml key is not read", async () => { const dir = tree({ "wrangler.toml": '# pages_build_output_dir = "public"\n' }, ["public"]); assert.equal((await resolveUploadDir(dir)).source, "site-root"); }); test("a key inside a toml section is not read as the top-level key", async () => { const dir = tree({ "wrangler.toml": '[env.preview]\npages_build_output_dir = "preview-out"\n' }, [ "preview-out", ]); assert.equal((await resolveUploadDir(dir)).source, "site-root"); }); test("a json config is read", async () => { const dir = tree({ "wrangler.json": '{"pages_build_output_dir":"out"}' }, ["out"]); assert.deepEqual(await resolveUploadDir(dir), { uploadDir: join(dir, "out"), source: "wrangler.json", declared: "out", }); }); test("a jsonc config is read past its comments", async () => { const dir = tree( { "wrangler.jsonc": '{\n // the served root\n "pages_build_output_dir": "out", /* trailing */\n "name": "x"\n}', }, ["out"], ); assert.deepEqual(await resolveUploadDir(dir), { uploadDir: join(dir, "out"), source: "wrangler.jsonc", declared: "out", }); }); test("a comment marker inside a jsonc string is not stripped", async () => { const dir = tree({ "wrangler.jsonc": '{"pages_build_output_dir":"a//b"}' }, ["a/b"]); assert.equal((await resolveUploadDir(dir)).declared, "a//b"); }); test("wrangler.json wins over wrangler.toml, matching wrangler's own precedence", async () => { const dir = tree( { "wrangler.json": '{"pages_build_output_dir":"from-json"}', "wrangler.toml": 'pages_build_output_dir = "from-toml"\n', }, ["from-json", "from-toml"], ); assert.equal((await resolveUploadDir(dir)).source, "wrangler.json"); }); test("an unparseable config throws naming the file, never a site-root fallback", async () => { const dir = tree({ "wrangler.json": "{not json" }); await assert.rejects(() => resolveUploadDir(dir), /wrangler\.json/); }); test("a declared dir that does not exist throws naming the resolved path", async () => { const dir = tree({ "wrangler.toml": 'pages_build_output_dir = "public"\n' }); await assert.rejects(() => resolveUploadDir(dir), new RegExp(join(dir, "public"))); }); test("a declared dir that is a file throws naming the resolved path", async () => { const dir = tree({ "wrangler.toml": 'pages_build_output_dir = "public"\n', public: "not a directory", }); await assert.rejects(() => resolveUploadDir(dir), new RegExp(join(dir, "public"))); }); test("a declared value escaping the site root throws naming the resolved path", async () => { const dir = tree({ "wrangler.toml": 'pages_build_output_dir = "../.."\n' }); await assert.rejects(() => resolveUploadDir(dir), /outside/); }); test("a non-string declared value throws naming the file", async () => { const dir = tree({ "wrangler.json": '{"pages_build_output_dir":42}' }); await assert.rejects(() => resolveUploadDir(dir), /wrangler\.json/); }); test("a declared dir that is a symlink out of the site root is refused, naming the path", async () => { const dir = tree({ "wrangler.toml": 'pages_build_output_dir = "public"\n' }); const outside = tree({ "SECRET.txt": "house credential" }); symlinkSync(outside, join(dir, "public")); await assert.rejects( () => resolveUploadDir(dir), new RegExp(`symbolic link|${join(dir, "public")}`), "a lexical containment check alone passes this and publishes whatever the link points at", ); }); test("a declared value escaping through a symlinked parent segment throws as outside", async () => { const dir = tree({ "wrangler.toml": 'pages_build_output_dir = "link/out"\n' }); const outside = tree({}, ["out"]); symlinkSync(outside, join(dir, "link")); await assert.rejects( () => resolveUploadDir(dir), /outside/, "the leaf is a real directory here, so only the realpath comparison catches it", ); }); test("a declared dir that is a symlink is refused even when it points inside the tree", async () => { const dir = tree({ "wrangler.toml": 'pages_build_output_dir = "public"\n' }, ["real"]); symlinkSync(join(dir, "real"), join(dir, "public")); await assert.rejects( () => resolveUploadDir(dir), /symbolic link/, "the staging copy cannot take a link as its source, and a link target can change between resolution and copy", ); }); test("a declared dir reached through a symlinked parent that stays in the tree is accepted", async () => { const dir = tree({ "wrangler.toml": 'pages_build_output_dir = "link/out"\n' }, ["real/out"]); symlinkSync(join(dir, "real"), join(dir, "link")); assert.equal((await resolveUploadDir(dir)).source, "wrangler.toml"); });