From 584f144f39a4b559f58f09890d55cf5f62526d14 Mon Sep 17 00:00:00 2001 From: Libby Date: Thu, 12 Nov 2026 19:51:59 +0000 Subject: [PATCH] Range check: build each package's own tagged version too, and bump cultnet-ts to 0.4.0 The declared-range check skipped a dependency's own version, so a package that gained an export while still carrying its latest tag's version passed. cultmesh-ts imported seven names cultnet-ts-v0.3.0 lacks. The check now builds that tag (through the package's own build, with repo contracts), a fixture test pins the failure, and cultnet-ts (Unreleased holds Breaking entries) is 0.4.0 with its dependents' ranges at ^0.4.0. Co-Authored-By: Claude Sonnet 5.5 --- package-lock.json | 6 +- package.json | 2 +- packages/cultmesh-browser/package.json | 2 +- packages/cultmesh-ts/package.json | 2 +- packages/cultnet-ts/package.json | 2 +- scripts/test-declared-dependency-ranges.mjs | 10 +-- .../test-declared-dependency-ranges.test.mjs | 64 +++++++++++++++++++ 7 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 scripts/test-declared-dependency-ranges.test.mjs diff --git a/package-lock.json b/package-lock.json index 0a1558da..e5fbf13b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1205,7 +1205,7 @@ "license": "MIT", "dependencies": { "@gamecult/cultcache-ts": "^0.15.0", - "@gamecult/cultnet-ts": "^0.3.0" + "@gamecult/cultnet-ts": "^0.4.0" }, "devDependencies": { "@gamecult/cultmesh-ts": "^0.2.0", @@ -1222,7 +1222,7 @@ "license": "MIT", "dependencies": { "@gamecult/cultcache-ts": "^0.15.0", - "@gamecult/cultnet-ts": "^0.3.0", + "@gamecult/cultnet-ts": "^0.4.0", "koffi": "3.3.0" }, "devDependencies": { @@ -1236,7 +1236,7 @@ }, "packages/cultnet-ts": { "name": "@gamecult/cultnet-ts", - "version": "0.3.0", + "version": "0.4.0", "license": "MIT", "dependencies": { "@gamecult/cultcache-ts": "^0.15.0", diff --git a/package.json b/package.json index a79661f2..1f883fa1 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "scripts": { "build:ts": "node scripts/run-typescript-workspaces.mjs build", "test:ts": "node scripts/run-typescript-workspaces.mjs test", - "test:pack:ts": "node scripts/test-typescript-package-closure.mjs && node scripts/test-declared-dependency-ranges.mjs", + "test:pack:ts": "node --test scripts/test-declared-dependency-ranges.test.mjs && node scripts/test-typescript-package-closure.mjs && node scripts/test-declared-dependency-ranges.mjs", "test:eve-browser-network": "node scripts/verify-eve-browser-network.mjs" }, "devDependencies": { diff --git a/packages/cultmesh-browser/package.json b/packages/cultmesh-browser/package.json index 0984ae6d..ab770f8c 100644 --- a/packages/cultmesh-browser/package.json +++ b/packages/cultmesh-browser/package.json @@ -42,7 +42,7 @@ }, "dependencies": { "@gamecult/cultcache-ts": "^0.15.0", - "@gamecult/cultnet-ts": "^0.3.0" + "@gamecult/cultnet-ts": "^0.4.0" }, "devDependencies": { "@types/node": "^24.0.0", diff --git a/packages/cultmesh-ts/package.json b/packages/cultmesh-ts/package.json index 39e0abba..7f2f20b6 100644 --- a/packages/cultmesh-ts/package.json +++ b/packages/cultmesh-ts/package.json @@ -50,7 +50,7 @@ }, "dependencies": { "@gamecult/cultcache-ts": "^0.15.0", - "@gamecult/cultnet-ts": "^0.3.0", + "@gamecult/cultnet-ts": "^0.4.0", "koffi": "3.3.0" }, "devDependencies": { diff --git a/packages/cultnet-ts/package.json b/packages/cultnet-ts/package.json index 60c9e13f..27ba3be4 100644 --- a/packages/cultnet-ts/package.json +++ b/packages/cultnet-ts/package.json @@ -1,6 +1,6 @@ { "name": "@gamecult/cultnet-ts", - "version": "0.3.0", + "version": "0.4.0", "description": "CultNet-style typed direct-pipe messaging and state replication for TypeScript runtimes.", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/scripts/test-declared-dependency-ranges.mjs b/scripts/test-declared-dependency-ranges.mjs index b528fb75..40935e39 100644 --- a/scripts/test-declared-dependency-ranges.mjs +++ b/scripts/test-declared-dependency-ranges.mjs @@ -22,6 +22,7 @@ import { fileURLToPath } from "node:url"; const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const rootRequire = createRequire(join(repoRoot, "package.json")); const ts = rootRequire("typescript"); +const npmCli = join(npmRoot(), "bin", "npm-cli.js"); const semver = createRequire(join(npmRoot(), "package.json"))("semver"); // npm ships semver, so the check needs no dependency of its own. npm's root is beside the node binary on Windows and under lib/ on Unix. @@ -84,16 +85,17 @@ function sourceFiles(directory) { }); } -// Builds the package as the tag released it, in a scratch directory that borrows the workspace's node_modules. +// Builds the package as the tag released it (with the repo-level contracts its generator reads), in a scratch directory that borrows the workspace's node_modules. function buildRelease(directory, tag, scratch) { const archive = join(scratch, `${tag}.tar`); - git("archive", "--format=tar", "-o", archive, tag, `packages/${directory}`); + git("archive", "--format=tar", "-o", archive, tag, `packages/${directory}`, "contracts"); const extracted = join(scratch, tag); mkdirSync(extracted); execFileSync("tar", ["-xf", archive, "-C", extracted]); symlinkSync(join(repoRoot, "node_modules"), join(extracted, "node_modules"), "junction"); const packageRoot = join(extracted, "packages", directory); - execFileSync(process.execPath, [rootRequire.resolve("typescript/bin/tsc"), "-p", "tsconfig.json"], { cwd: packageRoot, stdio: "inherit" }); + // The package's own build, which is how a release was made: cultnet-ts generates its contracts module first. + execFileSync(process.execPath, [npmCli, "run", "build", "--silent"], { cwd: packageRoot, stdio: "inherit" }); return rootRequire(join(packageRoot, "dist", "index.js")); } @@ -127,7 +129,7 @@ try { const prefix = `${dependency.directory}-v`; const admitted = tags.filter((tag) => { const version = tag.startsWith(prefix) ? tag.slice(prefix.length) : undefined; - return version !== undefined && semver.valid(version) !== null && semver.satisfies(version, range) && version !== dependency.manifest.version; + return version !== undefined && semver.valid(version) !== null && semver.satisfies(version, range); }); for (const tag of admitted) { if (!built.has(tag)) { diff --git a/scripts/test-declared-dependency-ranges.test.mjs b/scripts/test-declared-dependency-ranges.test.mjs new file mode 100644 index 00000000..10e6341d --- /dev/null +++ b/scripts/test-declared-dependency-ranges.test.mjs @@ -0,0 +1,64 @@ +// test-declared-dependency-ranges.mjs against a fixture repo: a dependency that gained an export a dependent imports, +// while its manifest still carries the version of its latest release tag, must fail the check, because a consumer's +// range admits that tagged release and it lacks the export. Bumping the version and the range makes it pass. +import assert from "node:assert/strict"; +import { execFileSync, spawnSync } from "node:child_process"; +import { copyFileSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const script = join(repoRoot, "scripts", "test-declared-dependency-ranges.mjs"); + +const write = (root, path, text) => { + mkdirSync(dirname(join(root, path)), { recursive: true }); + writeFileSync(join(root, path), text); +}; +const git = (root, ...args) => execFileSync("git", ["-c", "user.name=t", "-c", "user.email=t@t", ...args], { cwd: root, stdio: "pipe" }); +const manifest = (name, version, extra = {}) => + JSON.stringify({ name: `@t/${name}`, version, scripts: { build: "node ../../node_modules/typescript/bin/tsc -p tsconfig.json" }, ...extra }); +const tsconfig = JSON.stringify({ compilerOptions: { outDir: "dist", module: "commonjs", target: "es2020", declaration: false }, include: ["src"] }); + +function fixture() { + const root = mkdtempSync(join(tmpdir(), "declared-ranges-fixture-")); + write(root, "package.json", JSON.stringify({ name: "fixture", private: true, workspaces: ["packages/a", "packages/b"] })); + write(root, "contracts/.keep", ""); + write(root, "packages/a/package.json", manifest("a", "1.0.0")); + write(root, "packages/a/tsconfig.json", tsconfig); + write(root, "packages/a/src/index.ts", "export const foo = 1;\n"); + write(root, "packages/b/package.json", manifest("b", "1.0.0", { dependencies: { "@t/a": "^1.0.0" } })); + write(root, "packages/b/tsconfig.json", tsconfig); + write(root, "packages/b/src/index.ts", 'import { foo } from "@t/a";\nexport const b = foo;\n'); + mkdirSync(join(root, "scripts")); + copyFileSync(script, join(root, "scripts", "test-declared-dependency-ranges.mjs")); + symlinkSync(join(repoRoot, "node_modules"), join(root, "node_modules"), "junction"); + git(root, "init", "-q", "-b", "main"); + git(root, "add", "package.json", "contracts", "packages", "scripts"); + git(root, "commit", "-q", "-m", "release a 1.0.0"); + git(root, "tag", "a-v1.0.0"); + return root; +} + +const check = (root) => spawnSync(process.execPath, [join(root, "scripts", "test-declared-dependency-ranges.mjs")], { cwd: root, encoding: "utf8" }); + +test("a dependency that gained an export a dependent imports, still at its latest tag's version, fails the check", () => { + const root = fixture(); + try { + assert.equal(check(root).status, 0, "the fixture passes before the export is added"); + + write(root, "packages/a/src/index.ts", "export const foo = 1;\nexport const bar = 2;\n"); + write(root, "packages/b/src/index.ts", 'import { foo, bar } from "@t/a";\nexport const b = foo + bar;\n'); + const stale = check(root); + assert.notEqual(stale.status, 0, stale.stdout); + assert.match(stale.stderr, /a-v1\.0\.0 does not export bar/u); + + write(root, "packages/a/package.json", manifest("a", "1.1.0")); + write(root, "packages/b/package.json", manifest("b", "1.0.0", { dependencies: { "@t/a": "^1.1.0" } })); + const bumped = check(root); + assert.equal(bumped.status, 0, bumped.stderr); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); -- 2.47.3