mirror of
https://github.com/earendil-works/pi.git
synced 2026-09-28 14:13:13 +08:00
Separate stable and development entrypoints, exclude remote harness code from distributions, and verify isolated consumer installs before release. Fixes #9132.
111 lines
4.9 KiB
JavaScript
111 lines
4.9 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { spawnSync } from "node:child_process";
|
|
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import test from "node:test";
|
|
|
|
const script = fileURLToPath(new URL("./check-runtime-deps.mjs", import.meta.url));
|
|
|
|
async function check(t, manifest, source, extraFiles = {}) {
|
|
const root = await mkdtemp(join(tmpdir(), "pi-runtime-deps-"));
|
|
t.after(() => rm(root, { recursive: true, force: true }));
|
|
const files = {
|
|
"packages/example/package.json": JSON.stringify({ name: "example", version: "1.0.0", ...manifest }),
|
|
"packages/example/src/index.ts": source,
|
|
...extraFiles,
|
|
};
|
|
for (const [path, contents] of Object.entries(files)) {
|
|
const fullPath = join(root, path);
|
|
await mkdir(join(fullPath, ".."), { recursive: true });
|
|
await writeFile(fullPath, contents);
|
|
}
|
|
return spawnSync(process.execPath, [script], { cwd: root, encoding: "utf8" });
|
|
}
|
|
|
|
// #9132: workspace resolution and installing every release package masked a missing runtime dependency.
|
|
test("rejects undeclared imports even when the workspace package exists", async (t) => {
|
|
const result = await check(t, {}, 'export { createUnixServer } from "@earendil-works/pi-server/unix";', {
|
|
"packages/server/package.json": JSON.stringify({ name: "@earendil-works/pi-server", version: "1.0.0" }),
|
|
});
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /src[\\/]index\.ts:1: @earendil-works\/pi-server\/unix is not declared/);
|
|
});
|
|
|
|
test("accepts runtime declarations, builtins, self imports, relative imports, and erased types", async (t) => {
|
|
const result = await check(t, {
|
|
dependencies: { "@scope/runtime": "1.0.0" },
|
|
optionalDependencies: { optional: "1.0.0" },
|
|
peerDependencies: { peer: "1.0.0" },
|
|
}, `
|
|
import "node:fs";
|
|
import "fs/promises";
|
|
import "./local.ts";
|
|
import "example/subpath";
|
|
import { value, type T } from "@scope/runtime/subpath";
|
|
import optional from "optional";
|
|
export * from "peer";
|
|
import type { Type } from "type-only";
|
|
import { type OtherType } from "inline-type-only";
|
|
export type { Type } from "export-type-only";
|
|
export { type OtherType } from "export-inline-type-only";
|
|
`);
|
|
assert.equal(result.status, 0, result.stderr);
|
|
});
|
|
|
|
test("rejects dev-only dependencies, side-effect imports, mixed exports, and literal runtime loads", async (t) => {
|
|
const result = await check(t, { devDependencies: { dev: "1.0.0" } }, `
|
|
import "dev";
|
|
import {} from "empty-import";
|
|
export {} from "empty-export";
|
|
export { type T, value } from "mixed-export";
|
|
const lazy = () => import("lazy/subpath");
|
|
const required = require("required");
|
|
const resolved = require.resolve("resolved/subpath");
|
|
`);
|
|
assert.equal(result.status, 1);
|
|
for (const name of ["dev", "empty-import", "empty-export", "mixed-export", "lazy/subpath", "required", "resolved/subpath"]) {
|
|
assert.ok(result.stderr.includes(`${name} is not declared`), result.stderr);
|
|
}
|
|
});
|
|
|
|
test("allows imported JSON assets outside the TypeScript include pattern", async (t) => {
|
|
const result = await check(t, {}, 'import data from "./data.json";', {
|
|
"packages/example/tsconfig.build.json": JSON.stringify({ include: ["src/**/*.ts"], compilerOptions: { resolveJsonModule: true } }),
|
|
"packages/example/src/data.json": "{}",
|
|
});
|
|
assert.equal(result.status, 0, result.stderr);
|
|
});
|
|
|
|
test("permits dev-only dependencies in sources excluded from the published build", async (t) => {
|
|
const result = await check(t, { devDependencies: { server: "1.0.0" } }, "", {
|
|
"packages/example/tsconfig.build.json": JSON.stringify({ include: ["src/**/*.ts"], exclude: ["src/experimental"] }),
|
|
"packages/example/src/experimental/server.ts": 'import "server";',
|
|
});
|
|
assert.equal(result.status, 0, result.stderr);
|
|
});
|
|
|
|
// #9132: an experimental flag cannot prevent module resolution through a public entrypoint.
|
|
for (const statement of ['export * from "./experimental/server";', 'import type { Options } from "./experimental/server";']) {
|
|
test(`rejects excluded code reachable through ${statement}`, async (t) => {
|
|
const result = await check(t, { devDependencies: { server: "1.0.0" } }, statement, {
|
|
"packages/example/tsconfig.build.json": JSON.stringify({ include: ["src/**/*.ts"], exclude: ["src/experimental"] }),
|
|
"packages/example/src/experimental/server.ts": 'import "server"; export interface Options {}',
|
|
});
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /is excluded from example's build but imported by it/);
|
|
assert.match(result.stderr, /server is not declared/);
|
|
});
|
|
}
|
|
|
|
test("ignores tests, declarations, and private packages", async (t) => {
|
|
const result = await check(t, {}, "", {
|
|
"packages/example/test/test.ts": 'import "test-only";',
|
|
"packages/example/src/index.d.ts": 'import "declaration-only";',
|
|
"packages/private/package.json": JSON.stringify({ name: "private", private: true }),
|
|
"packages/private/src/index.ts": 'import "private-only";',
|
|
});
|
|
assert.equal(result.status, 0, result.stderr);
|
|
});
|