mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
461 lines
17 KiB
JavaScript
461 lines
17 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { execFileSync } from "node:child_process";
|
|
import { createHash } from "node:crypto";
|
|
import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, join } from "node:path";
|
|
import { describe, it } from "node:test";
|
|
import { Worker } from "node:worker_threads";
|
|
import {
|
|
ageBlockedPackages,
|
|
CONSUMER_TSCONFIG,
|
|
countTypeErrorsByPackage,
|
|
installConsumer,
|
|
keepPackedTarballs,
|
|
listPackedExportContracts,
|
|
listPackageSizeIssues,
|
|
listPackedJavaScriptImportIssues,
|
|
packageExportSpecifier,
|
|
renderBrowserConsumer,
|
|
renderTypeConsumer,
|
|
verifyCliLicense,
|
|
} from "./verify-packed-manifests.mjs";
|
|
|
|
const TSC = join(import.meta.dirname, "..", "node_modules", "typescript", "bin", "tsc");
|
|
|
|
describe("packed manifest verifier", () => {
|
|
it("passes a package inside its size budget", () => {
|
|
const budget = { packed: 1000, unpacked: 4000 };
|
|
assert.deepEqual(listPackageSizeIssues("pkg", { packed: 950, unpacked: 3900 }, budget), []);
|
|
});
|
|
|
|
it("passes a package a few hundred bytes over its budget, within 1%", () => {
|
|
const budget = { packed: 73369, unpacked: 266805 };
|
|
// The sizes the 0.8.187 publish job measured for aws-lambda: 115 B and 208 B over.
|
|
assert.deepEqual(listPackageSizeIssues("pkg", { packed: 73484, unpacked: 267013 }, budget), []);
|
|
assert.deepEqual(
|
|
listPackageSizeIssues(
|
|
"pkg",
|
|
{ packed: 1010, unpacked: 4040 },
|
|
{
|
|
packed: 1000,
|
|
unpacked: 4000,
|
|
},
|
|
),
|
|
[],
|
|
);
|
|
});
|
|
|
|
it("fails a package more than 1% over its size budget", () => {
|
|
const issues = listPackageSizeIssues(
|
|
"pkg",
|
|
{ packed: 1011, unpacked: 3900 },
|
|
{ packed: 1000, unpacked: 4000 },
|
|
);
|
|
assert.equal(issues.length, 1);
|
|
assert.match(
|
|
issues[0],
|
|
/pkg packed size 1011 bytes is over its budget of 1000 by more than 1%/,
|
|
);
|
|
});
|
|
|
|
it("makes a budget follow a package down once it shrinks", () => {
|
|
const issues = listPackageSizeIssues(
|
|
"pkg",
|
|
{ packed: 500, unpacked: 3900 },
|
|
{ packed: 1000, unpacked: 4000 },
|
|
);
|
|
assert.equal(issues.length, 1);
|
|
assert.match(issues[0], /lower it to 525/);
|
|
});
|
|
|
|
it("fails a malformed budget instead of skipping the metric", () => {
|
|
const sizes = { packed: 100, unpacked: 400 };
|
|
for (const budget of [{}, { paked: 100, unpacked: 400 }, { packed: "NaN", unpacked: 400 }]) {
|
|
assert.match(listPackageSizeIssues("pkg", sizes, budget).join("\n"), /invalid packed budget/);
|
|
}
|
|
});
|
|
|
|
it("asks for a budget for a new published package", () => {
|
|
assert.match(
|
|
listPackageSizeIssues("pkg", { packed: 100, unpacked: 400 }, undefined)[0],
|
|
/"pkg": \{ "packed": 105, "unpacked": 420 \}/,
|
|
);
|
|
});
|
|
|
|
it("requires the CLI package and tarball to declare Apache-2.0", () => {
|
|
assert.throws(
|
|
() => verifyCliLicense("packages/cli", {}, {}),
|
|
/packages\/cli must declare license Apache-2\.0/,
|
|
);
|
|
assert.throws(
|
|
() => verifyCliLicense("packages/cli", { license: "Apache-2.0" }, { license: "UNLICENSED" }),
|
|
/packages\/cli packed manifest must preserve license Apache-2\.0/,
|
|
);
|
|
assert.doesNotThrow(() =>
|
|
verifyCliLicense("packages/cli", { license: "Apache-2.0" }, { license: "Apache-2.0" }),
|
|
);
|
|
});
|
|
|
|
it("keeps every browser package live even when it declares sideEffects false", () => {
|
|
const consumer = renderBrowserConsumer(["@hyperframes/parsers", "@hyperframes/lint/browser"]);
|
|
|
|
assert.match(consumer, /import \* as packedBrowserModule0 from "@hyperframes\/parsers";/);
|
|
assert.match(consumer, /import \* as packedBrowserModule1 from "@hyperframes\/lint\/browser";/);
|
|
assert.match(consumer, /packedBrowserModules\.map\(\(module\) => Object\.keys\(module\)\)/);
|
|
assert.doesNotMatch(consumer, /import "@hyperframes\/parsers"/);
|
|
});
|
|
|
|
it("derives consumer specifiers from the packed export map", () => {
|
|
assert.equal(packageExportSpecifier("@hyperframes/sdk", "."), "@hyperframes/sdk");
|
|
assert.equal(
|
|
packageExportSpecifier("@hyperframes/sdk", "./adapters/fs"),
|
|
"@hyperframes/sdk/adapters/fs",
|
|
);
|
|
assert.deepEqual(
|
|
listPackedExportContracts([
|
|
{
|
|
packedPackage: {
|
|
name: "@hyperframes/example",
|
|
exports: {
|
|
".": { import: "./dist/index.js", types: "./dist/index.d.ts" },
|
|
"./runtime": "./dist/runtime.js",
|
|
},
|
|
},
|
|
},
|
|
]),
|
|
[
|
|
{
|
|
specifier: "@hyperframes/example",
|
|
typechecked: true,
|
|
environments: ["browser", "node"],
|
|
},
|
|
{
|
|
specifier: "@hyperframes/example/runtime",
|
|
typechecked: false,
|
|
environments: ["browser", "node"],
|
|
},
|
|
],
|
|
);
|
|
});
|
|
|
|
it("uses descriptor environments to separate Node and browser promises", () => {
|
|
assert.deepEqual(
|
|
listPackedExportContracts([
|
|
{
|
|
packedPackage: {
|
|
name: "@hyperframes/example",
|
|
exports: { ".": { import: "./dist/index.js", types: "./dist/index.d.ts" } },
|
|
},
|
|
descriptor: {
|
|
subpaths: {
|
|
".": { environments: ["browser"] },
|
|
},
|
|
},
|
|
},
|
|
]),
|
|
[
|
|
{
|
|
specifier: "@hyperframes/example",
|
|
typechecked: true,
|
|
environments: ["browser"],
|
|
},
|
|
],
|
|
);
|
|
});
|
|
|
|
function withPackedFiles(files, packedFiles, callback) {
|
|
const dir = mkdtempSync(join(tmpdir(), "hyperframes-pack-test-"));
|
|
try {
|
|
const packageDir = join(dir, "package");
|
|
mkdirSync(packageDir, { recursive: true });
|
|
for (const [file, source] of Object.entries(files)) {
|
|
mkdirSync(dirname(join(packageDir, file)), { recursive: true });
|
|
writeFileSync(join(packageDir, file), source, "utf8");
|
|
}
|
|
|
|
const tarball = join(dir, "package.tgz");
|
|
execFileSync("tar", ["-czf", tarball, "-C", dir, "package"]);
|
|
callback(tarball, new Set(packedFiles));
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
it("passes explicit relative JavaScript imports whose target is packed", () => {
|
|
withPackedFiles(
|
|
{
|
|
"dist/index.js": 'import { runtime } from "./generated/runtime-inline.js";\n',
|
|
"dist/generated/runtime-inline.js": "export const runtime = 'ok';\n",
|
|
},
|
|
["dist/index.js", "dist/generated/runtime-inline.js"],
|
|
(tarball, packedFiles) => {
|
|
assert.deepEqual(listPackedJavaScriptImportIssues(tarball, packedFiles), []);
|
|
},
|
|
);
|
|
});
|
|
|
|
it("reports explicit relative JavaScript imports whose target is missing from the tarball", () => {
|
|
withPackedFiles(
|
|
{
|
|
"dist/index.js": 'import { runtime } from "./generated/runtime-inline.js";\n',
|
|
},
|
|
["dist/index.js"],
|
|
(tarball, packedFiles) => {
|
|
assert.deepEqual(listPackedJavaScriptImportIssues(tarball, packedFiles), [
|
|
"dist/index.js:1 imports missing ./generated/runtime-inline.js",
|
|
]);
|
|
},
|
|
);
|
|
});
|
|
|
|
it("checks export-from, dynamic import, require, and mjs/cjs files", () => {
|
|
withPackedFiles(
|
|
{
|
|
"dist/index.js": 'export * from "./generated/exports.js";\n',
|
|
"dist/dynamic.mjs": 'await import("./generated/dynamic.js");\n',
|
|
"dist/require.cjs": 'require("./generated/require.js");\n',
|
|
},
|
|
["dist/index.js", "dist/dynamic.mjs", "dist/require.cjs"],
|
|
(tarball, packedFiles) => {
|
|
assert.deepEqual(listPackedJavaScriptImportIssues(tarball, packedFiles), [
|
|
"dist/index.js:1 imports missing ./generated/exports.js",
|
|
"dist/dynamic.mjs:1 imports missing ./generated/dynamic.js",
|
|
"dist/require.cjs:1 imports missing ./generated/require.js",
|
|
]);
|
|
},
|
|
);
|
|
});
|
|
|
|
it("reports extensionless relative imports", () => {
|
|
withPackedFiles(
|
|
{
|
|
"dist/index.js": 'export {\n runtime\n} from "./generated/runtime-inline";\n',
|
|
},
|
|
["dist/index.js"],
|
|
(tarball, packedFiles) => {
|
|
assert.deepEqual(listPackedJavaScriptImportIssues(tarball, packedFiles), [
|
|
"dist/index.js:1 imports ./generated/runtime-inline",
|
|
]);
|
|
},
|
|
);
|
|
});
|
|
|
|
it("reports side-effect imports whose target is missing from the tarball", () => {
|
|
withPackedFiles(
|
|
{
|
|
"index.js": 'import "./missing.js";\n',
|
|
},
|
|
["index.js"],
|
|
(tarball, packedFiles) => {
|
|
assert.deepEqual(listPackedJavaScriptImportIssues(tarball, packedFiles), [
|
|
"index.js:1 imports missing ./missing.js",
|
|
]);
|
|
},
|
|
);
|
|
});
|
|
|
|
it("names each typed export so tsc must resolve and check its declarations", () => {
|
|
assert.equal(
|
|
renderTypeConsumer(["@hyperframes/core", "@hyperframes/core/lint"]),
|
|
'import type * as packedTypes0 from "@hyperframes/core";\n' +
|
|
'import type * as packedTypes1 from "@hyperframes/core/lint";\n' +
|
|
"export {};\n",
|
|
);
|
|
});
|
|
|
|
it("attributes consumer type errors to the installed package that owns them", () => {
|
|
const output = [
|
|
"node_modules/@hyperframes/core/dist/adapters/gsap.d.ts(1,35): error TS2835: Relative import paths need explicit file extensions.",
|
|
"node_modules/@hyperframes/core/dist/index.d.ts(2,15): error TS2307: Cannot find module './core.types'.",
|
|
"node_modules/hyperframes/dist/cli.d.ts(1,1): error TS2307: Cannot find module './x'.",
|
|
"consumer.ts(1,30): error TS2307: Cannot find module '@hyperframes/missing'.",
|
|
" Did you mean './types.js'?",
|
|
].join("\n");
|
|
assert.deepEqual(countTypeErrorsByPackage(output), {
|
|
"@hyperframes/core": 2,
|
|
hyperframes: 1,
|
|
"consumer.ts": 1,
|
|
});
|
|
});
|
|
|
|
function typecheckConsumerOf(declaration) {
|
|
const dir = mkdtempSync(join(tmpdir(), "hyperframes-nodenext-test-"));
|
|
try {
|
|
const pkgDir = join(dir, "node_modules", "@hyperframes", "example");
|
|
mkdirSync(join(pkgDir, "dist"), { recursive: true });
|
|
writeFileSync(
|
|
join(pkgDir, "package.json"),
|
|
JSON.stringify({
|
|
name: "@hyperframes/example",
|
|
type: "module",
|
|
exports: { ".": { types: "./dist/index.d.ts", import: "./dist/index.js" } },
|
|
}),
|
|
);
|
|
writeFileSync(join(pkgDir, "dist", "index.d.ts"), declaration);
|
|
writeFileSync(join(pkgDir, "dist", "types.d.ts"), "export type Example = string;\n");
|
|
writeFileSync(join(dir, "consumer.ts"), renderTypeConsumer(["@hyperframes/example"]));
|
|
// The fixture has no @types/node to load; everything else matches the packed gate.
|
|
const compilerOptions = { ...CONSUMER_TSCONFIG.compilerOptions, types: [] };
|
|
writeFileSync(
|
|
join(dir, "tsconfig.json"),
|
|
JSON.stringify({ ...CONSUMER_TSCONFIG, compilerOptions }),
|
|
);
|
|
execFileSync(process.execPath, [TSC], { cwd: dir, encoding: "utf8", stdio: "pipe" });
|
|
return "";
|
|
} catch (error) {
|
|
return String(error.stdout);
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
it("fails a NodeNext consumer on an extensionless import inside a published declaration", () => {
|
|
assert.equal(CONSUMER_TSCONFIG.compilerOptions.skipLibCheck, false);
|
|
assert.match(
|
|
typecheckConsumerOf('export type { Example } from "./types";\n'),
|
|
/node_modules\/@hyperframes\/example\/dist\/index\.d\.ts\(1,\d+\): error TS283[45]/,
|
|
);
|
|
assert.equal(typecheckConsumerOf('export type { Example } from "./types.js";\n'), "");
|
|
});
|
|
});
|
|
|
|
// The install is synchronous, so the registry answers from a worker thread.
|
|
const REGISTRY = `
|
|
const { createServer } = require("node:http");
|
|
const { parentPort, workerData } = require("node:worker_threads");
|
|
const { packuments, tarballs } = workerData;
|
|
const server = createServer((req, res) => {
|
|
const path = decodeURIComponent(req.url.slice(1));
|
|
const body = path.includes("/-/") ? tarballs[path] : packuments[path];
|
|
if (!body) return res.writeHead(404).end();
|
|
if (typeof body !== "string") return res.writeHead(200).end(Buffer.from(body));
|
|
res.writeHead(200).end(body.replaceAll("{{origin}}", "http://" + req.headers.host));
|
|
});
|
|
server.listen(0, "127.0.0.1", () => parentPort.postMessage(server.address().port));
|
|
`;
|
|
|
|
function packTarball(dir, name, version) {
|
|
const pkgDir = join(dir, `${name}-${version}`, "package");
|
|
mkdirSync(pkgDir, { recursive: true });
|
|
writeFileSync(join(pkgDir, "package.json"), JSON.stringify({ name, version }));
|
|
const file = join(dir, `${name}-${version}.tgz`);
|
|
execFileSync("tar", ["-czf", file, "-C", dirname(pkgDir), "package"]);
|
|
return readFileSync(file);
|
|
}
|
|
|
|
/** `versions`: version -> { daysOld, served }; a version not served lists but its tarball 404s. */
|
|
async function withRegistry(name, versions, run) {
|
|
const dir = mkdtempSync(join(tmpdir(), "fake-registry-"));
|
|
const tarballs = {};
|
|
const packument = { name, "dist-tags": {}, versions: {}, time: {} };
|
|
for (const [version, { daysOld, served }] of Object.entries(versions)) {
|
|
const tgz = packTarball(dir, name, version);
|
|
const integrity = `sha512-${createHash("sha512").update(tgz).digest("base64")}`;
|
|
const path = `${name}/-/${name}-${version}.tgz`;
|
|
if (served) tarballs[path] = tgz;
|
|
packument.versions[version] = {
|
|
name,
|
|
version,
|
|
dist: { tarball: `{{origin}}/${path}`, integrity },
|
|
};
|
|
packument.time[version] = new Date(Date.now() - daysOld * 86_400_000).toISOString();
|
|
packument["dist-tags"].latest = version;
|
|
}
|
|
const worker = new Worker(REGISTRY, {
|
|
eval: true,
|
|
workerData: { packuments: { [name]: JSON.stringify(packument) }, tarballs },
|
|
});
|
|
try {
|
|
const port = await new Promise((resolve) => worker.once("message", resolve));
|
|
return await run(`http://127.0.0.1:${port}`, dir);
|
|
} finally {
|
|
await worker.terminate();
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
describe("ageBlockedPackages", () => {
|
|
const blockedBy = (first, second, name, range) =>
|
|
`Resolving dependencies\nerror: No version matching "${first}" found for specifier "${second}" (blocked by minimum-release-age: 86400 seconds)\nerror: ${name}@${range} failed to resolve\n`;
|
|
|
|
it("names the package whichever order bun quotes it in", () => {
|
|
assert.deepEqual(ageBlockedPackages(blockedBy("fake-dep", "^1.1.0", "fake-dep", "^1.1.0")), [
|
|
"fake-dep",
|
|
]);
|
|
assert.deepEqual(ageBlockedPackages(blockedBy("^1.1.0", "fake-dep", "fake-dep", "^1.1.0")), [
|
|
"fake-dep",
|
|
]);
|
|
assert.deepEqual(
|
|
ageBlockedPackages(blockedBy("^2.0.1", "@scope/pkg", "@scope/pkg", "^2.0.1")),
|
|
["@scope/pkg"],
|
|
);
|
|
});
|
|
|
|
it("names none when bun's output does not say which token is the package", () => {
|
|
const unpaired = `error: No version matching "^1.1.0" found for specifier "fake-dep" (blocked by minimum-release-age: 86400 seconds)\n`;
|
|
assert.equal(ageBlockedPackages(unpaired), null);
|
|
assert.equal(ageBlockedPackages("error: fake-dep@^9.0.0 failed to resolve\n"), null);
|
|
});
|
|
});
|
|
|
|
describe("packed consumer install", () => {
|
|
/** Installs `deps` into a fresh consumer against the registry; returns the installed fake-dep version. */
|
|
function install(registry, dir, deps) {
|
|
const consumer = join(dir, "consumer");
|
|
mkdirSync(consumer);
|
|
writeFileSync(
|
|
join(consumer, "package.json"),
|
|
JSON.stringify({ name: "c", dependencies: deps }),
|
|
);
|
|
const env = { ...process.env, BUN_INSTALL_CACHE_DIR: join(dir, "cache") };
|
|
installConsumer(consumer, { args: ["--registry", registry], env });
|
|
const installed = join(consumer, "node_modules", "fake-dep", "package.json");
|
|
return JSON.parse(readFileSync(installed, "utf8")).version;
|
|
}
|
|
|
|
it("skips a version published minutes ago whose tarball is not served yet", async () => {
|
|
const versions = {
|
|
"1.0.0": { daysOld: 10, served: true },
|
|
"1.1.0": { daysOld: 0, served: false },
|
|
};
|
|
const version = await withRegistry("fake-dep", versions, (registry, dir) =>
|
|
install(registry, dir, { "fake-dep": "^1.0.0" }),
|
|
);
|
|
assert.equal(version, "1.0.0");
|
|
});
|
|
|
|
it("installs a same-day dependency bump whose only matching version is an hour old", async () => {
|
|
const fresh = { daysOld: 1 / 24, served: true };
|
|
for (const versions of [
|
|
{ "1.1.0": fresh },
|
|
{ "1.0.0": { daysOld: 10, served: true }, "1.1.0": fresh },
|
|
]) {
|
|
const version = await withRegistry("fake-dep", versions, (registry, dir) =>
|
|
install(registry, dir, { "fake-dep": "^1.1.0" }),
|
|
);
|
|
assert.equal(version, "1.1.0");
|
|
}
|
|
});
|
|
|
|
it("still fails on a package the registry does not have", async () => {
|
|
const versions = { "1.0.0": { daysOld: 10, served: true } };
|
|
await withRegistry("fake-dep", versions, (registry, dir) =>
|
|
assert.throws(() => install(registry, dir, { "missing-dep": "^1.0.0" }), /missing-dep/),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("keepPackedTarballs", () => {
|
|
it("copies the tarballs and nothing else", () => {
|
|
const from = mkdtempSync(join(tmpdir(), "keep-from-"));
|
|
const to = join(mkdtempSync(join(tmpdir(), "keep-to-")), "out");
|
|
writeFileSync(join(from, "a-1.0.0.tgz"), "a");
|
|
writeFileSync(join(from, "notes.txt"), "x");
|
|
keepPackedTarballs(from, to);
|
|
assert.deepEqual(readdirSync(to), ["a-1.0.0.tgz"]);
|
|
assert.equal(readFileSync(join(to, "a-1.0.0.tgz"), "utf8"), "a");
|
|
rmSync(from, { recursive: true, force: true });
|
|
});
|
|
});
|