tsx: `watch` mode in `node.js 20` doesn't watch changes in non-entry files

Bug description

watch mode works on node.js lts/hydrogen v18, but doesn’t on 20+.

Reproduction

  1. Create 2 files
// file-1.ts - this is your entrypoint
import { example } from './file-2';
console.log(example);
// file-2.ts
export const example = 'Hello World!';
  1. Run tsx watch file-1.ts
  2. Modify value in file-2.ts
  3. Observe there’s no rerun by tsx

Environment

System:
  OS: MacOS (currently latest 13.3)
  CPU: m1 pro
  Shell: zsh
Binaries:
  Node: 20.1.0
npmPackages:
  tsx: `3.12.7` (current latest)

Can you work on a fix?

  • I’m interested in opening a pull request to address this issue.

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Reactions: 13
  • Comments: 15 (4 by maintainers)

Most upvoted comments

Found a temporary solution by using tsx as a loader instead:

node --watch --loader=tsx index.ts

Wrote a fix https://github.com/esbuild-kit/esm-loader/pull/64 based on @fspoettel research. Working well for me, but can somebody test it in him configurations also too?

@privatenumber, waiting for review.

Wow thank you so much for the insightful investigation @fspoettel I think we can work on a fix now

I don’t think this is caused by chokidar, but a change to IPC that affects @esbuild-kit/esm-loader.

It appears as if we don’t receive dependency messages from the esm-loader anymore. I patched in the following debug logs:

https://github.com/esbuild-kit/tsx/blob/9acf1c0eadcc6b7275b91a285acb180171b30d78/src/watch/index.ts#L86

runProcess.on('message', (data) => {
  console.log("received", data);

https://github.com/esbuild-kit/esm-loader/blob/332a0a0220dbb80de806ebb3a15ec7c3ce893bee/src/loaders.ts#L214

console.log("sending esm", url, "process.send", process.send);
if (process.send) {

https://github.com/esbuild-kit/cjs-loader/blob/3442035bd4d1251538141ecd3e80de1add41024a/src/index.ts#L55

console.log("sending cjs", filePath, "process.send", process.send);
if (process.send) {

This gives the following outputs (on macOS):

Node 18.17.0

> node-typescript-starter@0.0.0 dev
> tsx watch --clear-screen=false ./src/main.ts | pino-pretty

sending esm file:///Users/felix/code/node-ts-template/src/main.ts process.send [Function (anonymous)]
sending esm file:///Users/felix/code/node-ts-template/node_modules/dotenv/lib/main.js process.send [Function (anonymous)]
received {
  type: 'dependency',
  path: 'file:///Users/felix/code/node-ts-template/src/main.ts'
}
sending esm file:///Users/felix/code/node-ts-template/src/lib/logger.ts process.send [Function (anonymous)]
received {
  type: 'dependency',
  path: 'file:///Users/felix/code/node-ts-template/node_modules/dotenv/lib/main.js'
}
sending esm file:///Users/felix/code/node-ts-template/src/lib/config.ts process.send [Function (anonymous)]
received {
  type: 'dependency',
  path: 'file:///Users/felix/code/node-ts-template/src/lib/logger.ts'
}
received {
  type: 'dependency',
  path: 'file:///Users/felix/code/node-ts-template/src/lib/config.ts'
}
sending esm file:///Users/felix/code/node-ts-template/node_modules/pino/pino.js process.send [Function (anonymous)]
received {
  type: 'dependency',
  path: 'file:///Users/felix/code/node-ts-template/node_modules/pino/pino.js'
}
sending cjs /Users/felix/code/node-ts-template/node_modules/dotenv/lib/main.js process.send [Function (anonymous)]
received {
  type: 'dependency',
  path: '/Users/felix/code/node-ts-template/node_modules/dotenv/lib/main.js'
}

<...>

Node 20.5.0

> node-typescript-starter@0.0.0 dev
> tsx watch --clear-screen=false ./src/main.ts | pino-pretty

sending esm file:///Users/felix/code/node-ts-template/src/main.ts process.send undefined
sending esm file:///Users/felix/code/node-ts-template/node_modules/dotenv/lib/main.js process.send undefined
sending esm file:///Users/felix/code/node-ts-template/src/lib/logger.ts process.send undefined
sending esm file:///Users/felix/code/node-ts-template/src/lib/config.ts process.send undefined
sending esm file:///Users/felix/code/node-ts-template/node_modules/pino/pino.js process.send undefined
sending cjs /Users/felix/code/node-ts-template/node_modules/dotenv/lib/main.js process.send [Function (anonymous)]
sending cjs /Users/felix/code/node-ts-template/node_modules/pino/pino.js process.send [Function (anonymous)]
sending cjs /Users/felix/code/node-ts-template/node_modules/pino-std-serializers/index.js process.send [Function (anonymous)]
sending cjs /Users/felix/code/node-ts-template/node_modules/pino-std-serializers/lib/err.js process.send [Function (anonymous)]
received {
  type: 'dependency',
  path: '/Users/felix/code/node-ts-template/node_modules/dotenv/lib/main.js'
}
<...>

In this case, no esm module is ever received as process.send is not defined when the loader tries to send dependencies over IPC.

I looked for causes and found this change to the loader API that might be related.

In a github issue that discusses a seemingly similar case, it was suggested that globalPreload could be a workaround.

Hope this is helpful!