Skip to content

feat: Convert nock to TypeScript + ESM#2968

Merged
mikicho merged 9 commits into
betafrom
Michael/typescript
May 7, 2026
Merged

feat: Convert nock to TypeScript + ESM#2968
mikicho merged 9 commits into
betafrom
Michael/typescript

Conversation

@mikicho
Copy link
Copy Markdown
Member

@mikicho mikicho commented Apr 20, 2026

What

This PR modernizes nock's codebase in two steps:

  1. TypeScript — All source files converted from .js to .ts. Node.js runs .ts files directly via built-in type stripping (stable since v23.6). No transpile step, no build step for development.

  2. ESM — Converted from CommonJS (require/module.exports) to ES Modules (import/export). Package now declares "type": "module".

  3. This PR does not include a CommonJS version, but we can add it later if needed using a dual package build.

Why

TypeScript without a build step. Node.js v23.6+ strips type annotations natively at startup. This means:

  • Types are always in sync with the code
  • Contributors edit .ts files and run them directly — no compilation, no source maps, no watching
  • erasableSyntaxOnly ensures we only use type syntax that Node.js can erase (no enums, no constructor parameter properties, no namespaces)

ESM gives us:

  • Play more nicely with TypeScript and Node.js typestripping.

Auto-generated declarations. The hand-written index.d.ts was a maintenance burden — it duplicated types from the source and was drifting. Now npm run build:types generates all .d.ts files from source.

Changes

Source files (lib/, index.ts):

  • Renamed all .js.ts
  • Replaced JSDoc @param/@returns/@typedef with inline TypeScript
  • Converted require()/module.exportsimport/export
  • Replaced interfaces with inferred types where possible (BackContext, InterceptorSurface)
  • NetConnectNotAllowedError converted to a proper class
  • Replaced the Jest leak test with a plain Node.js script that creates 1000 mock cycles and checks heap growth stays under 2MB.

Configuration:

  • eslint.config.mjs: added typescript-eslint parser, updated file patterns
  • .gitignore: types directory (generated on publish)

