diff --git a/package.json b/package.json index 566099bb41..c332c9fc03 100644 --- a/package.json +++ b/package.json @@ -97,5 +97,8 @@ "@types/react-dom": "^19.0.0", "react": "^19.0.0", "react-dom": "^19.0.0" + }, + "engines": { + "bun": "1.4.4" } } diff --git a/scripts/verify-packed-manifests.mjs b/scripts/verify-packed-manifests.mjs index bc57ed57d7..fc6ea3a09a 100644 --- a/scripts/verify-packed-manifests.mjs +++ b/scripts/verify-packed-manifests.mjs @@ -479,7 +479,15 @@ function writeConsumerFixture(packDir, packedWorkspaces) { export const CONSUMER_MIN_RELEASE_AGE_SECONDS = 24 * 60 * 60; const BLOCKED_BY_AGE = - /No version matching "[^"]+" found for specifier "([^"]+)" \(blocked by minimum-release-age/g; + /No version matching "([^"]+)" found for specifier "([^"]+)" \(blocked by minimum-release-age/g; + +/** Packages bun refused as too new, or null when its output does not pair a name with a range. */ +export function ageBlockedPackages(stderr) { + const names = [...stderr.matchAll(BLOCKED_BY_AGE)].map(([, a, b]) => + [a, b].find((name) => stderr.includes(`${name}@${name === a ? b : a} failed to resolve`)), + ); + return names.length && names.every(Boolean) ? names : null; +} export function installConsumer(dir, { args = [], env = process.env } = {}) { const install = () => @@ -498,8 +506,8 @@ export function installConsumer(dir, { args = [], env = process.env } = {}) { } catch (error) { // A same-day dependency bump: every version its range allows is younger than the age, and the repo // installed it already, so its tarball is served. Only those packages skip the age. - const blocked = [...`${error.stderr ?? ""}`.matchAll(BLOCKED_BY_AGE)].map((m) => m[1]); - if (!blocked.length) throw error; + const blocked = ageBlockedPackages(`${error.stderr ?? ""}`); + if (!blocked) throw error; console.log(`Consumer install: newer than the release age, allowed: ${blocked.join(", ")}`); writeFileSync( join(dir, "bunfig.toml"), diff --git a/scripts/verify-packed-manifests.test.mjs b/scripts/verify-packed-manifests.test.mjs index fb05b1a15c..06af0d7abe 100644 --- a/scripts/verify-packed-manifests.test.mjs +++ b/scripts/verify-packed-manifests.test.mjs @@ -7,6 +7,7 @@ import { dirname, join } from "node:path"; import { describe, it } from "node:test"; import { Worker } from "node:worker_threads"; import { + ageBlockedPackages, CONSUMER_TSCONFIG, countTypeErrorsByPackage, installConsumer, @@ -353,6 +354,30 @@ async function withRegistry(name, versions, run) { } } +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) {