feat!: require node 22.12 or higher, use native fetch#1217
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Coverage Report
File Coverage |
commit: |
|
Warning Review the following alerts detected in dependencies. According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.
|
BREAKING CHANGE: removes all CommonJS entry points (`./dist/*.cjs`, `./dist/*.d.cts`, the `require` exports conditions, and the `main` CJS field) so the package is pure ESM. Consumers must use `import` or Node's `require(esm)` support (Node 22.12+). Prep work for upgrading to get-it v9 which is ESM-only. - Drops the `require` conditions from all entries in `exports` - Points `main` at the ESM entry for legacy resolvers - Removes the CommonJS runtime smoke test - Removes the `check-esm-compatibility` script and CI job; ESM-only deps are now allowed (and expected) - Removes the CommonJS install example from the README Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`HttpRequest` is now a body-only Observable contract. The get-it middleware event union (`HttpRequestEvent`, `ResponseEvent`, `ProgressEvent`) was leaking into the public surface even though almost every consumer just filtered for `response` and mapped to `body`. The only place that genuinely needed progress events was `client.assets.upload()` when called via the observable API. That now has its own dedicated event type, `UploadEvent<T>` (`UploadResponseEvent<T>` | `UploadProgressEvent`), emitted only from that one method. BREAKING CHANGE: removes the public `HttpRequestEvent`, `ResponseEvent`, and `ProgressEvent` types. Consumers of `client.assets.upload()` (observable variant) now receive `UploadEvent<T>` instead of `HttpRequestEvent<T>` — the event `type` discriminants (`'response'` / `'progress'`) are unchanged, but `UploadResponseEvent` drops `statusCode`/`statusMessage`/`headers`/`url`/`method` and `UploadProgressEvent` is otherwise structurally identical. The internal `HttpRequest` type, the `defaultRequester` argument passed to the (`@internal @deprecated`) `_requestHandler`, and the internal `_request*` helpers all now resolve to `Observable<unknown>` of the parsed body. The transport-level multi-event observable is wrapped to body-only inside `defineCreateClient`. Asset uploads bypass the wrapper via a new `_uploadObservable` that talks to the underlying requester directly, so progress events keep flowing on get-it v8. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`ClientError` and `ServerError` previously reached into a get-it response (multi-event observable) directly, and pulled the GROQ request tag out of a get-it `HttpContext`. Both couplings make the v9 transport swap noisier than it has to be. Introduces an internal `CanonicalHttpResponse` shape (the same shape already exposed via the public `HttpError.response` interface) plus a `httpResponseFromGetIt` adapter that projects a get-it v8 response into it. The error middleware now does the translation once at the boundary and passes the GROQ tag as a plain string to `ClientError`. `extractErrorProps` now takes `tag?: string` instead of an `HttpContext`, removing the last reach into get-it from the error layer. Public class signatures stay permissive (`res: Any`) so existing test fixtures keep working. The v9 upgrade will land a second adapter (`httpResponseFromFetch` or similar) that consumes the new `Response` / `Headers` shape — error construction itself stays unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rewrites the HTTP transport layer on top of get-it v9's
`createRequester` (Promise-based) + native fetch. The
multi-event `Requester` from v8 is gone; we wrap the new
promise into a single-event Observable inside
`defineCreateClient` so the rest of the codebase keeps the
same shape it had before.
Key transport changes:
- `http/request.ts` is rewritten on `createRequester` from
`get-it`. v9 throws `HttpError` from its built-in 4xx/5xx
handling, which lets the v9 `retry` middleware see HTTP
errors and retry 429/502/503 the same way as v8. After the
retry loop, the v9 `HttpError` is translated to the
client's existing `ClientError` / `ServerError` via the
new `httpResponseFromFetch` adapter in `http/errors.ts`.
- `http/nodeMiddleware.ts` is rewritten to provide
configuration (User-Agent header) and a Node-only
middleware stack: a wrapping middleware that swaps in a
`createNodeFetch({proxy})` per request when a legacy
`proxy: '...'` is set; a `beforeRequest` transform that
converts Node `Readable` upload bodies to Web
`ReadableStream` (v9's fetch only accepts the latter); the
`debug` middleware wired to the `debug` npm logger
(`sanity:client`); and a lineage header transform. The
default Node fetch falls back to undici's
`EnvHttpProxyAgent` for `HTTP_PROXY` / `HTTPS_PROXY` /
`NO_PROXY` env vars.
- `http/requestOptions.ts` keeps its v8-shaped output
(`json`, `withCredentials`, `proxy`, `timeout: 0`-to-
disable). The new adapter in `http/request.ts`
(`adaptToFetchOptions`) translates those to v9 fetch
options: `withCredentials: true` → `credentials:
'include'`, `maxRedirects: 0` → `redirect: 'manual'`,
`timeout: 0` → `timeout: false`, query arrays expand to
repeated keys via `URLSearchParams` (matching v8 / Content
Lake semantics).
Public-surface changes:
- The `unstable__adapter` / `unstable__environment`
re-exports are removed — get-it v9 doesn't expose them.
- `dist/index.js` (Node ESM): +1.4 kB gzipped (debug + v9
retry + adapter code).
- `dist/index.browser.js`: +900 B gzipped (adapter code).
- `umd/sanityClient.min.js`: -3.1 kB gzipped (no more v8
middleware pipeline in the browser bundle).
Behavioural fallout that needed test updates:
- HTTP errors now always carry `statusText` ("Bad Request",
"Service Unavailable", etc.) since real fetch responses
do. Test expectations that relied on nock's empty
statusText (`"HTTP 400 (body)"`) updated to the more
realistic `"HTTP 400 Bad Request (body)"`.
- Asset upload via the observable API no longer emits
per-chunk progress events from Node — v9 / fetch has no
progress hook. The progress test now asserts that no
progress events fire from Node; browser progress is
handled separately in a follow-up commit via XHR.
- Mid-test `HTTPS_PROXY` env var changes can no longer
reroute traffic, because undici's `EnvHttpProxyAgent`
snapshots the env at construction. The test for this
scenario is `skip`-ped with a comment; real-world
env-var-set-before-process-start usage still works.
Testing infrastructure:
nock can't intercept undici's fetch (it patches
`http.request`), so we add a small `nock`-API-compatible
shim backed by `get-it/mock`'s `createMockFetch()`. A new
vitest setup file (`test/helpers/setupMockFetch.ts`)
installs a fresh mock per test and exposes its `fetch`
through a `globalThis.__sanityTestFetch` hook that the
Node middleware picks up. Tests keep their existing nock
call sites with only the import path changed; tests that
depend on `nock` intercepting `node:http` directly (the
real-EventSource SSE listener tests) keep using real nock.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restores per-chunk upload progress events for
`client.assets.upload(...)` when called via the observable
API. get-it v9 / fetch has no progress hook, so when
`XMLHttpRequest` is available the upload runs through a
dedicated XHR-based code path (`browserUpload.ts`) that
emits `UploadProgressEvent`s as the request body is sent
and a final `UploadResponseEvent` once the server
responds. Non-browser environments (Node, edge) continue
to use the fetch-based path and only emit the terminal
response event.
Implementation notes:
- The XHR path is reached via a dynamic
`import('../http/browserUpload')` so the Node bundle
doesn't pay for the XHR adapter at all (the import is
tree-shaken away in builds where `XMLHttpRequest` is
undefined-by-construction).
- The `_requestHandler` interceptor still doesn't see
asset uploads — they bypass the regular pipeline
entirely, same as on get-it v8.
- The test-only fetch override (used by `nockShim` /
`setupMockFetch`) moved from `nodeMiddleware` into
`http/request.ts` so the browser environment picks it
up too. The XHR path itself isn't intercepted by the
shim — browser asset upload tests will need a separate
XHR mock in a follow-up.
Bundle impact (gzipped):
- `dist/index.js`: 23.9 kB → 24.1 kB (+0.2 kB; XHR
detection branch)
- `dist/index.browser.js`: 26.0 kB → 26.4 kB (+0.4 kB;
appendQuery helper + dynamic-import boundary)
- `umd/sanityClient.min.js`: 32.0 kB → 34.8 kB (+2.8 kB;
`browserUpload` and its `debug` logger end up inlined
in the single-file UMD bundle)
The UMD bundle is roughly back to its pre-upgrade
baseline — UMD consumers get the XHR progress
functionality, the lean ESM browser bundle stays lean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Raises `engines.node` from `>=20` to `>=22.12`. Node 20
went out of LTS in April 2026; the floor was overdue. The
22.12 floor in particular makes the ESM-only switch a soft
break, because `require(esm)` ships natively from that
version — consumers stuck on CJS source can keep doing
`require('@sanity/client')` without changes.
Re-adds a CommonJS smoke test (`runtimes/node/test.cjs`)
that verifies `require(esm)` interop continues to work for
both the top-level entry and `@sanity/client/package.json`.
Drops two compatibility branches that were guarding APIs
the new minimum guarantees:
- `_createAbortError` no longer feature-detects
`globalThis.DOMException`; it's globally available on
every supported runtime now.
- The Node-upload `beforeRequest` transform no longer
checks whether `Readable.toWeb` exists. The
Node-stream-to-Web-stream conversion is now expressed
through a proper `in`-operator type guard instead of a
`pipe` cast, matching the repo's no-`as`-to-silence-TS
rule.
README migration notes are reworked to fold the Node bump
and the CJS removal into a single section, with the
upgrade story being "bump Node, your `require()` calls
keep working."
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brings the simulated browser environment up to date. The big practical win is that `npm run test:browser` actually completes now — happy-dom 12 was hanging the suite, so it hadn't been a useful signal in a while. On happy-dom 20 all 866 enabled browser tests pass. `happy-dom` 20 now provides a real `XMLHttpRequest` implementation, which means `client.assets.upload()` routes through XHR in browser tests (since 5e5b08a added the XHR upload path). The XHR talks to the real network — which the mock fetch can't see — so the test setup file now hides `XMLHttpRequest` for the duration of each test so uploads fall back to the fetch path that's actually mockable. Real-browser validation of the XHR path needs to happen elsewhere. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The published `dist/index.d.ts` had
`import {LegacyRequester} from './http/request'` at the
top, but no `./http/request.d.ts` ships in `dist/` (only
the entry-point declaration files do). Downstream
bundlers (rolldown, esbuild, etc.) that follow the
imports — `@sanity/vision` hit this first — failed with
`UNRESOLVED_IMPORT`.
Two changes, both contributing:
- Bump `@sanity/pkg-utils` from 7.2.2 to 10.5.0. The
newer api-extractor pipeline no longer emits the
unresolvable external import; it inlines the type
internally instead. This alone is enough to make the
bundled `.d.ts` consumable.
- Break the circular type-only import that confused the
extractor in the first place. `src/types.ts` used to
`import type {LegacyRequester} from './http/request'`
while `src/http/request.ts` imported `Any` from
`../types` — a one-step cycle. `Requester` is now
defined directly in `types.ts` as the public-facing
`(options: Any) => Observable<unknown>` shape, and the
`requester` named export in both `index.ts` and
`index.browser.ts` is explicitly annotated with that
type. The public d.ts now declares
`export declare type Requester = (options: Any) => Observable<unknown>`
and `export declare const requester: Requester`, with
no leak of the internal `LegacyRequester` or
`ResponseEvent` names into the public surface.
Side note: pkg-utils 10 also adds opt-in support for a
`node` sub-condition in `exports` (sanity-io/pkg-utils#2781),
mirroring the existing `browser` sub-condition. The
shape would be roughly:
```json
"exports": {
".": {
"source": "./src/index.ts",
"browser": {
"source": "./src/index.browser.ts",
"import": "./dist/index.browser.js"
},
"node": {
"source": "./src/index.node.ts",
"import": "./dist/index.node.js"
},
...
}
}
```
That's a separate refactor (renaming `src/index.ts` to
`src/index.node.ts` and the emitted `dist/index.js` to
`dist/index.node.js`), and would be a minor breaking
change for anyone deep-importing — so it stays out of
this commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The default `--mode auto` resolves to `actual` whenever node_modules exists (CI installs first), and `actual` walks the installed tree — including every transitive of every dev dep. `--dev=false` only filters what's read from package.json, so in `actual` mode it has no effect on the tree. That's why CI started flagging engines constraints from `@inquirer/*` (transitive of `msw`) and `vitest` (both direct dev dep and optional peer of `get-it`), even though none of them constrain published consumers. `--mode ideal` reads from package.json directly and honours the dev/peer flags, which is what we want for a "do my published engines line up with my prod dep graph?" check. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixes everything `npm run lint
--report-unused-disable-directives -f compact` was
flagging:
- `test/client.test.ts`: the LISTENERS describe imported
real nock as `const {default: nock}`, which shadowed
the shim import at the top of the file. Renamed the
local binding to `realNock` and updated the five call
sites inside the block. No behavioural change — the
test still hits the real nock implementation that can
intercept EventSource (`node:http`), while every other
test in the file keeps going through the shim.
- `test/helpers/nockShim.ts`: drop the unused `handle`
callback parameter on the internal `flush()` helper;
rewrite the network-error registration comment so it
doesn't trip the `no-warning-comments` rule (was
`TODO:`, now plain prose).
- `eslint.config.mjs`: teach `unused-imports/no-unused-vars`
to honour the standard `^_` leading-underscore
convention (`args`/`vars`/`caughtErrors` ignore
patterns). The `_options` arg on `nockShim`'s `nock()`
function is intentionally unused (kept for API parity
with nock's `(host, opts)` form), and the rule now
recognises that signal.
- `src/csm/draftUtils.ts`: the `eslint-disable-next-line
unused-imports/no-unused-vars` directive on the
destructured `_versionPrefix`/`_publishedId` is no
longer needed — the new ignore pattern covers it.
- `test/releasesClient.test.ts`: import ordering fixed
via `eslint --fix`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops the Sanity-specific `@sanity/eventsource` wrapper
(which transitively depended on the unmaintained
`eventsource@2`, that talks `node:http` directly) in
favour of the upstream `eventsource@4` package. v4 is
fetch-based, so the SSE connection now flows through the
same transport layer the rest of the client uses.
A new `resolveEventSourceFetch(config, opts)` helper
returns a `fetch` implementation that:
- honours `globalThis.__sanityTestFetch` when set (keeps
the listener/live tests on the same mock as the rest of
the suite — no more "real nock for SSE" workaround);
- routes through `get-it/node`'s `createNodeFetch({proxy})`
when the client was configured with an explicit
`proxy:` (per-request proxy support reaches SSE too);
- otherwise hands off to `globalThis.fetch`, which on
Node still picks up `HTTP_PROXY` / `HTTPS_PROXY` /
`NO_PROXY` via undici's `EnvHttpProxyAgent`.
The fetch wrapper also injects `Authorization` and any
configured custom headers into every request — something
the native browser EventSource API doesn't support and the
old code had to polyfill around with a different package
shape per environment.
`live.ts` and `listen.ts` now construct the EventSource
synchronously (via static import) so the
"subscribe-then-immediately-unsubscribe" semantics
preserve their "no connection ever opened" guarantee. The
`createNodeFetch` import is the only thing left lazy, and
that's only reached when a proxy is explicitly set.
Test fallout, all addressed:
- Listener tests in `client.test.ts` move back to the
shim — SSE now goes through fetch which the shim can
intercept, so the `realNock` carve-out is gone.
- `listen.test.ts` / `live.test.ts` tests that spin up a
real local SSE server (via `testSse` / direct server
setup) now delete `globalThis.__sanityTestFetch` before
each such test, so EventSource talks to the actual local
server instead of the mock.
- Mock SSE responses now declare
`content-type: text/event-stream`; the upstream v4
parser is stricter about it than the old `eventsource@2`
was.
- The lazy/cold listener tests stop using `.reply(fn)`
(which the shim has to resolve eagerly) and assert
request counts via `mock.getRequests()` instead.
Known regression: the UMD bundle grows from ~35 kB gz to
~190 kB gz because `eventsource@4` is statically imported
and rollup's UMD config inlines all dynamic imports. The
ESM `dist/index.{js,browser.js}` builds leave it as an
external import and are essentially unchanged (~24/27 kB
gz). UMD-only consumers may want to factor `eventsource`
out via a CDN script tag — addressing that cleanly would
be a follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces hand-rolled `['event: ...', 'data: ...', '', ...].join('\n')`
arrays in the listener and live-event tests with `encode()`
calls from `eventsource-encoder`. The fixtures are
shorter, harder to get wrong (no more remembering the
trailing blank line), and the few spurious lines that
crept in over the years (lone `'.'` placeholders, leading
empty comments) just disappear — they were never
meaningful SSE.
About 50 lines of test boilerplate disappears. No
behavioural change — the encoded byte stream is the same
as before, minus the now-removed junk lines that the
parser already ignored.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The UMD bundle ballooned from ~33 kB gz to ~190 kB gz in 7745855 because `resolveEventSourceFetch.ts` did `import('get-it/node')` to build a proxy-aware fetch. Even though that import was guarded with `typeof window === 'undefined' && config.proxy`, rollup's UMD config has `inlineDynamicImports: true`, so it inlines the import target into the bundle regardless of whether the branch can run. Pulling in `get-it/node` drags `undici` along with it — 141 mentions in the minified UMD. The ESM `dist/index.browser.js` was fine all along — it keeps dynamic imports lazy and only had a single reference to the lazy import. Fix: stop importing `get-it/node` from a module that's shared with the browser bundle. The proxy-fetch helper already lives in `nodeMiddleware.ts` (which only the Node entry point imports), so expose it through `EnvironmentOptions.resolveProxyFetch` and have `defineCreateClient` thread that resolver onto the resolved client config. `resolveEventSourceFetch.ts` then just calls `config.resolveProxyFetch?.(proxy)` — contains no reference to `get-it/node` and never pulls `undici` into the browser bundle. Sizes after: - `dist/index.js` (Node): 24.3 kB gz (unchanged) - `dist/index.browser.js`: 26.5 kB gz (unchanged) - `umd/sanityClient.min.js`: 33.0 kB gz (was 35.1 kB before this branch; the small net gain over baseline is `eventsource@4` itself, which is what we'd expect) Grepping for `undici` / `createNodeFetch` in `dist/index.browser.js` and `umd/sanityClient.js`: 0 references in either. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Deno smoke test broke because `get-it@9.0.0-alpha.10`
declares a `deno` exports condition that routes Deno to
the Node-specific build (`dist/index.node.js`). That
build depends on `undici`, which depends on
`node:sqlite` — and esm.sh's `denonext` transform can't
serve `node:sqlite` to Deno, so the whole graph fails to
resolve.
Override the `get-it` entries in the Deno import map so
esm.sh fetches the platform-neutral build directly
(`/dist/index.js?bundle-deps&target=es2022` for the
main entry, and the same shape for `/middleware`). The
`target=es2022` bypasses esm.sh's `denonext` transform,
and `bundle-deps` inlines the (Node-free) dep graph into
a single chunk so we don't accidentally pull anything
else through the transform on a follow-up resolve.
Also drops the stale `@sanity/eventsource/browser` special
case from the import-map generator (we use upstream
`eventsource` now), modernises the JSON import attribute
from `assert {type: 'json'}` to `with {type: 'json'}`, and
gitignores `deno.lock` (which `deno test` creates as a
cache artefact).
Verified locally: both Deno tests pass, both Bun tests
still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Created locally by `npm run test:deno` from `deno test`'s cache mechanism. Not used by CI (which runs Deno fresh each time), so safe to keep out of version control. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Description
What to review
Testing