Skip to content

Comments

Use tsx for parsing typescript sources#771

Open
noseworthy wants to merge 2 commits intonode-config:masterfrom
noseworthy:use-tsx-for-parsing
Open

Use tsx for parsing typescript sources#771
noseworthy wants to merge 2 commits intonode-config:masterfrom
noseworthy:use-tsx-for-parsing

Conversation

@noseworthy
Copy link

@noseworthy noseworthy commented Aug 16, 2024

ts-node has issues working with esm, so switch it out in favour of using tsx which just works "out of the box".

Summary by CodeRabbit

  • Chores
    • Updated development dependencies for TypeScript compilation and runtime support. These changes affect how TypeScript files are processed during builds and development.

`ts-node` has issues working with `esm`, so switch it out in favour of
using `tsx` which just works "out of the box".
@noseworthy
Copy link
Author

I'm not sure if this is something you'd consider @lorenwest, but I was having issues working with TypeScript and node-config in my esm project. I was able to patch tsx into node-config and it made it so I was able to load my TypeScript configurations regardless if my project was an esm project or commonjs.

@lorenwest
Copy link
Collaborator

Thank you for bringing this to our attention. That sounds like a healthy option to support. Do you have any ideas about how an installation would choose tsx over ts-node? Maybe an environment variable or something? Or maybe an attempt to require tsx with a fallback to ts-node (assuming 100% compatibility)?

It has to run with current installations, so the default may have to be ts-node. This project doesn't include the parser dependencies so if we just switched it to tsx it would break existing installs that don't have that library installed.

@noseworthy
Copy link
Author

So that's why I'm not super dedicated to this approach. tsx uses es-build under the hood which isn't 100% compatible with tsc/TypesScript. For example es-build does not and will not support emitDecoratorMetadata and other deep type reflection features. Hopefully since node-config is only loading config files they wouldn't need those kinds of complicated features, but I'm sure there's someone out there who's doing some wild stuff in their default.ts.

Maybe this can be adapted so that ts-node is tried first, but then it falls back to tsx if ts-node can't transpile the file or something? Alternatively, the documentation could instruct users to install which ever library they prefer and node-config could try each and use whatever works first? Probably with ts-node being tried first as it has been the method used for so long?

I'm not super sure what the best approach is as I don't have much history with this project and wouldn't want to introduce something that's against the goals/style here. Thanks for taking a look though, and happy to bring tsx to your attention. I think it's a pretty neat project!

@lorenwest
Copy link
Collaborator

Leaving open if any contributors want to take on this issue.

@markstos
Copy link
Collaborator

I am not familiar with tsx and am neutral on this proposal.

@jdmarshall
Copy link
Collaborator

TypesScript. For evanw/esbuild#257 and other deep type reflection features.

Is that a bad thing in a config library? Least Power suggests to me that reflection in load time logic might be a bridge too far. I could see a case for this being a wontfix.

Do you know if this plays nice with nyc? If so then I’m on board. Does it make #740 worse, better, indifferent? Could you take a stab at updating the readme to reflect this caveat?

@jdmarshall
Copy link
Collaborator

Do you know if this plays nice with nyc?

Sadly, no.

SyntaxError: Missing initializer in const declaration

@jdmarshall jdmarshall added this to the 4.1 milestone Jul 17, 2025
@jdmarshall jdmarshall removed the 4.1 label Jul 17, 2025
@jdmarshall
Copy link
Collaborator

I was hoping for advice from the Istanbul people but no luck so far

istanbuljs/nyc#1595

@jdmarshall
Copy link
Collaborator

See #847

@jdmarshall
Copy link
Collaborator

Depending on how this ticket plays out, we may need to overlook the downsides of this solution.

@lorenwest @markstos

nodejs/node#59364

@Properko
Copy link

Properko commented Aug 21, 2025

