Repository navigation
💥 Modernize the toolchain and ship dual ESM + CJS - #17
Merged
Merged
Conversation
`@tselect/status-code@1.0.2` ships 32 files and 1,369,746 bytes unpacked, of which 1,352,289 are a single `.log22069` left over from a 2023 test run. The `.gitignore` had `.log*`, so the file was invisible to `git status` while npm included it in every publish — the registry tarball is the only thing that tells you what actually ships. `tslint.json` rode along too. The cause is `.npmignore`, which is a denylist and so cannot enumerate files that did not exist when it was written. It listed `test/`, `node_modules/`, `.idea/`, `tsconfig.json`, `*.ts` and `.nyc_output/`; a stray log matched none of them. Replaced with `"files": ["dist"]`, an allowlist, which cannot fail open in this direction. Also fixes `prepare: "tsc"`, which was a silent no-op: `tsconfig.json` sets `noEmit: true`, so it compiled nothing and a clean checkout would pack an empty `dist/`. It only worked because `dist/` happened to be present in the publishing tree. Replaced with `prepack: "npm run build"`, which passes `--noEmit false`. `.gitignore` is now the same list the other repos use. Note it drops `.log*` in favour of `*.log`, which deliberately does *not* hide `.log22069`-style names — with an allowlist guarding the tarball, a stray log is better left visible to `git status` than silently ignored. Verified by packing a clean copy of the tracked tree: 30 files and 5,397 bytes packed, and the tarball installed into a scratch consumer still exposes the same 12 exports and 86 enum keys as the published 1.0.2.
Drops `package-lock.json` for `pnpm-lock.yaml` and declares `packageManager: pnpm@11.21.0`, matching the four repos already migrated. Note pnpm 11 keeps its settings in `pnpm-workspace.yaml` rather than a `pnpm` key in `package.json` — this is not a workspace, the file is just where the settings live now. It carries the `esbuild >=0.28.1` override the other repos use: vitest still resolves into the range covered by GHSA-g7r4-m6w7-qqqr, and this pins it forward until vitest widens. Dev-only; nothing ships. Resolving the existing ranges against already-published patches takes `audit` from **15 vulnerabilities (2 low, 4 moderate, 9 high) to 3 (2 moderate, 1 high)** with no version-range edits at all. The remaining three are all under `nyc`, which the Vitest migration removes. Tests still report the baseline 29 passing.
Replaces the file-per-module `tsc` output with a bundled dual build, and adds
the `exports` map. `main`/`module`/`types` stay for older resolvers.
**Steps 3 and 6 of the checklist are one commit deliberately.** They cannot be
sequenced apart: tsdown's peer range is `^5 || ^6 || ^7`, so it refuses to run
on the old `typescript@4.9.5`, and TypeScript 7 removes
`moduleResolution: "node10"` (TS5108), so the old tsc build cannot survive the
upgrade either. Whichever landed first would leave the tree unbuildable.
TypeScript 4.9.5 → 7.0.2 and `@types/node` 12.7.0 → 22.x. `tsconfig.json`
moves to `moduleResolution: "bundler"`, gains `skipLibCheck` (required once
`include` covers `test/`) and drops `suppressImplicitAnyIndexErrors`, which
TypeScript 5.5 removed.
That last one is not cosmetic — it was hiding a real error in `toString`:
Object.keys(StatusCode).find(key => StatusCode[key] === code)
`StatusCode` is a numeric enum, so at runtime it holds both directions of the
mapping (`{ 200: 'OK', OK: 200 }`) and indexing it by an arbitrary string is
meaningful — but that is not expressible in the enum's own type, so TS 7
reports TS7015. Fixed by naming the runtime shape in a typed alias rather than
by re-suppressing the error or casting at the call site. This is why the
function returns `'200'` for `toString('OK')`, which is preserved exactly.
Emitted syntax stays at ES2015, matching what `target: es6` produced before;
CI asserts it with es-check.
Verified against the published 1.0.2 tarball: 115 inputs (all 43 enum values,
all 43 member names, and 29 edge cases including `NaN`, `Infinity`, `null`,
`undefined`, `'200'`, `'OK'`, booleans, `{}` and `[]`) across all 11 functions,
run against **both** new entry points, plus enum contents and export names —
**2,534 comparisons, 0 mismatches**. `attw` is clean on all four resolution
modes, es-check passes es2015 on both bundles, and `dist/index.d.mts` and
`dist/index.d.cts` are both present at 2,620 bytes.
Removes mocha, chai, nyc, ts-node, @types/mocha and @types/chai; adds vitest
and @vitest/coverage-v8. `test/mocha.opts`, `test/setup.ts` and
`test/global.d.ts` go with them — the last two existed only to hang chai's
`expect` off `global`, which Vitest makes unnecessary.
The port is mechanical: `describe`/`it` stay, `.to.equal(x)` becomes
`.toBe(x)`, and the `<any>` angle-bracket casts become `as` casts. All **29
baseline assertions are preserved verbatim** — diffed pairwise rather than
eyeballed, with none missing.
**`pnpm audit` reaches 0**, from 3 after the pnpm migration and 15 before it.
All three remaining advisories were under `nyc`, so deleting it achieves this
by subtraction rather than by `audit fix`.
Seven assertions are new, in five tests, and they close a real gap: the
inherited suite reported 100% of statements, functions and lines but only
**90% of branches**, below the 95% shared floor. Coverage is now **100% of
all four**. The gaps were:
* `toString`'s `|| null` fallback — never exercised, so nothing pinned what
the function does with a code outside the enum.
* `isConsumerErrorStatusCode`'s non-status-code path, the only one of the
eight predicates whose `false` case was untested.
Two of the new tests pin behaviour rather than assert intent, and say so at
the site. `StatusCode` is a numeric enum, so its runtime object carries a
reverse mapping and `Object.values` yields the 43 member *names* alongside the
43 numbers. `isStatusCode('OK')` is therefore `true` and `toString('OK')` is
`'200'`. That has been the published behaviour since 1.0.0; this is a tooling
refresh, so it is locked in place rather than quietly corrected.
This is the third repo in a row where the statement column looked finished and
the branch column did not.
Deletes `tslint.json` and the `tslint` dependency (EOL since 2019) for `@biomejs/biome`, which covers lint *and* format in one tool. The `$schema` pin is 2.5.11 here rather than the 2.5.8 the other repos carry — Biome rejects a config whose schema version does not match the CLI. Biome's recommended set found 13 issues across three rules. Ten were `useImportType` and fixed mechanically, along with an export sort in `src/index.ts` and a reformat of the enum. The other three needed decisions: **`isStatusCode(value: any)` → `unknown`.** A type guard exists to narrow a value whose type nobody has established, and `any` defeats that at every call site. This widens what the signature accepts, so no existing call stops compiling. `Object.values` of a numeric enum is typed `(string | StatusCode)[]` and its `includes` will not take an `unknown`, so the lookup now casts — no behaviour change. **`toString` shadows a global, twice, and stays.** It is the published export name and has been since 1.0.0; renaming it would break every consumer for a cosmetic gain, and the rule's real hazard — confusion with `Object.prototype.toString` — does not apply to a named module export always reached through an explicit import. Suppressed with `biome-ignore` at the two sites that need it, each carrying the reason, rather than switched off in `biome.json`, so a genuine future shadowing is still reported. Every one of these touches source, so the differential suite was re-run rather than reasoned about: the same 115 inputs × 11 functions against both new entry points, **2,534 comparisons, 0 mismatches**, with enum contents and export names unchanged. `tsc --noEmit` is clean, 34 tests pass, coverage holds at 100% of all four metrics.
The repo had no `LICENSE` file at all despite `"license": "MIT"` in `package.json` and an MIT badge in the README. Adds the same MIT text the other repos carry. README follows the shared order — title, badges, description, Requirements, Installation, Usage, API, License — and gains a real API section: all 43 enum members grouped by class, and every one of the 11 functions with a signature and a runnable example. It also documents the reverse-mapping behaviour of `isStatusCode` and `toString` under a warning, with the `typeof value === 'number' && isStatusCode(value)` workaround, rather than leaving consumers to discover it. Metadata fixes: `bugs.url` was missing `/issues`, so the badge and npm's "report an issue" link both pointed at the repo root; `description` now says what the package contains; `keywords` gains `http-status`, `status-code`, `status`, `esm` and `cjs`. **Every README example was executed, not just written.** 210 assertions, all passing, and the file typechecks clean under `strict` + `nodenext` with no casts beyond the ones the README itself shows. The three prose claims were checked across the whole enum rather than at one example each — that `isServerErrorStatusCode`/`isConsumerErrorStatusCode` are aliases of `is5xx`/`is4xx`, that `isNonErrorStatusCode` is the complement of `isErrorStatusCode` over valid codes, and that "at or above 400" is exactly 4xx ∪ 5xx — all 43 members each.
Adds `ci.yml` and `publish.yml` as thin callers of the reusable workflows in
`tselect-npm/.github`, both pinned at `@v1` and never `@main` — an edit
upstream would otherwise change this repo's behaviour with no review here.
`ci.yml` is byte-identical to `http-method`'s. The `concurrency` block has to
live in the caller: inside a reusable workflow `github.workflow` and
`github.ref` resolve to the caller's, so every repo calling it would share one
group and cancel each other. `es-check-target` is left at its default of
es2015, which is this package's declared ceiling — only `thrown` overrides it.
`publish.yml`'s filename is load-bearing: npm's trusted publisher validates
the *caller's* workflow filename, so renaming it breaks publishing with a 404
that mentions nothing about filenames. Its `since-ref` description is written
for this repo rather than copied — `status-code` needs no tag surgery, because
`v1.0.2` already sits on `a1569df`, exactly the commit the registry reports as
1.0.2's `gitHead`. That makes it the only repo of the seven whose tags were
already correct.
Declares `"engines": { "node": ">=22" }`. That is the bottom of the CI matrix,
so the floor is proven by construction rather than asserted; `>=20` is not
viable regardless, since pnpm 11 requires `node >=22.13`. **This is what makes
the release a major.**
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.
Fifth repo through the modernization runbook in the workspace
CHECKLIST.md, afterurl(the pilot),thrown,http-methodandschema. pnpm · tsdown (dual ESM + CJS) ·Vitest · Biome · TypeScript 7 · shared CI from
tselect-npm/.github@v1.The title is
:boom:deliberately. Squash is the only merge method the rulesetsallow, so this PR collapses to one commit and that commit's gitmoji is the only one
the release workflow reads.
:wrench:would infer as no release at all. The branch'sseries is preserved below, since this is the only place it survives.
Headline fix: the published package ships a 1.35 MB log
@tselect/status-code@1.0.2is 32 files and 1,369,746 bytes unpacked, of which1,352,289 are a single
.log22069left over from a 2023 test run.tslint.jsonrodealong too. The
.gitignorehad.log*, so the file was invisible togit statuswhilenpm included it in every publish — the registry tarball is the only thing that tells you
what actually ships.
The cause is
.npmignore: a denylist cannot enumerate files that did not exist when itwas written. Replaced with
"files": ["dist"]. The tarball is now 9 files and7,970 bytes.
Also fixed:
prepare: "tsc"was a silent no-op, becausetsconfig.jsonsetsnoEmit: true. A clean checkout would have packed an emptydist/; it only workedbecause
dist/happened to exist in the publishing tree.Why this is a major
engines.nodeis now">=22"— the bottom of the CI matrix, so the floor istested by construction.
>=20is not viable regardless: pnpm 11 requiresnode >=22.13.exportsmap closes deep imports. No consumers of@tselect/status-code/distexist anywhere public — the search returns nothing forthe package at all.
isStatusCode(value: any)becomesisStatusCode(value: unknown). This is awidening, so no existing call stops compiling, but it is a public signature change.
Coverage, and a quirk pinned rather than fixed
The inherited suite reported 100% of statements, functions and lines but only 90% of
branches, below the 95% floor. Coverage is now 100% of all four, via seven new
assertions in five tests. The gaps were
toString's|| nullfallback andisConsumerErrorStatusCode's non-status-code path. That is the third repo running wherethe statement column looked finished and the branch column did not.
Two of the new tests pin behaviour rather than assert intent.
StatusCodeis a numericenum, so its runtime object carries a reverse mapping and
Object.valuesyields the 43member names alongside the 43 numbers:
That has been the published behaviour since 1.0.0, so this tooling refresh preserves
it rather than quietly changing a type guard's answers. It is now documented under a
warning in the README, with the
typeof value === 'number' && isStatusCode(value)workaround, and pinned by tests that explain the cause.
toStringalso trips Biome'snoShadowRestrictedNames, twice, and stays — it is thepublished export name, and renaming it would break every consumer for a cosmetic gain.
Suppressed with
biome-ignoreat the two sites that need it, each carrying its reason,rather than switched off in
biome.json.Verification
Differential-tested against the published 1.0.2 tarball after every source-touching
commit: 115 inputs (all 43 enum values, all 43 member names, and 29 edge cases including
NaN,Infinity,null,undefined,'200','OK', booleans,{}and[]) acrossall 11 functions, run against both new entry points, plus enum contents and export
names — 2,534 comparisons, 0 mismatches.
Every README example was executed rather than just written: 210 assertions, all
passing, typechecking clean under
strict+nodenext. The three prose claims werechecked across the whole enum rather than at one example each — that
isServerErrorStatusCode/isConsumerErrorStatusCodeare aliases ofis5xx/is4xx,that
isNonErrorStatusCodeis the complement ofisErrorStatusCodeover valid codes,and that "at or above 400" is exactly 4xx ∪ 5xx.
From a clean clone of this branch: 34 tests, 100% coverage on all four metrics,
pnpm audit0 (from 15),biome checkandtsc --noEmitclean,attwclean on allfour resolution modes, es-check es2015 on both bundles, both declaration files non-empty
at 2,624 bytes, and
require()andimportagreeing on all 12 exports.Notes
v1.0.2already sitson
a1569df— exactly the commit the registry reports as 1.0.2'sgitHead— so therelease workflow's commit range resolves on the first try. The 17 remaining
@bluejay-era tags are clutter, but none collides with thev2.0.0this major wants,so they are left alone.
masterwas renamed tomainas part of this work. That retargeted the staleDependabot PR ⬆️ Bump serialize-javascript and mocha #16 automatically; it is moot once this merges, since it patches a
package-lock.jsonthat no longer exists for amochathat is no longer a dependency.1.0.2and the major goes out with the others at the end.