Skip to content

Build before publishing in the release workflow#31

Merged
grischaerbe merged 1 commit into
nextfrom
grischaerbe/publish-build-step
Apr 29, 2026
Merged

Build before publishing in the release workflow#31
grischaerbe merged 1 commit into
nextfrom
grischaerbe/publish-build-step

Conversation

@grischaerbe
Copy link
Copy Markdown
Owner

Summary

The release workflow's publish-npm job ran npm ci && npm publish with no build step. With dist/ gitignored and no prepack/prepublishOnly hook in package.json, the published tarball — whose files list points at dist — would be effectively empty. This adds an explicit npm run build between install and publish.

Test plan

  • Next release produces a tarball that contains the built dist/ output (verifiable via npm pack --dry-run on the release commit, or by inspecting the published artifact).

🤖 Generated with Claude Code

The publish-npm job ran `npm ci && npm publish` with no build step.
Combined with `dist/` being gitignored and no prepack/prepublishOnly
hook in package.json, a release would publish a tarball whose `files`
list ("dist", ...) points at a directory that doesn't exist — i.e. an
empty package. Insert `npm run build` between install and publish.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@grischaerbe grischaerbe merged commit 6b7a86d into next Apr 29, 2026
5 checks passed
@grischaerbe grischaerbe deleted the grischaerbe/publish-build-step branch April 29, 2026 20:51
grischaerbe added a commit that referenced this pull request Apr 29, 2026
…er (#18)

* Move cache policy from per-entry to per-instance

Policy and maxAge are now configured once on the Cacheables constructor
and apply to every entry in that instance, replacing the per-call options
arg on cacheable(). This eliminates cross-policy cache entries and the
errors that came with them. To compose policies, create multiple instances.

The constructor key was renamed cachePolicy -> policy (the class name
already implies "cache") and the CacheableOptions export was removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Rename cacheable() to remember()

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Introduce multilayer storage adapter pattern (v4)

Replace the single in-memory store with a composable IStorageAdapter
contract: reads cascade L1 → Ln, hits back-fill missing layers preserving
storedAt, and writes synthesize meta on L1 then propagate to deeper
layers. Adapters are required at construction; clear/delete/isCached
become async; keys() is removed; an optional namespace prefix isolates
instances that share an adapter; Cacheables gains a TMeta generic for
typed sidecar metadata. Ships a built-in MemoryAdapter and refines
read() to return { value } | undefined so adapters can store undefined
values without colliding with the absence signal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Make namespace a required Cacheables option

Promotes `namespace` from optional to required on `CacheablesOptions` to
prevent silent key collisions when adapters are shared across instances.
Drops the undefined branch in `#fullKey` and removes the verbatim-key
behaviour test along with related README/test updates.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Rename Cacheables to Cacheable and storage adapters to buckets

Renames the main class from `Cacheables` to `Cacheable` and reframes the
storage-adapter concept as buckets — `IStorageAdapter` becomes `IBucket`,
`MemoryAdapter` becomes `MemoryBucket`, the `adapters` constructor option
becomes `buckets`, and the `CacheablesOptions` type becomes
`CacheableOptions`. Files moved from `src/adapters/` to `src/buckets/`
(and similarly under `tests/`); README and v3 → v4 migration notes
updated accordingly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Rename migration section to v2 → v3 in README

The latest published version on npm is v2.0.0; the breaking changes
land in v3, not v4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Remove the "enabled" option from Cacheable (#15)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Replace log/logTiming flags with a pluggable ILogger (#16)

Introduces an ILogger interface and a default ConsoleLogger
implementation, replacing the boolean log and logTiming options with a
single logger option. Consumers can now route cache messages into any
logging stack via a one-line adapter; omitting the logger keeps the
engine silent.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Revise the v2 → v3 migration section in the README (#17)

Drops two inaccurate claims (`IStorageAdapter`/`MemoryAdapter` and
`CacheablesOptions` renames never existed in v2), adds the missing
high-impact migration steps (`cacheable()` → `remember()`, per-call
options removed, `Cacheables.key` → `Cacheable.key`, `namespace`
required), and leads with a v2/v3 before-after example.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add PR CI checks: typecheck, build, lint (#19)

* Add CI workflow with typecheck, build, and lint checks

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Rename prepublish to prepublishOnly so it doesn't run on npm ci

The deprecated prepublish lifecycle is aliased to prepare in modern npm,
so it ran during CI's npm ci and triggered the full test suite — masking
the actual job step. prepublishOnly runs only on npm publish.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Remove prepublishOnly script

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add test job to CI workflow

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add prettier check to lint CI job

Adds a format:check script (prettier --check .) and a .prettierignore
so the existing eslint-plugin-prettier coverage of src/**/*.ts is
extended to README, configs, and tests. Reformats the few files that
were drifting from the project prettier config.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Stop sampling the maxAge boundary in fetch-policy tests

The max-age and stale-while-revalidate-with-maxAge tests waited for
exactly maxAge ms after caching, leaving the age check sitting on its
boundary (`age <= maxAge` / `age > maxAge`). Tiny event-loop jitter
flipped the result, so the tests passed locally but failed in CI.

Wait long enough past maxAge that boundary jitter cannot affect the
outcome. Verified 10/10 under CPU contention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Remove isCached from the public API (#20)

Existence checks are subsumed by `cache.meta(key)`, which returns
`undefined` when no layer has the key. Tests and docs are updated
accordingly, and the v2 → v3 migration note now points users at
`meta()` as the replacement.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Make namespace a positional argument to the Cacheable constructor (#21)

The constructor signature changes from `new Cacheable(options)` to
`new Cacheable(namespace, options)`. The namespace is the instance's
identity and is almost always a literal at the call-site, so leading
with it reads more naturally and saves a line in the common case.
The remaining fields (`buckets`, `policy`, `maxAge`, `logger`) stay
in the options bag, preserving the discriminated union that ties
`maxAge` to its policies.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Tighten README and expand cache policy mechanics (#22)

* Tighten README and expand cache policy mechanics

Compact the intro and structural sections, annotate the headline example,
and split Cache Policies into per-policy subsections with dedicated
runnable examples covering freshness and concurrent dedup behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Apply prettier formatting to README

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Restore compact headline example, move comments to Usage section

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Rename Usage example namespace to weather-data and key to karlsruhe

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add summary table at the top of the Cache Policies section

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Align policy 'use this' closers and add one to max-age

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Tone down network-only-non-concurrent 'use this' line

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Merge timing and hit-count logs into one HIT/MISS line per call (#23)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add cache.resolve(producer, key) returning bucket meta (#24)

`resolve` shares the policy and dedup pipeline with `remember` but returns
`Promise<TMeta>` instead of the producer's value. This fits use cases where
the bucket projects derived data through `TMeta` (e.g. a filesystem bucket
that stores bytes and exposes a local URL on its meta) — callers can stay on
a freshness-aware path instead of falling back to the policy-bypassing
`cache.meta(key)` for the data they actually want.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Mint cascade write meta in the engine, fan out in parallel (#25)

* Mint cascade write meta in the engine, fan out in parallel

The engine now mints `{ storedAt: Date.now() }` upfront and writes to
every bucket in parallel, instead of writing L1 first, probing it for
synthesized meta, and only then fanning out to L2+. Read and write
paths now share the same rule: the engine owns meta, buckets persist
`storedAt` verbatim. Bucket meta synthesis remains the standalone-use
fallback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Split engine meta from bucket view (TView)

Previously `Cacheable<TMeta extends IBaseMeta>` mixed engine probing
data (`storedAt`) with bucket-defined sidecar fields (etag, url, ...)
in a single typed extension. This split makes the two concerns
orthogonal:

- `BucketEntryMeta = { storedAt: number }` — fixed, non-generic,
  engine-internal. Used by `bucket.meta()` for freshness probing.
- `TView` — bucket-defined user-facing projection. Surfaced
  exclusively through a new `bucket.resolve(key)` method and
  `cache.resolve(...)`.

`bucket.write(meta)` is now required (no synthesis branch) and
non-generic. `cache.meta()` is removed; users wanting `storedAt`
include it in `TView`. The engine carries only `storedAt` between
buckets — bucket-specific fields no longer cross bucket boundaries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fix prettier formatting on three files

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Split value and view cascades; refresh stale layers on fill

cache.resolve() now uses a separate view-cascade that skips the value
read on the L1 hot path, so a filesystem bucket holding 5 MB of bytes
returns just the URL without opening the file. bucket.resolve() returns
{ view } | undefined (mirroring read's wrapper) so void-TView buckets
distinguish "present, no projection" from "absent" — which lets the
engine race-heal undefined-after-meta-hit and throw a strict-mode error
on undefined-after-write. cascadeFill also now refreshes stale lower
layers under max-age instead of skipping them as long as anything is
present.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add two-tier dedup: per-mode policy outer, per-key producer inner

Restores the per-policy outer dedup that the value/view split lost, but
keys it by mode (`${fullKey}:value` vs `${fullKey}:view`) so cross-mode
callers don't share an inflight with mismatched shape. The producer
dedup stays keyed by fullKey alone, so concurrent cache.remember +
cache.resolve still trigger one producer call.

Concurrent same-mode hit callers now do 1 meta probe per layer again,
not N. New tests assert metaCalls counts to lock the behavior in.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Rename #dedupInto to #dedup

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Rename IBucket.resolve to IBucket.view

The bucket-level method now matches the TView generic and the { view }
wrapper it returns. cache.resolve() stays as the public API — it
"resolves a view from the buckets," with the engine calling
bucket.view() under the hood. Also picks up some internal cleanups:
CascadeFn type, #dedupKey helper, #cascadeRead / #cascadeResolve
naming that mirrors cache.remember / cache.resolve.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Pass hitMeta through cascade fill instead of reconstructing (#26)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Remove Cacheable.key from the public API (#27)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Drop README mention of cache key helper (#28)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* v3 hardening: packaging, NodeNext, SWR dedup, strict-mode resolve, dep refresh (#29)

* Restrict published files to dist + README + LICENSE

Without an explicit `files` allow-list, npm packed every tracked file in
the repo (.idea, .github, src, tests, tsconfigs, agent collaboration
files under .context, etc.). Whitelist only what consumers need so the
tarball drops from 56 files / 126kB to 25 files / 66kB.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Include namespace in HIT/MISS log lines

Two Cacheable instances sharing one logger were indistinguishable when
the format only carried the unprefixed key. Switch to
`Cacheable "<namespace>:<key>": …` so log streams remain unambiguous.
Updates the README example to match the implementation it sits next to,
and updates tests to assert the new format.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Update Build badge to current shields.io API

The previous URL used the deprecated /github/workflow/status/ endpoint,
which has been 404ing since shields.io retired it. Switch to the
current /github/actions/workflow/status/ endpoint and target the live
ci.yml workflow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Update ILogger jsdoc to match the merged HIT/MISS log format

The doc still described two log lines per call (timing + hit-count)
even though those were merged in #23. Brought the jsdoc in line with
the implementation so IDE hover docs match what consumers actually see.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Drop the misleading CacheOptions type re-export

CacheOptions in v3 had a completely different shape from v2's
CacheOptions (logger/policy/maxAge vs enabled/log/logTiming), so its
"backwards-compatible" comment was misleading — no v2 type would
satisfy it. Remove the type from types.ts and the public re-export
from index.ts. CacheableOptions is the single options type now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Re-export Policy and PolicyOptions from the package entry

Both types were defined in src/types.ts but never reached consumers
through src/index.ts. Surfacing them lets users type a policy literal
or build option-bag types in their own code.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Throw strict-mode error when L1 view is absent post-cascade-fill

#cascadeResolve previously returned undefined and let the run fall
through to the producer when L1's view came back absent after a
successful cascadeFill. The IBucket contract calls this case a
strict-mode error — the bucket just got a write and immediately denies
the entry exists. Throw and surface the bug instead of silently
masking it with another producer call.

The hitIdx === 0 branch keeps healing: cascadeFill skipped L1 there,
so an absent view is a probe-vs-view race against an external delete,
not a post-fill contract violation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Drop in-flight registrations on delete

Before this change delete() only wiped the bucket — the policy and
producer in-flight maps still pointed at the deleted key, so a
remember() racing the delete could attach to a producer whose write
was about to land back on top of the deletion. Clear those entries
synchronously alongside the bucket.delete() fan-out.

The producer itself is not aborted — that requires threading an
AbortSignal through #produceAndWrite and is left as a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fix the fetch examples to cache the parsed body, not the Response

Caching a Response object directly is a footgun — the body can be
consumed exactly once, so the first .json()/.text() works and every
subsequent call on the cached Response throws. Both the README hero
snippet and the Usage section example showed this antipattern.

Switch them to .then((r) => r.json()) so the cache stores the parsed
data, and add an `await` to the hero snippet so the example is a
runnable program rather than a fire-and-forget that would silently
swallow rejections.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Wrap stale-while-revalidate runs in the outer policy dedup

SWR was the only policy whose body ran outside #dedupPolicy, so 100
concurrent stale reads each issued their own cascade probe and value
read even though the producer was already shared via the inner
producer-inflight map. With multilayer buckets that meant N×M extra
meta calls plus N reads on the hit layer for every stale burst.

Wrapping the run in #dedupPolicy makes SWR consistent with the other
deduplicating policies: concurrent stale reads share one cascade probe
+ one value read + one background revalidation. The README claim
"Concurrent stale reads share one revalidation" now also holds for the
cascade itself, not just the producer call.

Sequential semantics are unchanged (dedup only affects concurrent
calls), so the existing fetchPolicies SWR tests pass without edits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Replace ConsoleLogger class with a consoleLogger singleton

The class wrapped a single one-line method and required users to write
`new ConsoleLogger()` for what is functionally a constant. Export the
singleton ILogger directly instead. Pre-3.0 we still get to make this
shape change for free.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Bump CJS target to ES2022 for native private fields

Targeting ES2015 forced TypeScript to transpile every #private field
access through __classPrivateFieldGet/Set helpers, ballooning the CJS
Cacheable.js to ~16kB. Bumping to ES2022 (Node 16.4+) lets the runtime
use native private fields directly. Cacheable.js shrinks from 15.8kB
to ~12kB and the helper preamble disappears.

Engines below Node 16.4 are no longer supported by the CJS build —
acceptable as part of v3's breaking changes, and matches the floor we
will pin in package.json.engines.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Move "types" first in the exports conditional map

Per the conditional-exports resolution rules, "types" should come
before "import"/"require" because it is matched ahead of them in
TypeScript's resolver. Order matters even when all keys point to the
right files; publint and arethetypeswrong both flag this.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Switch to NodeNext module resolution

Brings the dual-publish setup onto modern TypeScript:

- Root tsconfig: module/moduleResolution = NodeNext.
- src/package.json marks the source tree as ESM so the mjs build emits
  ESM and the cjs build (still moduleResolution = Node + module =
  CommonJS) emits CJS, with fixup-packages.ts continuing to set the
  per-dist `type` field.
- All relative imports in src/ now carry .js extensions, as required
  under NodeNext.
- tests/tsconfig.json overrides module/moduleResolution back to
  CommonJS/Node so ts-jest keeps working without ESM jest plumbing.
  jest.config.js wires the test-specific tsconfig and adds a
  moduleNameMapper that strips trailing .js so jest-resolve can find
  the .ts source.
- Drops the stale `./example/` ignore pattern from the jest config in
  the same touch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Pin lib to ES2022 instead of ESNext

ESNext drifts every TS release and silently grants the source access
to runtime APIs that may not exist on the floor we promise to support
(Node 16.4+ for native private fields). ES2022 matches the CJS target,
so any accidental use of newer APIs is caught by tsc rather than at
runtime in older environments. DOM stays — `performance.now()` and the
"works in browser" claim both rely on it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Bump publish workflow actions to v4

actions/checkout@v2 and actions/setup-node@v2 are end-of-life and
already throw deprecation warnings in CI logs. Match the @v4 versions
the ci.yml workflow already uses.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Pin minimum Node to >=18

CJS target is now ES2022 (native private fields, Node 16.4+) and the
public CI matrix runs on Node 20. Pinning the floor at 18 (the oldest
LTS still in maintenance through 2025) makes the support window
explicit so npm warns users on older Node before they hit a runtime
error.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Declare the package side-effect-free for tree-shaking

All exports are pure value declarations and a singleton object — no
top-level side effects. Telling bundlers so lets them drop unused
exports (e.g. consoleLogger in apps that pass a custom ILogger)
during tree-shaking.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Refresh package description for v3

The previous description still pitched v2's "simple in-memory cache".
v3's headline features are multilayer buckets, five policies, and
concurrency-safe dedup — surface those to npm, npms.io, and search
crawlers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Expand keywords for v3 positioning

Add multilayer/multi-tier, swr / stale-while-revalidate, and
redis/s3 to surface the package for searches that match the new
pluggable-bucket design.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Spell out all six IBucket methods in the resolve example

The truncated `// read / write / meta / delete / clear …` placeholder
made it look like only view() needed to be implemented. Replace with
the same six-method skeleton the later "Writing your own bucket"
section uses, so anyone copy-pasting from the cache.resolve docs sees
the full surface they need to satisfy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Drop the void-resolve sentence from the API section

Calling resolve() on a Cacheable<void> is strictly slower than
remember() and yields nothing useful, so we don't want to encourage
the pattern. The MemoryBucket sentence framed it as a feature.
The IBucket section already covers TView = void for users actually
implementing a bucket.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Drop the non-null assertions from the cascade engine

#findHitIdx now returns the bucket + meta together as a CascadeHit
object, so callers narrow the result through a falsy check instead of
re-indexing with `!`. The L1 reference is captured once in the
constructor as #l1 (one isolated assertion at construction time, where
the precondition is enforced) and used directly elsewhere.
#cascadeFill switched to forEach so the bucket parameter is already
narrowed.

No behavior change — same tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Bump typescript, prettier, size-limit, tsx

Patch/minor floors moved up to current releases. TypeScript pinned to
5.9 because ts-jest 29 isn't yet compatible with TS 6.x — bumping
ts-jest is part of the Jest 30 migration in a follow-up commit.

- typescript      5.4.5  → 5.9.x
- prettier        3.3.1  → 3.8.x
- size-limit      11.1.4 → 12.1.x (+ preset)
- tsx             4.11.2 → 4.21.x

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Migrate ESLint to v9 flat config + tseslint v8

- eslint            8.57   → 9.x
- @typescript-eslint stack 7.12  → 8.x  (via the typescript-eslint helper)
- eslint-config-prettier   9.1   → 10.x
- new eslint.config.mjs replaces .eslintrc.js with the flat-config
  shape. The .mjs extension is needed because the root package.json is
  still CJS; the source tree's separate src/package.json declares
  type: module for the build.

Also picks up two prettier reflow nits exposed by running prettier
through the new pipeline (long imports/signatures wrapped to 80 cols).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Bump jest stack to v30 + @types/jest v30

- jest         29.7  → 30.x
- @types/jest  29.5  → 30.x
- ts-jest      29.1  → 29.4 (last 29.x line; supports both jest 29/30
  and TS 5.x. ts-jest 30 is not yet released.)

@types/jest 30 dropped the .lastCalledWith shorthand; switch the test
files to .toHaveBeenLastCalledWith. Behavior is identical.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add publint + arethetypeswrong checks to CI

Both tools caught real issues in the dual-publish setup:

- The shared "types" condition resolved CJS consumers to the ESM
  declaration file (FalseESM masquerade). Split exports into
  per-condition import/require blocks where each carries its own
  "types" entry pointing at the matching dist (mjs vs cjs).
- publint asked for an explicit pkg.type so Node doesn't have to
  detect-and-cache. Set "type": "commonjs" at the root; src/package.json
  still overrides for the build, and the dist/{cjs,mjs}/package.json
  fixup is unchanged.

CI gains a `publish-shape` job that runs `publint` + `attw --pack .`
on the freshly built dist, so packaging regressions land in PR review
rather than after publish.

attw matrix is now 🟢 across node10, node16 (from CJS), node16 (from
ESM), and bundler. publint reports "All good!".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Switch build pipeline to tsdown and fix attw resolution (#30)

* Refactor build pipeline to tsdown

Replace dual tsc invocation (tsconfig.cjs.json + tsconfig.mjs.json) and
the post-build fixup script with a single tsdown config that emits both
CJS and ESM with type declarations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fix attw resolution errors

Point package.json paths at the files tsdown actually emits and split
types per condition so node10, node16 (CJS+ESM) and bundler resolve
correctly:

- Pin tsdown entry to ./src/index.ts so output is dist/index.* (was
  dist/src.*).
- Update main/types and exports to .cjs/.mjs/.d.cts/.d.mts.
- Fix size-limit path that referenced the old dist/mjs layout.
- Move tsdown into devDependencies — it is a build tool, not runtime.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Rename jest.config to .cjs

The package is now type=module, so Node loads .js as ESM and the
CommonJS jest config blew up with "module is not defined". Use the .cjs
extension to keep it as CommonJS without rewriting the config.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Migrate tests from jest to vitest

Vitest is ESM-first and uses Vite's resolver, so the workarounds we
needed for jest under "type": "module" all go away:

- Drop jest.config.cjs (and the .cjs rename); replace with a small
  vitest.config.ts that enables globals so describe/it/expect keep
  working without imports.
- Drop ts-jest, jest, @types/jest; add vitest. Tests transform via
  esbuild, no NodeNext .js suffix mapping needed.
- Switch the two jest.fn() calls to vi.fn() (vi is a vitest global).
- Restore tests/tsconfig.json with moduleResolution: "Bundler" and
  vitest/globals types so the editor type-checks tests against how
  vitest actually resolves them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Convert .prettierrc.js to ESM

Same problem as jest.config.js: under "type": "module" Node loads .js
as ESM, so module.exports throws ReferenceError when prettier (via
eslint-plugin-prettier) loads the config. Switching to export default
keeps the filename and works under either module system.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Upgrade ESLint to v10 and switch config to TypeScript

- eslint and @eslint/js to v10 (typescript-eslint already declares
  ^10.0.0 in its peer range, so no other plugin updates needed).
- Rename eslint.config.mjs to eslint.config.ts; ESLint 9.18+ loads
  TypeScript configs natively when jiti is present.
- Add jiti as a devDependency (the loader ESLint uses for .ts configs).
- Drop @typescript-eslint/eslint-plugin and @typescript-eslint/parser
  from direct deps — the unified typescript-eslint meta-package
  already bundles them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Move Prettier config to TypeScript

Prettier 3.5+ loads .ts config files via Node's built-in type stripping
(stable on >=22.6, on by default from 22.18). Convert .prettierrc.js to
.prettierrc.ts and add a `satisfies Config` for typed feedback in the
editor.

Bump CI from Node 20 to Node 22 so the new config loads in CI too. The
package's own engines.node stays at >=18 — this is a dev-tooling
requirement, not a runtime one.

Prettier itself is already at the latest 3.8.3, no upgrade needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Drop jiti from devDependencies

ESLint only needs jiti to load eslint.config.ts on runtimes without
native TypeScript stripping. We pinned CI to Node 22, where strip-types
is on by default (>=22.18), so ESLint just imports the TS config
directly — verified via --debug, no jiti loader involvement. It's still
pulled in transitively by ESLint's optional peer and by size-limit /
vite, so it remains in node_modules; we just no longer need to declare
it ourselves.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Build before publishing in the release workflow (#31)

The publish-npm job ran `npm ci && npm publish` with no build step.
Combined with `dist/` being gitignored and no prepack/prepublishOnly
hook in package.json, a release would publish a tarball whose `files`
list ("dist", ...) points at a directory that doesn't exist — i.e. an
empty package. Insert `npm run build` between install and publish.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Trim verbose Response-body comments from README (#32)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant