import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import {
mkdir,
mkdtemp,
readFile,
rm,
symlink,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import { publishSource } from "../scripts/publish-source.mjs";
async function fixture(t) {
const root = await mkdtemp(join(tmpdir(), "source-export-"));
t.after(() => rm(root, { recursive: true, force: true }));
for (const folder of ["src", "scripts", "tests", "assets", ".vercel", "dist"])
await mkdir(join(root, folder));
await writeFile(
join(root, "src/page.html"),
'Hello & goodbye',
);
await writeFile(
join(root, "scripts/build.mjs"),
"export const build = true;\n",
);
await writeFile(join(root, "tests/example.test.mjs"), "// Portable test\n");
await writeFile(join(root, "assets/logo.svg"), "");
await writeFile(join(root, "package.json"), '{"private":true}');
await writeFile(join(root, "README.md"), "# Setup\n");
await writeFile(join(root, ".env"), "PRIVATE=never-export");
await writeFile(join(root, ".vercel/project.json"), "private-project");
await writeFile(join(root, "src/.env"), "PRIVATE=never-export");
await writeFile(join(root, "dist/old.js"), "old-output");
return root;
}
test("source publication exposes portable text and local asset hashes only", async (t) => {
const root = await fixture(t);
await symlink(join(root, ".env"), join(root, "src/linked.js"));
const destination = join(root, "dist");
await publishSource(root, destination);
const manifest = JSON.parse(
await readFile(join(destination, "source/manifest.json")),
);
const bundle = JSON.parse(
await readFile(join(destination, "source/bundle.json")),
);
assert.equal(manifest.version, 1);
assert.deepEqual(
manifest.files.map(({ path }) => path),
[
"README.md",
"package.json",
"scripts/build.mjs",
"src/page.html",
"tests/example.test.mjs",
],
);
for (const entry of manifest.files) {
const raw = await readFile(join(destination, entry.url));
assert.equal(entry.bytes, raw.length);
assert.equal(entry.sha256, createHash("sha256").update(raw).digest("hex"));
assert.equal(
bundle.files.find(({ path }) => path === entry.path).content,
raw.toString("utf8"),
);
}
assert.equal(manifest.assets[0].url, "assets/logo.svg");
assert.deepEqual(bundle.assets, manifest.assets);
const index = await readFile(join(destination, "source/index.html"), "utf8");
assert.ok(index.includes("<main title="example">"));
assert.ok(index.includes("Hello & goodbye"));
assert.ok(!index.includes(" {
const root = await fixture(t);
await writeFile(
join(root, "src/local.js"),
`const file = '${"/" + "Users/"}someone/private';`,
);
await assert.rejects(
publishSource(root, join(root, "dist")),
/machine-specific path.*src\/local.js/i,
);
});
test("allowlisted roots cannot follow a symlink into private files", async (t) => {
const root = await fixture(t);
await rm(join(root, "scripts"), { recursive: true });
await symlink(join(root, ".vercel"), join(root, "scripts"));
const manifest = await publishSource(root, join(root, "dist"));
assert.ok(!manifest.files.some(({ path }) => path.startsWith("scripts/")));
});