H-6762: Stop @hashintel/ds-helpers publishing empty tarballs - #9190
H-6762: Stop @hashintel/ds-helpers publishing empty tarballs#9190claude[bot] wants to merge 2 commits into
Conversation
libs/@hashintel/ds-helpers/styled-system/ is the entire published payload of @hashintel/ds-helpers, but it is generated by @hashintel/ds-components (panda outdir "../ds-helpers/styled-system"). That codegen ran with --clean, which calls fs-extra emptyDirSync() on the directory. changeset publish publishes packages concurrently (Promise.all, queue concurrency 10), and ds-components' and petrinaut's prepublishOnly hooks both re-run that codegen -- so a `panda codegen --clean` fires while npm is packing ds-helpers, and the tarball ships with only the six metadata files. That is what 0.1.1, 0.2.0 and 0.2.1 look like on npm; 0.2.1 is still `latest`. - drop --clean from ds-components' codegen:panda, so nothing empties the published directory (turbo does not clean outputs before restoring a cache hit, so freshness is unaffected in CI) - record why in ds-components/turbo.json so it is not re-added - add a dependency-free prepack verifier to ds-helpers that fails the pack when any exports/main/types target is missing, turning the remaining deterministic failure modes into a failed release instead of a silent empty publish - drop the ./recipes export and the import/require conditions on ./types: panda never generates styled-system/recipes/ (no recipes in the preset) and emits types/ as .d.ts only, so those targets have never resolved - add a changeset bumping both packages as a patch. Patch rather than major on the grounds that the two removed export entries never resolved to real files in any published version, so no consumer can regress by losing them. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019N9k8hWgVbTi81ufCMpAWv
Same bug class as the previous commit, one level up, and deliberately kept as a separate commit so it can be dropped independently. libs/@hashintel/ds-components/tsup.config.ts had `clean: true`, which empties `dist/` -- the entire published payload of @hashintel/ds-components -- while @hashintel/petrinaut's concurrent `prepublishOnly: turbo run build` rebuilds this package during `changeset publish`. That is the identical mechanism as panda's `--clean` emptying ds-helpers' styled-system/. THIS IS NOT REPRODUCED. The only supporting evidence is that the published file counts are non-monotonic for a growing component library: 0.1.2 24 files 0.2.0 163 files 0.2.1 58 files 0.2.2 86 files (latest) HEAD 254 files (local pack) which is consistent with a truncating race but does not prove one -- HEAD's source is not 0.2.2's source, so the file-count diff is confounded. It is changed here because `clean: false` costs nothing: CI always builds from an absent dist, and a stale local dist is fixed with `rm -rf dist`. Verified after the change: a from-scratch build (dist removed first) still produces 254 files in `npm pack --dry-run`, with all 7 main/types/exports targets resolving. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019N9k8hWgVbTi81ufCMpAWv
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
PR SummaryMedium Risk Overview
Patch changeset for both packages. Reviewed by Cursor Bugbot for commit 37582a5. Bugbot is set up for automated code reviews on this repo. Configure here. |
Requested via Slack thread
🌟 What is the purpose of this PR?
@hashintel/ds-helpershas been publishing near-empty tarballs to npm since March.Before. Installing the design system from the registry gave you a package whose every entry point pointed at a file that was not in the tarball.
0.1.1,0.2.0and0.2.1each contain six files —package.json,CHANGELOG.md, three licence files andREADME.md— and no code at all.0.2.1is still thelatesttag.After. The tarball always contains the generated payload — 126 files — and if it ever does not, the pack fails loudly instead of publishing an empty package.
Root cause
libs/@hashintel/ds-helpers/styled-system/is the entire published payload of@hashintel/ds-helpers, but it is written by a different package:@hashintel/ds-components' Panda codegen, whosepanda.config.ts:7setsoutdir: "../ds-helpers/styled-system". That codegen ran aspanda codegen --clean, and--cleanreaches@pandacss/node'sctx.output.empty()→fsExtra.emptyDirSync()on the outdir, followed by an asynchronous rewrite. Nothing else in the repo deletes that directory.changeset publishpublishes packages concurrently —@changesets/cli@2.30.0runsPromise.allover a queue withNPM_PUBLISH_CONCURRENCY_LIMIT = 10, and only drops to serial for interactive 2FA (process.stdin.isTTY, false in CI). Each concurrentnpm publish <dir>runs that package's ownprepublishOnly→prepack→prepare.Several of those hooks re-run the deleter:
@hashintel/ds-componentsprepublishOnly: turbo run build@hashintel/petrinautprepublishOnly: turbo run build(depends onds-components)@hashintel/ds-helpersprepack: turbo run codegen --filter @hashintel/ds-helpersRoot
turbo.jsonhasbuild.dependsOn: ["codegen"], so all three reach@hashintel/ds-components#codegen. Turbo does not de-duplicate across processes — in a two-process run both logged the same task hash as a cache miss and executed it simultaneously:So a
panda codegen --cleanfires whilenpm packis reading the directory, and the tarball is written with it emptied.Evidence
Reproduction. Repeatedly
npm packds-helpers while ds-components'codegen:pandaruns concurrently:0.2.1The preconditions held in the actual release. npm publish timestamps for the 2026-05-22 run:
ds-helpers finished packing at 18:00:24Z while
ds-componentsandpetrinaut— both runningturbo run build→panda codegen --clean— were still in flight, i.e. two codegens were live when 0.2.1 packed.Directly observed. A 50 ms sampler on the directory during concurrent
npm publish --dry-runof both packages (cold turbo cache, 5 iterations) saw the directory reach zero files after being fully populated in 2 of 5 runs:0 → 41 → 120 → 0 → 120.0.2.0(also 6 files) has a second, fully deterministic cause on top of this: its publishedpackage.jsonhas noscriptskey at all — the "thin artifact package" refactor left ds-helpers with no generation hook, so on a fresh CI checkoutstyled-system/simply never existed.0.1.1(5 files) predates the current layout entirely; not reproduced, no claim made about it.tsup clean: false) is suspected by analogy, not reproduced — see below.🔗 Related links
@hashintel/ds-helperson npm —0.2.1islatest🚫 Blocked by
🔍 What does this change?
Commit 1 —
H-6762: Stop @hashintel/ds-helpers publishing empty tarballslibs/@hashintel/ds-components/package.json—codegen:pandadrops--clean(panda codegen --clean→panda codegen). This deletes the only code path that empties the published directory, making a 6-file tarball structurally impossible rather than merely unlikely.rm -rf libs/@hashintel/ds-helpers/styled-system. Verified separately that turbo does not clean outputs before restoring a cache hit (a sentinel file placed instyled-system/survived a>>> FULL TURBOrestore), so cache correctness is unaffected.--cleanback, the correct shape is to give Panda a ds-components-privateoutdirand have ds-helpers'prepackcopy it into place. That is a bigger change and is not made here.libs/@hashintel/ds-components/turbo.json— a comment above thecodegentask'soutputsrecording why--cleanmust not come back.libs/@hashintel/ds-helpers/scripts/verify-package-contents.mjs(new, dependency-free) — resolves every literal target reachable frommain/module/types/exports, asserts each exists and is non-empty, and asserts every directory listed infilesis non-empty. Exits 1 with a list of what is missing. Not itself published (filesdoes not includescripts).libs/@hashintel/ds-helpers/package.json—prepacknow runs that verifier after codegen; and twoexportsentries are removed:./recipes—styled-system/recipes/is never generated. There are nodefineRecipe/defineSlotRecipecalls inds-components/src/, and the generated tree contains onlycss jsx patterns tokens types helpers.mjs. The only in-repo references are the legacy import string a codemod migrates away from (ds-components/scripts/migrate-beta-fractal-pilots.ts:365,379and its test).import/requireconditions on./types— Panda emitsstyled-system/types/as.d.tsonly, so those conditions can never resolve. (They also dangled in the last good release,0.1.2.) Thetypescondition is kept;./typesis consumed only viaimport type(5 call sites)..changeset/h-6762-ds-helpers-empty-tarball.md—patchfor both@hashintel/ds-helpersand@hashintel/ds-components. Without a changeset,changeset publishcuts no version and the fix does not ship.exportsentries would normally be a breaking change, but neither has ever resolved to a real file in any published version, so nothing can regress by losing them. Hencepatch, notmajor.Commit 2 —
H-6762: Stop tsup wiping ds-components' dist (SUSPECTED, NOT REPRODUCED)Kept as a separate commit specifically so it can be dropped if you disagree.
libs/@hashintel/ds-components/tsup.config.ts:13hadclean: true, which emptiesdist/— the entire published payload of@hashintel/ds-components— while@hashintel/petrinaut's concurrentprepublishOnly: turbo run buildrebuilds this package duringchangeset publish. That is the identical mechanism one level up. Set toclean: false.This one is suspected by analogy, not reproduced. The only supporting evidence is that published file counts are non-monotonic for a growing component library:
That is consistent with a truncating race but does not prove one — HEAD's source is not 0.2.2's source, so the file-count diff is confounded. It is changed here because
clean: falsecosts nothing: CI always builds from an absentdist, and a stale localdistis fixed withrm -rf dist.What this deliberately does not do
A pre-publish tarball-content assertion in
release.ymlwould not have caught 0.2.1. Such a gate runs serially, and a serial pack always succeeds — I measured 126 files every time. The failure only exists during the concurrent publish, after the gate has already passed. A gate placed afterchangeset publishwould detect it but cannot un-publish; npm has already movedlatest. That is why the fix removes the wipe rather than adding a check.The
prepackverifier added here is second-line defence for the deterministic failure modes — missing generation hook (the real 0.2.0 cause), codegen failure, wrongoutdir— not for the race.🐾 Next steps
Follow-ups found while investigating, not addressed here:
libs/@hashintel/ds-helpers/AGENTS.md:79claimsstyled-system/is "(committed)" — it is git-ignored (.gitignore:2). Either the comment or the policy is wrong; committing the generated tree would remove this whole class of problem, but that is a repo-policy call.@hashintel/ds-componentslists@hashintel/ds-helpersin bothdependenciesandpeerDependencies, so registry consumers ofds-components@0.2.1/0.2.2pull the brokends-helperstwice over.0.2.1is stilllateston npm for@hashintel/ds-helpers. Merging this fixes future publishes but does not fix the registry — someone still needs to cut and promote a good release.🛡 What tests cover this?
No automated test — a concurrency race is not something to assert on in CI. The
prepackverifier is itself the runtime check, and it fails the publish rather than a test run. Verification was done by hand:❓ How to test this?
yarn install && rm -rf libs/@hashintel/ds-helpers/styled-systemcd libs/@hashintel/ds-helpers && npm pack --dry-run— theprepackhook regeneratesstyled-system/, the verifier printsverified 9 entry-point target(s) present., and npm reports 126 files.rm -rf styled-system && node scripts/verify-package-contents.mjs— it should exit 1 and list every missing target.main: repeatedlynpm packds-helpers in one shell while loopingturbo run codegen --filter @hashintel/ds-components --forcein another. Onmainsome packs come out at 6 files; on this branch none do.Pre-Merge Checklist 🚀
🚢 Has this modified a publishable library?
This PR:
📜 Does this require a change to the docs?
The changes in this PR:
🕸️ Does this require a change to the Turbo Graph?
The changes in this PR:
turbo.json's have been updated to reflect thisscriptschanged in twopackage.jsons (ds-components#codegen:pandalost a flag,ds-helpers#prepackgained a step). No task was added or removed and no dependency edge changed;ds-components/turbo.jsongains only a comment on the existingcodegentask.