Avoid double-transformation of a file when multiple ts-node hooks are registered
Maintainer edit: If you would like to submit a pull request implementing this feature, I have described a straightforward implementation here: https://github.com/TypeStrong/ts-node/issues/409#issuecomment-1068172200
If I require 'ts-node/register' twice, it does not run well. For examples,
my.ts:
export default class My {
msg: string;
constructor() {
this.msg = 'hello';
}
}
index.js:
require('ts-node/register');
My = require('./my').default;
console.log(new My().msg);
If I run with node index.js, it works. But if I run with ts-node index.js, it fails.
In the real world, I run app with ts-node. And config module, which I use, also register ts-node ( https://github.com/lorenwest/node-config/blob/master/lib/config.js#L872 ).
There's no great way to do this except for throwing an error and making it impossible for your code to execute. If I silently allow one and not the other, especially in this case, it's pretty likely the compiler options or configuration options are different and that'll result in an inconsistent runtime. I'd recommend you use a different module (there should be no reason for a config module to alter your runtime, it should do transpilation separately) or make an issue on that module.
I'll keep this open to remind me to throw an error if you try to double register in the future. This will not solve your problem though.
I face similar issue with "config" module. It registers ts-node explicitly https://github.com/lorenwest/node-config/blob/02c98b7f5947ece93357652b69d9cb3da6decfd3/lib/config.js#L878
@blakeembrey, after i read your comment it is clear that fix should be done on "config" side. Can you please suggest the best way to make sure that ts-node is already registered?
@sixmen https://github.com/lorenwest/node-config/pull/458 should fix your issue
I face similar issue with webpack-cli.
ts-node --project ./tool/tsconfig.json ./node_modules/webpack/bin/webpack.js --config ./tool/config.webpack.prod.ts
I modify the config file path with ts-node. But webpack-cli re-register ts-node.
When CWD equals ./, it loads the config file ./tsconfig.json. overwrite my config file ./tool/tsconfig.json.
If there is a way to detect whether it is registered, the caller can solve this problem.
@ZSkycat Do you have a suggestion for how this could be detected? My only thought is storing a global that can be accessed somehow by all modules, but I have no consensus on where this should be stored.
FWIW, you should look at using environment variables in that case too - it's why they're provided.
@blakeembrey I don't know how to use it (environment variables). and it makes the command longer. https://github.com/dividab/tsconfig-paths/issues/48
So, I use it in a strange way.
cd tool && webpack --config config.webpack.dev.ts
@ZSkycat It looks like two issues:
- You might misunderstand
cross-env- you need to remove the&&since it would be executingts-nodewithout the environment variable (says runxand, when successful, runy-x && y) tsconfig-pathsappears to be parsing as JSON, which means you can't have any of TypeScript's enhancements totsconfig.jsonsuch as comments or trailing commas (actually, it's stripping comments - but trailing commas still appear to be an issue)
@blakeembrey
I have a suggestion for how this could be detected
export function isRegistered(){
return require.extensions['.ts'] != undefined || require.extensions['.tsx'] != undefined;
}
Maybe I should create PR?

That’s how this module works but it doesn’t tell you if it’s already been registered - anything could be on those extensions already. A global would be the most appropriate way to solve this, but not sure where it should live.
I think, whether it is "ts-node" or anything else. They register the extension '.ts' or '.tsx'. "ts-node" re-register will cause problems Of course, We also have a way to detect whether it is ts-node registered.
see here https://github.com/TypeStrong/ts-node/blob/master/src/index.ts#L412
- Add custom attributes to handler
- Can even make the caller get configuration data
function registerExtension (
ext: string,
ignore: RegExp[],
register: Register,
originalHandler: (m: NodeModule, filename: string) => any
) {
const old = require.extensions[ext] || originalHandler
let handler = function (m: any, filename) {
if (shouldIgnore(filename, ignore)) {
return old(m, filename)
}
const _compile = m._compile
m._compile = function (code: string, fileName: string) {
debug('module._compile', fileName)
return _compile.call(this, register.compile(code, fileName), fileName)
}
return old(m, filename)
};
handler.__tsNode = true;
handler.__context = {};
require.extensions[ext] = handler;
}
export function isRegistered(){
return require.extensions['.ts'] != undefined || require.extensions['.tsx'] != undefined;
}
export function isTsNodeRegistered(){
if (require.extensions['.ts'] != undefined && require.extensions['.ts'].__tsNode) return true;
if (require.extensions['.tsx'] != undefined && require.extensions['.tsx'].__tsNode) return true;
return false;
}
@ZSkycat Your assumptions are incorrect here. Just because someone has registered .ts and .tsx, that does not mean there'll be issues. Anything could be registered, such as some function that modifies files before ts-node receives them or just logs them. There's also definitely functions that run afterwards, such as babel or instanbul for code coverage. They would remove your __tsNode flag by wrapping it in another function.
This is why I suggested it needs to be a global of some kind. It could be within the module, such as just exports.registered = true, but this can fail if two different versions of ts-node are used. Most likely this needs to live somewhere in the global namespace or somewhere like process.
I spent all day figuring out why my code was compiled twice and found that karma re-registers ts-node. My only path forward was to register and replace it with a noop. It's dirty but very affective :)
require('ts-node').register(/* your options */)
// karma re-registers ts-node
// https://github.com/karma-runner/karma/blob/7617279f68507642fdc7eb0083916f3c6c2a28cb/lib/config.js#L37
// override with noop so it's only called once per process
require('ts-node').register = function() {}
@gmathieu Sorry about that. It's a tricky problem, can you turn it off in karma? When the error gets added to avoid re-registering, it might cause some trouble for you if not.
@blakeembrey karma's config parser automatically registers ts-node in order to support karma.conf.ts. Not much we can do about it other than overriding require('ts-node').register = function() {}. Thanks for following up.
Most likely this needs to live somewhere in the global namespace or somewhere like process.
How about attached to the module module?
require(''module').tsNodeRegistered = true;
I suggest this because it seems the "module" module is responsible for creating require() functions and handling require.extensions.
There’s actually already a global symbol now that could be used. The reason I haven’t done this issue is because it’s a little more complex - you need to check for overlaps in registration directories and not just for a second register. We could put all the registrations in an array and iterate over it when a new registration occurs, check if the directory is already registered.
This sort of issue is also less common now too. I haven’t seen the confusion come up as often, but another solution could be to add a log to the compiler errors when it looks like a TS to TS issue.
@d3lm moving discussion here, and renaming the issue to more accurately describe what I believe is the best solution.
Highlighting Blake's comment above:
you need to check for overlaps in registration directories and not just for a second register. We could put all the registrations in an array and iterate over it when a new registration occurs, check if the directory is already registered.
What if we devise a way to detect when another ts-node instance has already transformed a given file, and we perform this check on a per-file basis? This avoids the need for any complex logic to check for overlapping scopeDir. If an instance of ts-node decides to transform a file, it records that information somewhere so that subsequent ts-node hooks know to skip their own transformation.
Here is one possible solution:
- Standardize on a shared data structure stored somewhere:
process[Symbol.for('ts-node.transformation-meta')] = new Map(); - Each ts-node instance assigns itself a unique ID: could be a UUID, a random number, a non-random sequential number, anything
- When a ts-node instance might need to transform a file, check the data structure:
- If no other instances have transformed the file, then store our own unique ID in that data structure, "claiming" the file
- If another instance has already claimed the file, then skip transformation
I have labelled this issue to indicate we will accept a pull request implementing the proposal outlined in my previous comment. Please do not hesitate to ask me for guidance. Once you know where to look, the implementation and tests should be straightforward.
My limited time is needed elsewhere, so I won't be able to implement this any time soon.
@cspotcode Your proposed solution makes sense to me 👍