Build before publishing in the release workflow#31
Merged
Conversation
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
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>
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
The release workflow's
publish-npmjob rannpm ci && npm publishwith no build step. Withdist/gitignored and noprepack/prepublishOnlyhook inpackage.json, the published tarball — whosefileslist points atdist— would be effectively empty. This adds an explicitnpm run buildbetween install and publish.Test plan
dist/output (verifiable vianpm pack --dry-runon the release commit, or by inspecting the published artifact).🤖 Generated with Claude Code