b3a7f98e5a
CI / E2E Cloudflare (4/8) (push) Failing after 0s
CI / E2E Cloudflare (8/8) (push) Failing after 1s
CI / Lint (push) Failing after 1s
Auto Extract / Extract (push) Failing after 4s
CI / Version Check (push) Failing after 9s
CI / Integration Tests (push) Failing after 1s
CI / E2E tests (1/8) (push) Failing after 1s
CI / E2E tests (2/8) (push) Failing after 2s
CI / E2E tests (3/8) (push) Failing after 2s
CI / Browser Tests (push) Failing after 1s
CI / E2E tests (5/8) (push) Failing after 1s
CI / E2E Cloudflare (2/8) (push) Failing after 2s
CI / Typecheck (push) Failing after 1s
CI / Changeset Validation (push) Failing after 2s
CI / E2E Cloudflare (5/8) (push) Failing after 1s
CI / E2E Cloudflare (6/8) (push) Failing after 1s
CI / E2E Cloudflare (7/8) (push) Failing after 1s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1s
Format / Format (push) Failing after 0s
CodeQL / Analyze (actions) (push) Failing after 4s
CI / E2E tests (4/8) (push) Failing after 1s
CI / E2E tests (6/8) (push) Failing after 1s
CI / E2E tests (7/8) (push) Failing after 2s
CI / E2E tests (8/8) (push) Failing after 1s
CI / E2E Cloudflare (1/8) (push) Failing after 1s
CI / E2E Cloudflare (3/8) (push) Failing after 2s
Preview Releases / Publish Preview (push) Failing after 0s
zizmor / Run zizmor (push) Failing after 1s
Release / Release (push) Failing after 2s
CI / Smoke Tests (push) Failing after 5m36s
CI / Tests (push) Failing after 6m36s
Release / Sync Templates (push) Has been skipped
CI / E2E Tests (push) Has been cancelled
173 lines
6.2 KiB
TypeScript
173 lines
6.2 KiB
TypeScript
/**
|
|
* End-to-end coverage for `scaffold()`: the filesystem half of init.
|
|
*
|
|
* Each test runs against a fresh tempdir so writes don't collide. The
|
|
* suite's job is to verify the file tree, the overwrite policy, and
|
|
* the "the scaffold round-trips through the loader" invariant — the
|
|
* scaffolder shouldn't produce a manifest that its own validator
|
|
* rejects (when the user has supplied all required fields).
|
|
*/
|
|
|
|
import { mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
|
|
import { InitError, scaffold } from "../src/init/scaffold.js";
|
|
import type { ScaffoldInputs } from "../src/init/templates.js";
|
|
import { loadManifest } from "../src/manifest/load.js";
|
|
|
|
const FULL_INPUTS: ScaffoldInputs = {
|
|
slug: "gallery",
|
|
publisher: "did:plc:abc123def456",
|
|
publisherHandle: "example.com",
|
|
license: "MIT",
|
|
author: { name: "Jane Doe" },
|
|
security: { email: "security@example.com" },
|
|
description: undefined,
|
|
repo: undefined,
|
|
};
|
|
|
|
const MINIMAL_INPUTS: ScaffoldInputs = {
|
|
slug: "gallery",
|
|
publisher: undefined,
|
|
publisherHandle: undefined,
|
|
license: undefined,
|
|
author: undefined,
|
|
security: undefined,
|
|
description: undefined,
|
|
repo: undefined,
|
|
};
|
|
|
|
describe("scaffold", () => {
|
|
let targetDir: string;
|
|
|
|
beforeEach(async () => {
|
|
// Each test gets its own tempdir, then immediately removes the
|
|
// dir so the scaffolder is creating from scratch (matches what
|
|
// a real `init` invocation does into a brand-new directory).
|
|
const root = await mkdtemp(join(tmpdir(), "emdash-init-test-"));
|
|
targetDir = join(root, "plugin");
|
|
});
|
|
|
|
afterEach(async () => {
|
|
// rm the parent root, not just targetDir, so we don't leak
|
|
// the mkdtemp parent behind.
|
|
const root = targetDir.replace(/\/plugin$/, "");
|
|
await rm(root, { recursive: true, force: true });
|
|
});
|
|
|
|
it("writes the expected file tree", async () => {
|
|
const result = await scaffold({ targetDir, inputs: FULL_INPUTS, force: false });
|
|
expect(result.written).toHaveLength(7);
|
|
|
|
// Spot-check the structure rather than pinning the array order.
|
|
const fileSet = new Set(result.written.map((p) => p.replace(`${targetDir}/`, "")));
|
|
expect(fileSet.has("emdash-plugin.jsonc")).toBe(true);
|
|
expect(fileSet.has("package.json")).toBe(true);
|
|
expect(fileSet.has("tsconfig.json")).toBe(true);
|
|
expect(fileSet.has(".gitignore")).toBe(true);
|
|
expect(fileSet.has("README.md")).toBe(true);
|
|
expect(fileSet.has("src/plugin.ts")).toBe(true);
|
|
expect(fileSet.has("tests/plugin.test.ts")).toBe(true);
|
|
});
|
|
|
|
it("produces a manifest that the loader accepts", async () => {
|
|
await scaffold({ targetDir, inputs: FULL_INPUTS, force: false });
|
|
const { manifest } = await loadManifest(targetDir);
|
|
expect(manifest.slug).toBe("gallery");
|
|
// `version` is intentionally omitted from the scaffold manifest;
|
|
// the build reads it from package.json instead.
|
|
expect(manifest.version).toBeUndefined();
|
|
expect(manifest.publisher).toBe("did:plc:abc123def456");
|
|
expect(manifest.license).toBe("MIT");
|
|
});
|
|
|
|
it("produces a minimal manifest that round-trips through the loader (with empty publisher)", async () => {
|
|
// Minimal scaffold writes TODO placeholders. The loader's JSONC
|
|
// parse succeeds; the schema rejects on `publisher` being empty.
|
|
// We catch that explicitly so the user knows what to fix.
|
|
await scaffold({ targetDir, inputs: MINIMAL_INPUTS, force: false });
|
|
await expect(loadManifest(targetDir)).rejects.toMatchObject({
|
|
name: "ManifestError",
|
|
code: "MANIFEST_VALIDATION_ERROR",
|
|
});
|
|
});
|
|
|
|
it("refuses to overwrite an existing file without --force", async () => {
|
|
// Pre-create one of the target files with different content.
|
|
const { mkdir } = await import("node:fs/promises");
|
|
await mkdir(targetDir, { recursive: true });
|
|
await writeFile(join(targetDir, "package.json"), "{}", "utf8");
|
|
|
|
await expect(scaffold({ targetDir, inputs: FULL_INPUTS, force: false })).rejects.toMatchObject({
|
|
name: "InitError",
|
|
code: "TARGET_FILE_EXISTS",
|
|
conflicts: ["package.json"],
|
|
});
|
|
|
|
// The original file must be untouched.
|
|
const contents = await readFile(join(targetDir, "package.json"), "utf8");
|
|
expect(contents).toBe("{}");
|
|
});
|
|
|
|
it("does not partially write when a conflict is detected", async () => {
|
|
// Same setup as above. The conflict check runs BEFORE any
|
|
// write, so nothing else should appear in the target dir.
|
|
const { mkdir } = await import("node:fs/promises");
|
|
await mkdir(targetDir, { recursive: true });
|
|
await writeFile(join(targetDir, "package.json"), "{}", "utf8");
|
|
|
|
await expect(scaffold({ targetDir, inputs: FULL_INPUTS, force: false })).rejects.toThrow(
|
|
InitError,
|
|
);
|
|
|
|
const entries = await readdir(targetDir);
|
|
// Only the pre-existing file should be there.
|
|
expect(entries).toEqual(["package.json"]);
|
|
});
|
|
|
|
it("overwrites existing files when --force is set", async () => {
|
|
const { mkdir } = await import("node:fs/promises");
|
|
await mkdir(targetDir, { recursive: true });
|
|
await writeFile(join(targetDir, "package.json"), "{}", "utf8");
|
|
|
|
await scaffold({ targetDir, inputs: FULL_INPUTS, force: true });
|
|
const contents = await readFile(join(targetDir, "package.json"), "utf8");
|
|
// The scaffold wrote the real package.json over the stub.
|
|
const parsed = JSON.parse(contents) as { name: string };
|
|
expect(parsed.name).toBe("gallery");
|
|
});
|
|
|
|
it("creates intermediate directories (src/, tests/)", async () => {
|
|
await scaffold({ targetDir, inputs: FULL_INPUTS, force: false });
|
|
const srcStat = await stat(join(targetDir, "src"));
|
|
const testsStat = await stat(join(targetDir, "tests"));
|
|
expect(srcStat.isDirectory()).toBe(true);
|
|
expect(testsStat.isDirectory()).toBe(true);
|
|
});
|
|
|
|
it("invokes onFileWritten once per file in scaffold order", async () => {
|
|
const calls: string[] = [];
|
|
await scaffold({
|
|
targetDir,
|
|
inputs: FULL_INPUTS,
|
|
force: false,
|
|
onFileWritten: (rel) => calls.push(rel),
|
|
});
|
|
// The seven files, in some deterministic order (see FILES in
|
|
// scaffold.ts). The exact order is part of the API surface
|
|
// for CLI progress output.
|
|
expect(calls).toEqual([
|
|
"emdash-plugin.jsonc",
|
|
"package.json",
|
|
"tsconfig.json",
|
|
".gitignore",
|
|
"README.md",
|
|
"src/plugin.ts",
|
|
"tests/plugin.test.ts",
|
|
]);
|
|
});
|
|
});
|