Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid double-transformation of a file when multiple ts-node hooks are registered #409

Open
sixmen opened this issue Aug 21, 2017 · 19 comments
Labels
enhancement you can do this Good candidate for a pull request.
Milestone

Comments

@sixmen
Copy link

sixmen commented Aug 21, 2017

Maintainer edit: If you would like to submit a pull request implementing this feature, I have described a straightforward implementation here: #409 (comment)

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 ).

@blakeembrey
Copy link
Member

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.

@electroma
Copy link

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?

@electroma
Copy link

@sixmen node-config/node-config#458 should fix your issue

@ZSkycat
Copy link

ZSkycat commented Jun 19, 2018

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.

@blakeembrey
Copy link
Member

@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.

@ZSkycat
Copy link

ZSkycat commented Jun 19, 2018

@blakeembrey
I don't know how to use it (environment variables). and it makes the command longer.
dividab/tsconfig-paths#48

So, I use it in a strange way.
cd tool && webpack --config config.webpack.dev.ts

@blakeembrey
Copy link
Member

blakeembrey commented Jun 20, 2018

@ZSkycat It looks like two issues:

  1. You might misunderstand cross-env - you need to remove the && since it would be executing ts-node without the environment variable (says run x and, when successful, run y - x && y)
  2. tsconfig-paths appears to be parsing as JSON, which means you can't have any of TypeScript's enhancements to tsconfig.json such as comments or trailing commas (actually, it's stripping comments - but trailing commas still appear to be an issue)

@ZSkycat
Copy link

ZSkycat commented Jun 20, 2018

@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?

image

@blakeembrey
Copy link
Member

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.

@ZSkycat
Copy link

ZSkycat commented Jun 20, 2018

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;
}

@blakeembrey
Copy link
Member

blakeembrey commented Jun 20, 2018

@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.

@gmathieu
Copy link

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() {}

@blakeembrey
Copy link
Member

@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.

@gmathieu
Copy link

@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.

@cspotcode
Copy link
Collaborator

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.

@blakeembrey
Copy link
Member

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.

@cspotcode cspotcode changed the title Prevent re-register Avoid double-transformation of a file when multiple ts-node hooks are registered Mar 15, 2022
@cspotcode
Copy link
Collaborator

cspotcode commented Mar 15, 2022

@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

@cspotcode cspotcode added this to the 10.8.0 milestone Mar 15, 2022
@cspotcode cspotcode added the you can do this Good candidate for a pull request. label Mar 16, 2022
@cspotcode
Copy link
Collaborator

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.

@d3lm
Copy link

d3lm commented Mar 16, 2022

@cspotcode Your proposed solution makes sense to me 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement you can do this Good candidate for a pull request.
Projects
None yet
Development

No branches or pull requests

7 participants