fix(build): add clean step so renames cannot leak stale files to npm - #93
Merged
Conversation
The TypeScript build does not delete output for sources that no longer exist, so any file rename left orphaned .js/.d.ts in dist/. Since package.json ships dist/, those artifacts reached the published tarball. Reproduced on main before this change: planting dist/sync/strings-client.js and dist/sync/sync-strings.js then running `npm run build` left both in `npm pack --dry-run` output. Note the clean step must also remove tsconfig.tsbuildinfo. tsconfig sets "incremental": true, so deleting dist/ alone leaves the build-info file claiming the output is current — tsc then emits nothing and the build fails at `chmod +x dist/cli/index.js`. This is why removing dist/ on its own is not sufficient. Adds tests/unit/package-manifest.test.ts to pin the invariants. An equivalent change was made and silently lost once before (sync-1q8x was closed as complete, but `git log -S '"clean"' -- package.json` shows it never landed), so the guard matters as much as the fix. Verified: build from clean state regenerates dist with the bin exec bit set, planted stale files no longer survive, tarball has zero internal-name hits at 323 files / 197.4 kB, and 232 suites / 5501 tests pass on Node 24.18.0.
sjsyrek
added a commit
that referenced
this pull request
Jul 27, 2026
The watchAndSync block intermittently failed CI on unrelated dependency PRs (#81 Jun 30 Node 22, #91 and #93 Jul 27 Node 24), always as a 10s test timeout and never as an assertion, hitting a different test each time. Both #81 and #91 passed on plain re-run with no code change. Three changes, in order of importance: 1. jest.setTimeout(30_000) for the block. The suite runs in ~1.8s locally but was observed at 10.8-10.9s on GitHub runners with 232 suites competing for ~4 cores — i.e. sitting on the 10s default, which tipped an arbitrary test over each time. This is the headroom the block actually needed. 2. flushWatchSetup now waits for an observable readiness signal (mockWatcherOn having been called) instead of burning a fixed round count, then drains a settle budget. attachDebouncedWatchLoop calls watcher.on() in the same synchronous continuation as the process.on('SIGINT') registration, so listeners-attached implies the handler each test uses for shutdown is registered. A fixed count could under-wait; this cannot. On a genuine stall it throws with the pending state instead of timing out mutely. 3. Settle rounds 250 -> 25. Every test in the block passes with as few as 5 (measured), so 25 keeps a 5x margin. On method: the historical escalation was 20 -> 50 -> 250 rounds, each assuming the budget was too small. Suite duration is flat across 250/25/5 rounds, so widening it could never have worked. Four further hypotheses were tested and disproven — slow sweepStaleBackups I/O (its projectRoot '/test' does not exist, so readdir ENOENTs immediately), a hang in controller.shutdown() (microtask-only), expensive flush rounds (0.00ms per 250), and CPU contention (old code passed 12/12 under 16 spinners on 11 cores). Details in bead sync-94ua. Honest limitation: the flake could not be reproduced locally — ~58 runs across five contention profiles (CPU, I/O+CPU, full-suite, starved budget) produced one failure whose identity was lost. So this targets the signature the evidence supports rather than a reproduction, and the new diagnostic ensures the next occurrence identifies itself. Verified: 49 tests in the block pass; 20/20 under combined I/O and CPU contention; lint, type-check, and 5501 tests green on Node 24.18.0.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a
cleanstep to the build so a file rename cannot leak stale compiled output into the published npm tarball. Closes the gap left bysync-1q8x, which was marked complete but never actually landed.The bug, reproduced on
maintscdoes not delete output for sources that no longer exist, andpackage.jsonshipsdist/. So orphaned artifacts from any rename reach the tarball. Verified before the fix:Those filenames are exactly what the v1.1.0 squash-merge existed to keep off the public record.
The prior change was lost without trace —
git log -S '"clean"' -- package.jsonreturns no commits, so it was never committed despite the issue being closed "Completed in commit."Changes Made
tests/unit/package-manifest.test.ts— new, 8 assertions pinning these invariantsCHANGELOG.md—### Changedentry underUnreleasedWhy
tsconfig.tsbuildinfomust go tooThis is the non-obvious part, and the reason the naive fix fails.
tsconfig.jsonsets"incremental": true. Deleting onlydist/leaves the 101 KB build-info file asserting the output is current, sotscemits nothing and the build dies:So
rm -rf diston its own — which is what the original issue prescribed — breaks the build. That may well be why the earlier attempt disappeared.Test Coverage
New
tests/unit/package-manifest.test.tscovers:cleanexists, and removes bothdistandtsbuildinfobuildrunscleanfirst, and sets the bin exec bitprepublishOnlyrebuildsfilesexcludes*.tsbuildinfoand*.js.mapbintargets resolve insidedist/This change was silently lost once; a test makes a repeat impossible.
Verified on Node 24.18.0:
Backward Compatibility
✅ No runtime change — build tooling only; no source modified.
⚠️ Builds are now always full, never incremental.
✅ Same published surface, minus stale artifacts.
tsconfig.tsbuildinfois removed on every build, so the incremental cache no longer applies tonpm run build. Measured cost is a few seconds; correctness of the published tarball is worth more.npm run type-checkis unaffected and still incremental.❌ Breaking changes: none.
Size: Small ✓
One script addition, one script edit, one new test file.