FYI, just ran into an issue (I belive nodejs/node#59364 referenced above) when trying to use native ts support after upgrading node from 22.17 to 22.18.

I load config/default.ts when import config from 'config'; occurs in the codebase.
The error I got after updating to node 22.18 was:

Error: Cannot parse config file: '<PROJECT_PATH>/config/default.ts': Error: Must use import to load ES Module: <PROJECT_PATH>/config/default.ts
require() of ES modules is not supported.
require() of <PROJECT_PATH>/config/default.ts from <PROJECT_PATH>/node_modules/config/parser.js is an ES module file as it is a .ts file whose nearest parent package.json contains "type": "module" which defines all .ts files in that package scope as ES modules.
Instead change the requiring code to use import(), or remove "type": "module" from <PROJECT_PATH>/package.json.

I made a wrapper config/default.js with:

import c from './defaultConfig.ts';
export default c;

This avoids require() on the config file with the unmaintained ts-node and uses node's amaro's type stripping instead.
tsx would have worked as well, but this way it's not really required as a dependency given node is capable of "running" ts today.

Maybe you could somehow work this into your lib?

@jdmarshall jdmarshall modified the milestones: 4.1, 5.0 Jan 10, 2026
@jdmarshall
Copy link
Collaborator

jdmarshall commented Jan 20, 2026

@Properko You're trying to use native mode rather than ts-node, right?

I took a poke at if the type erasure stuff works with node-config and that's a big negatory, good buddy.

All the examples show problems with import statements that require syntax that only works in NodeJS but is a syntax error in all versions of ts-node (I checked) and in my IDE, which is not a good sign.

I don't know if the documentation is wrong. I don't have a TS project at hand to test this with.

@Properko
Copy link

Properko commented Jan 20, 2026

@jdmarshall right. I'd been using node to run typescript with experimental flags and it was unflagged in 22.18.

At the same time something broke with ts-node in 22.18 as I've posted above. So to avoid config using ts-node and crashing on import, my workaround is to make the config lib think I have a javascript config, but that's just wrapper that loads a typescript file which returns the config. I don't nee any dependency/flags for this to work as an end-user.

OTOH, the native typescript is not supported in node_modules (to prevent lib maintainers from publishing raw typescript), so I'm not sure whether the config library can rely on having this capability built-in to node or if it would need to depend on and use amaro explicitly.

@Properko
Copy link

Properko commented Jan 27, 2026

That said, this PR with tsx looks good to me and would prevent the issue I ran into too.
Just thought exploring the native type stripping might be worth it, but if that doesn't work (probably due to the usage limitations in node_modules), disregard my comments.

^ actually nvm, most of my suggestions are based on type stripping, while you support typescript fully including enums etc. which is still flagged in node.

@coderabbitai
Copy link

coderabbitai bot commented Feb 15, 2026

📝 Walkthrough

Walkthrough

Dependency migration replacing ts-node with tsx in package.json and updating the TypeScript runtime module binding from 'ts-node' to 'tsx/cjs/api' in parser.js configuration.

Changes

Cohort / File(s) Summary
TypeScript Runtime Migration
package.json, parser.js
Replaced ts-node (^10.9.2) with tsx (^4.17.0) in devDependencies and updated the module binding path from 'ts-node' to 'tsx/cjs/api' for TypeScript compilation support.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Poem

🐰 Hop, hop, hooray! A speedy new friend,
From ts-node to tsx, the migration's end,
Faster TypeScript runs through the code,
Our transpiler skips down a lighter load! ✨

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: replacing ts-node with tsx for TypeScript parsing in the package.json and parser.js configuration.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into master

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@package.json`:
- Line 79: parser.js currently hardcodes require('tsx/cjs/api') which breaks
users who only have ts-node; modify the TS runtime resolution in parser.js (the
require logic that loads 'tsx/cjs/api') to first attempt requiring 'ts-node'
inside a try/catch and fall back to requiring 'tsx/cjs/api' if that fails, and
also add support for an environment variable (e.g., TS_RUNTIME) that, when
present, forces require of that module name instead of the default resolution
order; ensure the change is implemented where the TS runtime is loaded so tests
still use tsx but existing users with ts-node are supported.

In `@parser.js`:
- Line 35: tsParser is calling TS_DEP.register() with ts-node-specific options
that tsx's register() doesn't accept, so those options are silently ignored;
locate the TS_DEP constant and the tsParser function where register() is invoked
and remove the ts-node options (lazy, ignore, transpileOnly, compilerOptions)
from the call, updating the call to either TS_DEP.register() or
TS_DEP.register({ namespace: '...' }) to match tsx's API, and if any behavior
from those options is required, implement the equivalent via tsx
configuration/environment or another supported API rather than passing ts-node
options to register().

"semver": "5.3.0",
"toml": "^2.0.6",
"ts-node": "^10.9.2",
"tsx": "^4.17.0",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Breaking change for users with ts-node installed.

Replacing ts-node with tsx in devDependencies is fine for the project's own tests, but since parser.js now hardcodes tsx/cjs/api as the TS runtime dependency, any user who currently has ts-node installed (but not tsx) will get a runtime error when loading .ts config files. The PR discussion suggested a fallback strategy (try ts-node first, fall back to tsx, or vice versa) but none was implemented.

Consider either:

  1. Attempting require('ts-node') first with a try/catch fallback to require('tsx/cjs/api'), or
  2. Allowing users to configure the TS parser via an environment variable.

This would preserve backward compatibility while adding tsx support.

🤖 Prompt for AI Agents
In `@package.json` at line 79, parser.js currently hardcodes
require('tsx/cjs/api') which breaks users who only have ts-node; modify the TS
runtime resolution in parser.js (the require logic that loads 'tsx/cjs/api') to
first attempt requiring 'ts-node' inside a try/catch and fall back to requiring
'tsx/cjs/api' if that fails, and also add support for an environment variable
(e.g., TS_RUNTIME) that, when present, forces require of that module name
instead of the default resolution order; ensure the change is implemented where
the TS runtime is loaded so tests still use tsx but existing users with ts-node
are supported.

PPARSER_DEP = 'properties',
XML_DEP = 'x2js',
TS_DEP = 'ts-node';
TS_DEP = 'tsx/cjs/api';
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, locate parser.js and examine lines 35 and 93-102
find . -name "parser.js" -type f | head -5

Repository: node-config/node-config

Length of output: 78


🌐 Web query:

tsx/cjs/api register function API documentation

💡 Result:

The tsx/cjs/api CommonJS Register API is documented here: “CommonJS Register API”. (tsx.is)

What register() does (CJS):

  • const tsx = require('tsx/cjs/api')
  • const unregister = tsx.register() enables requiring TypeScript files in CommonJS (e.g. require('./file.ts')), and unregister() disables it again. (tsx.is)

Scoped / namespaced registration:

  • const api = tsx.register({ namespace: '...' })
  • Then load TS via api.require('./file.ts', __filename) and clean up with api.unregister(). (tsx.is)

Type/overload summary (from tsx’s packaged docs):

  • register(options?: RegisterOptions): Unregister
  • register({ namespace: string, ... }: RegisterOptions): NamespacedUnregister (returns an object that includes require, resolve, and unregister). (tessl.io)

Citations:


🏁 Script executed:

# Read parser.js lines 30-40 to see line 35
sed -n '30,40p' parser.js

# Also read lines 90-105 to see the tsParser function
sed -n '90,105p' parser.js

Repository: node-config/node-config

Length of output: 728


🏁 Script executed:

# Let me verify the complete tsParser function to see the full scope
sed -n '90,110p' parser.js

Repository: node-config/node-config

Length of output: 710


tsx/cjs/api.register() does not accept ts-node options — configuration will be silently ignored.

The TS_DEP was changed to tsx/cjs/api (line 35), but tsParser (lines 93–102) still calls .register() with ts-node-specific options (lazy, ignore, transpileOnly, compilerOptions). According to tsx documentation, register() only accepts an optional { namespace: string } parameter. The ts-node options will be silently ignored, causing the TypeScript registration to behave differently than intended.

The registration call should be updated to match tsx's API:

Proposed fix
 Parser.tsParser = function(filename, content) {
   if (!require.extensions['.ts']) {
-    require(TS_DEP).register({
-      lazy: true,
-      ignore: ['(?:^|/)node_modules/', '.*(?<!\.ts)$'],
-      transpileOnly: true,
-      compilerOptions: {
-        allowJs: true,
-      }
-    });
+    require(TS_DEP).register();
   }
🤖 Prompt for AI Agents
In `@parser.js` at line 35, tsParser is calling TS_DEP.register() with
ts-node-specific options that tsx's register() doesn't accept, so those options
are silently ignored; locate the TS_DEP constant and the tsParser function where
register() is invoked and remove the ts-node options (lazy, ignore,
transpileOnly, compilerOptions) from the call, updating the call to either
TS_DEP.register() or TS_DEP.register({ namespace: '...' }) to match tsx's API,
and if any behavior from those options is required, implement the equivalent via
tsx configuration/environment or another supported API rather than passing
ts-node options to register().

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.

5 participants