From ceac0f798b7c24dcdd2de9c0fb0422565603504d Mon Sep 17 00:00:00 2001 From: Daniel Heene Date: Fri, 9 Oct 2026 10:58:15 +0200 Subject: [PATCH] fix(jobs): load .ts handlers on Node 26 via --import tsx/esm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Payload's dynamicImport() uses eval('import(...)') to hide handler path imports from bundler static analysis. On Node ≥ 26, tsx's module.register() is deprecated and module.registerHooks() is broken when loaded through Payload's bin.js tsImport() call — so no tsx loader is active when the eval'd import fires, and Node chokes on the raw .ts files. Fix: register tsx as a persistent ESM loader upfront via --import tsx/esm in NODE_OPTIONS (dev, _next, _payload) and in start-worker.mjs's spawn args. Combined with --disable-transpile on the Payload CLI, this skips bin.js's own tsx setup (which crashes on Node 26 when tsx is already loaded) and lets all eval'd dynamic imports resolve .ts files and @/ aliases. --- package.json | 6 +++--- scripts/start-worker.mjs | 21 ++++++++++++++++++--- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 75141b33..9b0a2f5b 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "deps:fix": "syncpack fix --dependency-types prod,dev,peer", "deps:lint": "syncpack lint --dependency-types prod,dev,peer", "deps:update": "syncpack update", - "dev": "NODE_OPTIONS=\"--trace-deprecation --disable-warning=DeprecationWarning\" node scripts/dev.mjs", + "dev": "NODE_OPTIONS=\"--import tsx/esm --trace-deprecation --disable-warning=DeprecationWarning\" node scripts/dev.mjs", "dev:app": "bun run _next dev", "dev:storybook": "bun run _storybook dev -p 6006", "dev:email": "bun run _email dev --dir src/emails --port 3005", @@ -36,8 +36,8 @@ "lint": "oxlint && oxfmt --check", "load-env": "node scripts/load-env.mjs", "migrate": "bun run _payload migrate", - "_next": "NODE_OPTIONS=\"--trace-deprecation --disable-warning=DeprecationWarning\" next", - "_payload": "NODE_OPTIONS=\"--trace-deprecation --disable-warning=DeprecationWarning\" payload", + "_next": "NODE_OPTIONS=\"--import tsx/esm --trace-deprecation --disable-warning=DeprecationWarning\" next", + "_payload": "NODE_OPTIONS=\"--import tsx/esm --trace-deprecation --disable-warning=DeprecationWarning\" payload --disable-transpile", "_email": "NODE_OPTIONS=\"--trace-deprecation --disable-warning=DeprecationWarning\" email", "_storybook": "NODE_OPTIONS=\"--trace-deprecation --disable-warning=DeprecationWarning\" storybook", "prepare": "lefthook install", diff --git a/scripts/start-worker.mjs b/scripts/start-worker.mjs index 3104a715..c3b16bb3 100644 --- a/scripts/start-worker.mjs +++ b/scripts/start-worker.mjs @@ -38,10 +38,21 @@ const require = createRequire(import.meta.url) const payloadPkgRoot = path.dirname(path.dirname(require.resolve('payload'))) const payloadCli = path.join(payloadPkgRoot, 'bin.js') +// --import tsx/esm: registers tsx as a persistent ESM loader so that Payload's +// dynamicImport() — which uses eval('import(...)') to hide the call from +// bundler static analysis — can load the .ts handler files at run time. +// --disable-transpile: skips Payload bin.js's own tsx registration, which +// would crash on Node ≥ 26 because tsx 4.22.4's module.register() and +// module.registerHooks() paths are both broken there when tsx is already active. +const tsxArgs = ['--import', 'tsx/esm'] +const payloadArgs = ['--disable-transpile'] + const jobRunner = spawn( process.execPath, [ + ...tsxArgs, payloadCli, + ...payloadArgs, 'jobs:run', '--cron', // every 5 seconds: the web app only enqueues jobs, so this is how quickly @@ -57,9 +68,13 @@ const jobRunner = spawn( }, ) -const healthServer = spawn(process.execPath, [payloadCli, 'run', 'scripts/health-server.ts'], { - stdio: 'inherit', -}) +const healthServer = spawn( + process.execPath, + [...tsxArgs, payloadCli, ...payloadArgs, 'run', 'scripts/health-server.ts'], + { + stdio: 'inherit', + }, +) const children = [jobRunner, healthServer] let shuttingDown = false