Skip to content

fix!: replace unmaintained, insecure ts-node with tsx#6903

Draft
serhalp wants to merge 2 commits intomainfrom
serhalp/frb-1365-replace-ts-node-in-netlifybuild
Draft

fix!: replace unmaintained, insecure ts-node with tsx#6903
serhalp wants to merge 2 commits intomainfrom
serhalp/frb-1365-replace-ts-node-in-netlifybuild

Conversation

@serhalp
Copy link
Member

@serhalp serhalp commented Jan 28, 2026

Summary

ts-node has unresolvable transitive security warnings and has been unmaintained for ~4 years.
tsx is a drop-in replacement that uses esbuild for fast TypeScript on-the-fly execution.

What changes

ts-node was only used for a single scenario in production code.

For local Build Plugins written in TypeScript (local .ts files referenced in netlify.toml):

Behavior Before (ts-node) After (tsx)
Transpilation
Execution
Type checking at load time

Plugins with type errors will now load successfully and may fail at runtime instead of at load time. Implicit type-checking was an undocumented feature and is removed in this PR. It is not consistent with the behaviour of Netlify Functions and Netlify Edge Functions .ts files. And relatively very few users use local Build Plugins to begin with, let alone .ts ones.

Module loading strategy

ts-node only supported CommonJS, so the old code used require() to load TS plugins:

if (tsNodeService !== undefined) {
  return require(pluginPath)
}

tsx supports both module systems natively, so we now use import() for all plugins (TS or not) and
register both loaders:

registerESM()  // hooks into Node's ESM loader for import()
registerCJS()  // hooks into Node's CJS loader for nested require() calls

Registration API differences:

Aspect ts-node tsx
register() returns: Service object with config access Unregister function (not needed)
Config introspection tsNodeService.config.raw.compilerOptions Not available nor needed
ESM/CJS handling CJS only via require() Separate registerESM() + registerCJS()

The service object was used to enrich error messages with the resolved tsconfig.
This is no longer possible with tsx, so addTsErrorInfo() has been removed.

Technical changes

  • Replaced ts-node with tsx in @netlify/build
  • Removed ts-node from @netlify/api devDependencies (this was unused)
  • Replaced a single use of ts-node in a @netlify/build-info test
  • Removed addTsErrorInfo() and tsNodeService parameter threading, as this existed only to
    augment type-checking error details (no longer a thing at all)
  • Deleted tests that asserted type-checking behavior
  • Not quite related, but this PR also includes an npm audit fix that resolves a glob security warning

ts-node has unresolvable transitive security warnings and has been unmaintained for ~4 years.
`tsx` is a drop-in replacement that uses esbuild for fast TypeScript on-the-fly execution.

**For [local Build
Plugins](https://docs.netlify.com/extend/develop-and-share/develop-build-plugins/#local-plugins)
written in TypeScript (local `.ts` files referenced in `netlify.toml`):**

| Behavior | Before (ts-node) | After (tsx) |
|----------|------------------|-------------|
| Transpilation | ✅ | ✅ |
| Type checking at load time | ✅ | ❌ |

Plugins with type errors will now load successfully and may fail at runtime instead of at load time.
The implicit type-checking was an undocumented feature. It is not consistent with the behaviour of
Netlify Functions and Netlify Edge Functions `.ts` files. And relatively very few users use local
Build Plugins to begin with, let alone `.ts` ones.

`ts-node` only supported CommonJS, so the old code used `require()` to load TS plugins:

```js
if (tsNodeService !== undefined) {
  return require(pluginPath)
}

`tsx` supports both module systems natively, so we now use `import()` for all plugins (TS or not) and
register both loaders:

```js
registerESM()  // hooks into Node's ESM loader for import()
registerCJS()  // hooks into Node's CJS loader for nested require() calls
```

Registration API differences:

| Aspect | ts-node | tsx |
|--------|---------|-----|
| `register()` returns | Service object with config access | Unregister function (not needed) |
| Config introspection | `tsNodeService.config.raw.compilerOptions` | Not available nor needed |
| ESM/CJS handling | CJS only via `require()` | Separate `registerESM()` + `registerCJS()` |

The service object was used to enrich error messages with the resolved tsconfig.
This is no longer possible with tsx, so `addTsErrorInfo()` has been removed.

- Replaced `ts-node` with `tsx` in `@netlify/build`
- Removed `ts-node` from `@netlify/api` devDependencies (this was unused)
- Replaced a single use of `ts-node` in a `@netlify/build-info` test
- Removed `addTsErrorInfo()` and `tsNodeService` parameter threading, as this existed only to
  augment type-checking error details (no longer a thing at all)
- Deleted tests that asserted type-checking behavior
@github-actions
Copy link
Contributor

This pull request adds or modifies JavaScript (.js, .cjs, .mjs) files.
Consider converting them to TypeScript.

"supports-color": "^10.0.0",
"terminal-link": "^4.0.0",
"ts-node": "^10.9.1",
"tsx": "^4.21.0",
Copy link
Member Author

Choose a reason for hiding this comment

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

"supports-color": "^10.0.0",
"terminal-link": "^4.0.0",
"ts-node": "^10.9.1",
"tsx": "^4.21.0",
Copy link
Member Author

Choose a reason for hiding this comment

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

This doesn't add any subdependencies, as we already use esbuild@0.27.2 just like tsx: https://node-modules.dev/grid/depth#install=esbuild@0.27.2+tsx@^4.21.0.
It does add ~600 KB for tsx itself.
But we save ~2 MB from removing ts-node: https://node-modules.dev/chart#selected=ts-node@10.9.2&install=ts-node (note: we won't save the 23 MB from typescript because we depend on it elsewhere).

Comment on lines -22755 to -22756
},
"peerDependenciesMeta": {
Copy link
Member Author

Choose a reason for hiding this comment

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

transitive dep with a CVE

Copy link
Member Author

Choose a reason for hiding this comment

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

unrelated other CVE

@serhalp serhalp changed the title fix: replace unmaintained, insecure ts-node with tsx fix!: replace unmaintained, insecure ts-node with tsx Jan 28, 2026
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.

1 participant