All checks were successful
check / check (push) Successful in 5s
rootDir was ./src while include also matched bin/**/*, which is TS6059: tsc refuses to emit at all when a compiled file sits outside rootDir. rootDir is now the repository root, which is the smallest change that makes the two agree and leaves the source layout the README documents alone. Output keeps the shape of the source tree, so main and types move to dist/src/index.js and dist/src/index.d.ts while bin.quak stays at dist/bin/quak.js. The alternative, moving the CLI body into src/ behind a shim in bin/, would hold main at dist/index.js at the cost of churning the CLI and contradicting the layout diagram in the README. Clearing TS6059 exposed two type errors that had never been reached, because the config error aborts before checking: StateAddress was read as a namespace member off the default import, and the secretstream pull was called without the additional-data argument, which libsodium does not make optional. The type is now taken from the module's named export and the pull passes null for ad, matching the null already passed on the push side in encryptBlob. Neither changes what runs. noEmitOnError stops a failed build from leaving output behind. It emitted despite the error before, which is how a stale bin/quak.js came to sit next to bin/quak.ts in a working tree, where eslint then read it and failed make check on a generated file. script/build compiles and then checks that the files package.json advertises are among the ones the compiler wrote, since tsc knows nothing about the manifest and a green build could still ship a package whose main resolves to nothing. It also sets the executable bit on the bin entries, which tsc does not carry over from the source even though it does copy the shebang. The Makefile target is now a shim over it, as the other targets are, and package.json's build script points at it so yarn build gets the same checks. The Dockerfile runs make build after make check, so a branch that does not compile cannot reach main. What make check itself runs is unchanged. package.json gains a quak script, so the yarn quak commands the README's Getting Started block has always listed resolve to the built CLI.
117 lines
4.8 KiB
TypeScript
117 lines
4.8 KiB
TypeScript
// The package manifest promises three files that only exist after a build:
|
|
// `main`, `types`, and the `quak` binary. Nothing in the test suite used to
|
|
// look at them, and `make check` runs test, lint and fmt-check but never the
|
|
// build, so `tsconfig.json` and `package.json` were free to drift apart. They
|
|
// did: `rootDir` was `./src` while `include` also pulled in `bin/**/*`, which
|
|
// is TS6059, and no build had succeeded for as long as that was true.
|
|
//
|
|
// These tests read both files and check the contract between them, without
|
|
// running a build, so they stay in the fast unit suite. The complementary
|
|
// check — that the files really landed on disk — is in `script/build`, which
|
|
// runs after the compiler and is the only place that can honestly answer it.
|
|
import { describe, expect, it } from "vitest";
|
|
import { readFileSync } from "node:fs";
|
|
import { fileURLToPath } from "node:url";
|
|
import { join, posix } from "node:path";
|
|
|
|
interface TsConfig {
|
|
compilerOptions: {
|
|
outDir: string;
|
|
rootDir: string;
|
|
};
|
|
include: string[];
|
|
}
|
|
|
|
interface PackageJson {
|
|
main: string;
|
|
types: string;
|
|
bin: Record<string, string>;
|
|
scripts: Record<string, string>;
|
|
}
|
|
|
|
const repoRoot = fileURLToPath(new URL("../../", import.meta.url));
|
|
|
|
const readJSON = <T>(name: string): T =>
|
|
JSON.parse(readFileSync(join(repoRoot, name), "utf-8")) as T;
|
|
|
|
const tsconfig = readJSON<TsConfig>("tsconfig.json");
|
|
const pkg = readJSON<PackageJson>("package.json");
|
|
|
|
// Paths in the two manifests are written with a leading "./"; normalize so
|
|
// they can be compared and joined. Everything here is POSIX-style because
|
|
// that is what both JSON files contain, regardless of the host OS.
|
|
const clean = (p: string): string => posix.normalize(p);
|
|
const outDir = clean(tsconfig.compilerOptions.outDir);
|
|
const rootDir = clean(tsconfig.compilerOptions.rootDir);
|
|
|
|
// The directory prefix of a glob: the part before the first segment
|
|
// containing a wildcard. "src/**/*" -> "src", "bin/**/*" -> "bin".
|
|
const globRoot = (pattern: string): string => {
|
|
const segments = clean(pattern).split("/");
|
|
const wildcard = segments.findIndex((s) => s.includes("*"));
|
|
return (
|
|
segments
|
|
.slice(0, wildcard === -1 ? segments.length : wildcard)
|
|
.join("/") || "."
|
|
);
|
|
};
|
|
|
|
// Where tsc will write the output for a source file: the path relative to
|
|
// rootDir, re-rooted under outDir, with the extension swapped.
|
|
const emitted = (source: string, extension: string): string =>
|
|
"./" +
|
|
posix
|
|
.join(outDir, posix.relative(rootDir, clean(source)))
|
|
.replace(/\.ts$/, extension);
|
|
|
|
describe("tsconfig include and rootDir", () => {
|
|
// TS6059 is not a style complaint: tsc refuses to emit anything at all
|
|
// when a compiled file sits outside rootDir, so this single mismatch
|
|
// took out both the library and the CLI artifacts.
|
|
it("compiles only files that live under rootDir", () => {
|
|
for (const pattern of tsconfig.include) {
|
|
const root = globRoot(pattern);
|
|
const relative = posix.relative(rootDir, root);
|
|
expect(
|
|
relative === "" || !relative.startsWith(".."),
|
|
`include pattern ${pattern} matches files outside rootDir ` +
|
|
`${rootDir}; tsc rejects that with TS6059`,
|
|
).toBe(true);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("package.json entrypoints", () => {
|
|
// Each of these is the path a consumer resolves — `import { Client } from
|
|
// "quak"` for main, the editor for types, `npx quak` for the bin — so a
|
|
// wrong value is a broken package even when the build itself is green.
|
|
it("names the file tsc emits for src/index.ts as main", () => {
|
|
expect(pkg.main).toBe(emitted("src/index.ts", ".js"));
|
|
});
|
|
|
|
it("names the declaration tsc emits for src/index.ts as types", () => {
|
|
expect(pkg.types).toBe(emitted("src/index.ts", ".d.ts"));
|
|
});
|
|
|
|
it("names the file tsc emits for bin/quak.ts as the quak binary", () => {
|
|
expect(pkg.bin.quak).toBe(emitted("bin/quak.ts", ".js"));
|
|
});
|
|
|
|
// The README's Getting Started block tells the reader to run
|
|
// `yarn quak login` straight after `yarn build`. That only works if a
|
|
// `quak` script exists and points at the built CLI, not at the source.
|
|
it("runs the built CLI from the quak script", () => {
|
|
expect(pkg.scripts.quak).toBeDefined();
|
|
expect(pkg.scripts.quak).toContain(pkg.bin.quak);
|
|
});
|
|
});
|
|
|
|
describe("bin/quak.ts", () => {
|
|
// tsc copies the shebang into the emitted file, so it has to be in the
|
|
// source for the installed binary to be directly executable.
|
|
it("starts with a node shebang", () => {
|
|
const source = readFileSync(join(repoRoot, "bin/quak.ts"), "utf-8");
|
|
expect(source.split("\n")[0]).toBe("#!/usr/bin/env node");
|
|
});
|
|
});
|