From e3bcddcc8bfe08a07f500c7f1af428b7c26bc908 Mon Sep 17 00:00:00 2001 From: Maximiliano Salvatti <40447063+msalvatti@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:13:15 -0300 Subject: [PATCH 1/3] fix(deps): restore the empty dependencies contract and guard it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two review findings from the dependency sweep, both correct. `package.json` lost its `"dependencies": {}` key. Nobody removed it — `pnpm add` rewrites the manifest and drops an empty object without a word, so an ordinary dependency bump deleted the one line that states this package ships nothing at runtime. README, CLAUDE.md, AGENTS.md and the Dependabot config all assert that contract; the manifest had stopped saying it. Restore it, and assert it in the dogfood smoke test so it cannot vanish quietly again. The check distinguishes a missing key from an empty one, because they are different claims and only one is the contract. Red-checked both ways: removing the key fails with "the zero-dependency contract is unstated", and adding a real dependency fails naming it. Also drop the pull-request numbers from the Dependabot grouping comment. Comments under `.github/**` are timeless by project rule, and a PR reference is exactly the kind that reads as noise once the pull request is closed. The failure mode is described instead, which is what a future reader needs, plus a pointer to the workspace overrides that enforce the other half of the invariant. --- .github/dependabot.yml | 7 ++++--- package.json | 1 + scripts/dogfood-smoke-test.mjs | 19 +++++++++++++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8bd9045..1092ce9 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -44,9 +44,10 @@ updates: # error TS2322: Type 'Redis' is not assignable to type 'ConnectionOptions' # # on every site that hands a client to a Queue, Worker, QueueEvents or - # FlowProducer. Both #33 (ioredis alone) and #39 (bullmq alone) failed exactly - # that way, while the same two versions applied together are green. Grouping - # all update types keeps them moving as the pair they are. + # FlowProducer, while the very same versions applied together typecheck + # cleanly. Grouping all update types keeps them moving as the pair they are. + # The workspace `overrides` in pnpm-workspace.yaml enforce the other half of + # the invariant: one resolved copy across the root and every example. bullmq-stack: patterns: - 'bullmq' diff --git a/package.json b/package.json index 54fe2f6..363cfdc 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "release": "pnpm publish --provenance", "prepare": "husky" }, + "dependencies": {}, "peerDependencies": { "@nestjs/common": "^11.0.0", "@nestjs/core": "^11.0.0", diff --git a/scripts/dogfood-smoke-test.mjs b/scripts/dogfood-smoke-test.mjs index 2da5333..16cccc1 100644 --- a/scripts/dogfood-smoke-test.mjs +++ b/scripts/dogfood-smoke-test.mjs @@ -143,6 +143,25 @@ try { fail(`npm pack --dry-run failed: ${err instanceof Error ? err.message : String(err)}`) } +// -- 5b. Zero-runtime-dependency contract ------------------------------------ +// The empty `dependencies` object is the contract, not decoration: it is what the +// README, CLAUDE.md and the Dependabot config all assert about this package. It is +// also fragile — `pnpm add` rewrites the manifest and drops an empty object without +// a word, so the key disappears on an ordinary dependency bump and nothing else in +// the pipeline notices. Assert the key EXISTS and is empty; a missing key and an +// empty one are different claims, and only one of them is the contract. +section('5b. Zero-runtime-dependency contract') +const manifest = req(resolve(ROOT, 'package.json')) +if (!Object.hasOwn(manifest, 'dependencies')) { + fail('package.json has no `dependencies` key — the zero-dependency contract is unstated') +} else if (Object.keys(manifest.dependencies).length > 0) { + fail( + `package.json declares runtime dependencies: ${Object.keys(manifest.dependencies).join(', ')}`, + ) +} else { + pass('package.json declares an explicit empty `dependencies`') +} + // -- 6. Consumer file: link smoke -------------------------------------------- section('6. Consumer file: link smoke (resolution check, ESM + CJS)') try { From e638081ed1308b45bb0967cf4cde10065d615d03 Mon Sep 17 00:00:00 2001 From: Maximiliano Salvatti <40447063+msalvatti@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:13:41 -0300 Subject: [PATCH 2/3] docs(smoke): list the new manifest check in the script header --- scripts/dogfood-smoke-test.mjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/dogfood-smoke-test.mjs b/scripts/dogfood-smoke-test.mjs index 16cccc1..37872b2 100644 --- a/scripts/dogfood-smoke-test.mjs +++ b/scripts/dogfood-smoke-test.mjs @@ -7,9 +7,10 @@ * 2. ESM import resolves the expected named exports (server + shared) * 3. CJS require resolves the expected named exports (server + shared) * 4. Tarball contents (npm pack --dry-run) contain only dist/ + meta files - * 5. A minimal consumer (file: link in an OS temp dir) resolves both subpaths + * 5. The manifest still declares an explicit empty `dependencies` + * 6. A minimal consumer (file: link in an OS temp dir) resolves both subpaths * through the published `exports` map, in ESM *and* CJS - * 6. Behavioral smoke against the built artifact: forRoot wires a DynamicModule + * 7. Behavioral smoke against the built artifact: forRoot wires a DynamicModule * and the fail-fast validation path still throws * * Exit codes: 0 pass · 1 assertion failed · 2 build artifacts missing. From b55526fbaff721091888565bd6ba43f7fb649355 Mon Sep 17 00:00:00 2001 From: Maximiliano Salvatti <40447063+msalvatti@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:21:22 -0300 Subject: [PATCH 3/3] fix(smoke): report a malformed dependencies field instead of throwing on it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Object.hasOwn` is true for `"dependencies": null`, and `Object.keys(null)` then throws. The check meant to explain which contract broke would instead kill the run with an opaque TypeError — a guard that crashes is worse than one that reports, because the operator loses the one message that was the point. Validate the shape before reading it, so `null`, a string and an array each fail with the value they carried. Red-checked all four rejections plus the passing case; the `null` case has to be exercised with `node` directly, since pnpm refuses to run scripts at all when the manifest is malformed and never reaches the script. --- scripts/dogfood-smoke-test.mjs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/scripts/dogfood-smoke-test.mjs b/scripts/dogfood-smoke-test.mjs index 37872b2..1c229af 100644 --- a/scripts/dogfood-smoke-test.mjs +++ b/scripts/dogfood-smoke-test.mjs @@ -153,12 +153,15 @@ try { // empty one are different claims, and only one of them is the contract. section('5b. Zero-runtime-dependency contract') const manifest = req(resolve(ROOT, 'package.json')) -if (!Object.hasOwn(manifest, 'dependencies')) { +const declared = Object.hasOwn(manifest, 'dependencies') ? manifest.dependencies : undefined +if (declared === undefined) { fail('package.json has no `dependencies` key — the zero-dependency contract is unstated') -} else if (Object.keys(manifest.dependencies).length > 0) { - fail( - `package.json declares runtime dependencies: ${Object.keys(manifest.dependencies).join(', ')}`, - ) +} else if (declared === null || typeof declared !== 'object' || Array.isArray(declared)) { + // A guard that throws is worse than no guard: the run dies on an opaque + // TypeError instead of reporting which contract broke. + fail(`package.json \`dependencies\` is not an object (got ${JSON.stringify(declared)})`) +} else if (Object.keys(declared).length > 0) { + fail(`package.json declares runtime dependencies: ${Object.keys(declared).join(', ')}`) } else { pass('package.json declares an explicit empty `dependencies`') }