Tooling:

  • nyc → c8 for coverage
  • Lint scripts target **/*.{js,ts}

What didn't change

  • Zero runtime behavior changes. All 655 tests pass without modification to the test logic.
  • Public API is identical. import nock from 'nock' works, nock.Scope, nock.Interceptor etc. all resolve.
  • No new dependencies. typescript-eslint is dev-only.

@mikicho
Copy link
Copy Markdown
Member Author

mikicho commented Apr 20, 2026

@markscamilleri Following #2861, I'd love to hear your thoughts on this.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Modernizes the nock codebase by moving the runtime sources to TypeScript (run directly in Node via type-stripping) and converting the package from CommonJS to ESM, along with updating tests/tooling to match.

Changes:

  • Converted runtime entrypoint and lib/ sources from CJS .js to ESM .ts, and updated package metadata (type, exports, main).
  • Replaced hand-maintained typings + dtslint with generated declarations (tsc --emitDeclarationOnly) and added TS project configs.
  • Updated Mocha/Jest tests and examples to ESM imports, updated lint/coverage tooling and CI workflow.

Reviewed changes

Copilot reviewed 95 out of 99 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
types/tslint.json Removed dtslint config (switching away from dtslint).
types/tsconfig.json Removed old DefinitelyTyped-style tsconfig for types tests.
types/tests.ts Removed dtslint-based type test suite.
types/index.d.ts Removed hand-written declaration file (now generated).
tsconfig.json Added base TS config for source type-checking.
tsconfig.build.json Added TS config for generating .d.ts into types/.
package.json Switched to ESM + .ts entrypoint, updated engines, scripts, deps, exports, and types generation.
eslint.config.mjs Added TypeScript ESLint config and updated patterns/ignores for ESM+TS.
.gitignore Ignored generated types/ output.
.mocharc.json Replaced JS mocha config with JSON config for ESM project.
.mocharc.js Removed old CommonJS mocha config file.
.github/workflows/continuous-delivery.yaml Added a CI step to generate types before release.
CHANGELOG.md Removed obsolete TODO notes about dtslint/types generation.
index.ts New ESM TypeScript entrypoint wiring up exports and types.
index.js Removed old CommonJS entrypoint.
lib/back.ts Converted nock-back implementation to TS/ESM and added type surface.
lib/common.ts Converted shared utilities to TS/ESM and added typed signatures.
lib/debug.ts Converted debuglog helpers to TS/ESM.
lib/debug.js Removed old CommonJS debug module.
lib/global_emitter.ts Added typed global emitter (TS/ESM).
lib/global_emitter.js Removed old CommonJS global emitter.
lib/handle-request.ts Converted request handling to TS/ESM; NetConnectNotAllowedError now a class.
lib/intercept.ts Converted interceptor registry to TS/ESM and updated undici lazy-load path.
lib/interceptor.ts Converted Interceptor implementation to TS/ESM and exported key types.
lib/interceptors/builtin.ts Converted builtin interceptor wiring to TS/ESM.
lib/interceptors/undici.ts Converted undici integration to TS/ESM and updated dispatcher classes.
lib/match_body.ts Converted request-body matching util to TS/ESM.
lib/playback_interceptor.ts Converted playback logic to TS/ESM and tightened types.
lib/recorder.ts Converted recorder to TS/ESM, introduced TS types for recorder options/output.
lib/scope.ts Converted Scope to TS/ESM and introduced TS types for options/definitions.
lib/stringify.ts Converted safe stringify helper to TS/ESM and exported default.
lib/utils/node/index.ts Added TS/ESM utilities for GET-with-body support.
lib/utils/node/index.js Removed old CommonJS node utils module.
lib/create_response.js Removed old CommonJS response creation helper.
tests/setup.js Converted test setup to ESM imports.
tests/servers/index.js Converted test server helpers to ESM exports and updated path resolution.
tests/test_abort.js Converted to ESM imports and updated nock import target.
tests/test_abort_signal.js Converted to ESM imports and updated nock import target.
tests/test_back.js Converted to ESM and updated fixture path resolution.
tests/test_client_request.js Converted to ESM imports and updated server helper import.
tests/test_common.js Converted to ESM imports and updated internal module imports.
tests/test_destroy.js Converted to ESM imports and updated nock import target.
tests/test_fetch.js Converted to ESM imports and updated fixture path resolution.
tests/test_gzip_request.js Converted to ESM imports and updated nock import target.
tests/test_ipv6.js Converted to ESM imports and updated nock import target.
tests/test_reply_with_error.js Converted to ESM imports and updated nock import target.
tests/test_socket.js Converted to ESM imports and updated nock import target.
tests/test_stringify.js Updated test to import stringify from TS source.
tests/test_undici.js Converted to ESM imports and updated nock import target.
tests/test_unix_socket.js Converted to ESM imports and adjusted Windows skip logic.
tests/got/got_client.js Converted got client helper to ESM default export.
tests/got/fixtures/logging.mjs Updated to import nock from TS entrypoint.
tests/got/test_allow_unmocked.js Converted to ESM imports and updated server/nock imports.
tests/got/test_allow_unmocked_https.js Converted to ESM imports and updated server/nock imports.
tests/got/test_back_filters.js Converted to ESM imports and updated fixtures path resolution.
tests/got/test_basic_auth.js Converted to ESM imports and updated nock import target.
tests/got/test_body_match.js Converted to ESM imports and updated nock import target.
tests/got/test_content_encoding.js Converted to ESM imports and updated nock import target.
tests/got/test_default_reply_headers.js Converted to ESM imports and updated nock import target.
tests/got/test_define.js Converted to ESM imports and updated nock import target.
tests/got/test_delay.js Converted to ESM imports and updated assets path resolution.
tests/got/test_dynamic_mock.js Converted to ESM imports and updated nock import target.
tests/got/test_events.js Converted to ESM imports and updated assets path resolution.
tests/got/test_fake_timer.js Converted to ESM imports and updated nock import target.
tests/got/test_header_matching.js Converted to ESM imports and updated nock import target.
tests/got/test_intercept.js Converted to ESM imports and updated internal util import.
tests/got/test_intercept_parallel.js Converted to ESM imports and updated nock import target.
tests/got/test_logging.js Converted to ESM imports and updated fixtures path resolution.
tests/got/test_net_connect.js Converted to ESM imports and updated server/nock imports.
tests/got/test_nock_lifecycle.js Converted to ESM imports and updated server/nock imports.
tests/got/test_nock_off.js Converted to ESM imports and updated reload logic to dynamic import.
tests/got/test_passthrough.js Converted to ESM imports and updated server/nock imports.
tests/got/test_persist_optionally.js Converted to ESM imports and updated assets path resolution.
tests/got/test_query.js Converted to ESM imports and updated nock import target.
tests/got/test_query_complex.js Converted to ESM imports and updated nock import target.
tests/got/test_recorder.js Converted to ESM imports and updated server/nock imports.
tests/got/test_redirects.js Converted to ESM imports and updated nock import target.
tests/got/test_remove_interceptor.js Converted to ESM imports and updated nock import target.
tests/got/test_reply_body.js Converted to ESM imports and updated nock import target.
tests/got/test_reply_function_async.js Converted to ESM imports and updated nock import target.
tests/got/test_reply_function_sync.js Converted to ESM imports and updated nock import target.
tests/got/test_reply_headers.js Converted to ESM imports and updated assets path resolution.
tests/got/test_reply_with_file.js Converted to ESM imports and updated assets path resolution.
tests/got/test_repeating.js Converted to ESM imports and updated nock import target.
tests/got/test_request_overrider.js Converted to ESM imports and updated server/nock imports.
tests/got/test_scope.js Converted to ESM imports and updated fixture path resolution.
tests/got/test_stream.js Converted to ESM imports and updated assets path resolution.
tests/got/test_url_encoding.js Converted to ESM imports and updated got helper import.
tests_jest/memory_leak.spec.js Converted Jest test to ESM import and TS entrypoint reference.
examples/_log.js Converted example logger helper to ESM default export.
examples/binary-reply.js Converted example to ESM imports and updated path resolution.
examples/delay-response.js Converted example to ESM imports and updated nock import target.
examples/net-connect-default-no-mock.js Converted example to ESM imports.
examples/net-connect-default-other-mock.js Converted example to ESM imports and updated nock import target.
examples/net-connect-disabled-different-host.js Converted example to ESM imports and updated nock import target.
examples/net-connect-mock-same-host-different-path.js Converted example to ESM imports and updated nock import target.
examples/socket-delay-abort.js Converted example to ESM imports and updated nock import target.
examples/socket-delay-no-abort.js Converted example to ESM imports and updated nock import target.
Comments suppressed due to low confidence (3)

lib/intercept.ts:214

  • activate() lazy-loads the undici interceptor via createRequire(...)._require('./interceptors/undici.ts'). Since ./interceptors/undici.ts is an ES module (uses import/export) and has a .ts extension, this require() call is expected to throw (e.g., ERR_REQUIRE_ESM / unknown extension), and that error code is not handled by the current catch block. Consider switching this to a dynamic import() approach (and restructuring activate accordingly), or providing a loadable CommonJS entry specifically for this sync require path.
    lib/recorder.ts:173
  • record is typed as record(recOptions: boolean | RecorderOptions), but the public API supports calling rec() with no arguments (and the implementation tolerates undefined). Making this parameter optional (recOptions?: ...) preserves backward-compatible typings for TS consumers.
    lib/scope.ts:328
  • getScopeFromDefinition uses URL.parse(...), but URL here is the global WHATWG URL class (not node:url’s legacy url module) and does not have a .parse() method. This will throw at runtime when nockDef.port is present. Import and use the legacy parser (e.g., import url from 'node:url' + url.parse(...)), or switch to new URL(nockDef.scope) and read .port from that.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread package.json
Comment on lines 19 to +23
"engines": {
"node": ">=18.20.0 <20 || >=20.12.1"
"node": ">=22.6.0"
},
"main": "./index.ts",
"type": "module",
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description states Node’s built-in TS type-stripping is “stable since v23.6”, but engines.node is set to >=22.6.0 while main/exports point at .ts sources. If runtime execution truly depends on stable type-stripping, consider aligning engines.node with that minimum to avoid installs that cannot execute the entrypoint.

Copilot uses AI. Check for mistakes.
@gr2m
Copy link
Copy Markdown
Member

gr2m commented Apr 20, 2026

  1. TypeScript — All source files converted from .js to .ts. Node.js runs .ts files directly via built-in type stripping (stable since v23.6). No transpile step, no build step for development.

I always preferred to have JS sources for libraries that are used by others. With Node 23.6+, we would publish the TS code directly, without a build step?

@mikicho
Copy link
Copy Markdown
Member Author

mikicho commented Apr 21, 2026

@gr2m I knew I forgot something 😅 (Node.js does not allow this anyway)
Added a tiny build step and tested it locally.

@mikicho mikicho changed the base branch from beta to main April 21, 2026 09:39
@mikicho mikicho changed the base branch from main to beta April 21, 2026 09:40
@mikicho mikicho requested a review from Copilot April 21, 2026 10:50
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 100 out of 104 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (2)

lib/scope.ts:392

  • getScopeFromDefinition calls URL.parse(...), but URL here is the global WHATWG URL class and does not have a parse method. This will throw at runtime when a definition includes a port. Import the legacy parser from node:url (e.g. import url from 'node:url' and use url.parse(...)), or rewrite this logic to use new URL(...) and read .port.
    lib/intercept.ts:249
  • activate() uses createRequire(...)._require('./interceptors/undici.ts') to load an ESM module. On Node versions without require(esm) support (and/or when loading TS sources), this throws ERR_REQUIRE_ESM and undici mocking gets silently disabled even if undici is installed. Prefer a sync-safe approach: import the local undici wrapper normally and have it require('undici') internally, or bump the supported Node engine(s) to versions where require(esm) is guaranteed.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

strategy:
fail-fast: false
matrix:
node-version:
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workflow comment says it verifies against engines.node, but the matrix no longer tests Node 20 even though package.json declares >=20.12.1. Either add Node 20 back to the matrix or raise the engines.node minimum to match what CI actually validates.

Suggested change
node-version:
node-version:
- 20

Copilot uses AI. Check for mistakes.
Comment on lines +160 to +170
## ESM

Nock is now a pure ES module. If your project uses CommonJS, you can still use `require()` with the Node.js's built-in `require(esm)` support (stable since v22.12):

```js
// ESM
import nock from 'nock'

// CommonJS (Node.js >= 22.12)
const { default: nock } = require('nock')
```
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section documents CommonJS usage via require(esm) (Node >=22.12), but package.json currently declares support for Node >=20.12.1. Please clarify the minimum Node version required for CommonJS consumers here (or update engines.node accordingly) so users on Node 20/21 don’t assume require('nock') will work.

Copilot uses AI. Check for mistakes.
@mikicho
Copy link
Copy Markdown
Member Author

mikicho commented May 4, 2026

@gr2m I'd love your feedback on this one.

@mikicho
Copy link
Copy Markdown
Member Author

mikicho commented May 7, 2026

@gr2m I'm merging it. Please share your feedback anytime. thanks!

@mikicho mikicho merged commit d0ac909 into beta May 7, 2026
14 checks passed
@mikicho mikicho deleted the Michael/typescript branch May 7, 2026 19:33
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 7, 2026

🎉 This PR is included in version 15.0.0-beta.11 🎉

The release is available on:

Your semantic-release bot 📦🚀

@gr2m
Copy link
Copy Markdown
Member

gr2m commented May 7, 2026

Looking great!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants