-
-
Notifications
You must be signed in to change notification settings - Fork 533
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
Support .js
extensions.
#783
Comments
This sounds largely up to TypeScript to define the working functionality, then |
The current working functionality of tsc does not match ts-node. The links I provided discuss potential future functionality, but at the moment tsc and ts-node do not behave the same, and if tsc doesn't change its behavior then ts-node will, over time, become more and more incompatible with libraries and projects that are targeting modern ES. I see you marked this as an "enhancement" rather than a "bug", this conflicts with my understanding that ts-node and tsc are supposed to behave as close to the same as possible (which, in this case, they do not)? I was under the impression that ts-node was using ts-server under the hood, which already does this sort of path resolution. Perhaps the path resolution logic is in a higher layer that ts-node doesn't get for free? What "module specifier rewrite" are you referring to? The one in microsoft/TypeScript#16577 that people are asking for but hasn't been implemented or approved for inclusion yet in tsc? |
@MicahZoltu In which way, other than having no transpiled output, does |
Ah, I think I understand the problem better now. The issue isn't that ts-node is failing to compile the typescript, it is that it is failing to execute the compiled code. script.ts import { Foo } from 'foo.js'
new Foo() foo.ts export class Foo {} If I I now better understand (correct me if this is wrong) that this is because ts-node doesn't pre-compile the whole project, it compiles files as-needed. So after it compiles script.ts (which will compile fine because TSC is loose with the extensions during resolution), it will begin executing it and upon reaching the attempt to import |
Pretty much, yes. Technically, when type-checking, it has compiled the file already but then node.js takes over resolution and will fail to find the file because |
Ran into this again today, I would like to throw in a vote on my own issue (which I had forgotten I created until I searched GitHub) that ts-node get something like this added (behind a flag is fine). I'm working a lot with esNext code, and at the moment I'm in a pretty bad spot because if I write a library such that it works with esnext, I can't run it with ts-node which is how I do all of my testing. If I write it so it works with ts-node, then it will fail when it comes time to run in the browser. If there isn't interest in getting a flag added to ts-node that tells it to "infer |
Unfortunately, at the moment you have to choose whether to write imports that work in ts-node, or that work in the browser as es-modules. Luckily, for this project there was one function in a second file so we can just pull that into the index file and circumvent the problem. For other projects, resolving this issue will get a lot more messy.
I’m not sure I understand the request of inferring TS when JS is missing, or the linked commit. Both these describe how the module already works today. It doesn’t work with an explicit extension which is different. |
class Foo() {}
import { Foo } from 'foo.js' // A
import { Foo } from 'foo' // B
The request here is to make |
I see, I was confused by the commit referenced because I thought you meant the extension missing, not the file. You should submit a PR if you want any progress on this issue. |
I took a gander at ts-node to see how difficult a PR would be. IIUC, once a single file is transpiled, it is handed off to nodejs to execute. Nodejs will encounter a line like Does this sound correct to you? How is it that ts-node receives an opportunity to handle extensionless files, like if If the above is correct, then I think the way to implement this would be to register Does that approach seem reasonable to you? |
If there’s no JS file on disk, node.js will not trigger the JS extension handler, so that doesn’t work. The only way to do this would be to rewrite paths before node.js executes the file to require dependencies. |
This is getting a bit off topic, but how is it that ts-node receives a callback when nodejs encounters |
Try https://nodejs.org/dist/latest-v12.x/docs/api/modules.html#modules_all_together. |
Thanks for the link. I read over the flow and unfortunately it doesn't mention require extension points so it is still unclear why ts-node doesn't get an opportunity to reroute module loading for non-existent JS files. I think at this point I'm convinced that fixing this in ts-node is very non-trivial, so my inquire now is just professional curiosity. |
Node.js doesn't invoke any loader for a file that doesn't exist. It just enumerates |
If I have |
At this point, I'd recommend you just play with it yourself by editing |
I think the thing you're asking is described by:
Just replace |
Ah, I see. What I was missing was the fact that adding an entry to Thanks for explaining it! |
Note to anyone who ends up here while trying to deal with this issue while we wait for an official fix from Microsoft: You can use a simple transformer I wrote to have the TypeScript compiler add the |
There's no indication that anything is "broken" on the TypeScript end, or that there will be a change. Since it's completely valid, and preferable, in TypeScript to include the |
As of ES2015, TypeScript is not emitting valid JavaScript that can execute in a browser. IMO, this is a bug in TypeScript since one of its design goals is the ability to compile to JS that runs in a browser. |
What's not valid JavaScript? If you include the |
The following is a valid TypeScript file: import { Foo } from './foo'
new Foo() The following is the generated JavaScript if you use es2015 modules and target es2018: import { Foo } from './foo'
new Foo() The latter will not execute in a browser, and there are no plans to change the ES specification such that the latter will execute in a browser. Either TypeScript should be changed such that the above TypeScript file is flagged as invalid (meaning, TypeScript throws a compiler error because you left off the extension) or the TypeScript compiler should be changed such that it appends the proper extension during emit. |
Yeah, but we're not talking about Node invoking require hooks for non-existent So, Node.js will invoke ts-node's hook for |
Ok, here's how to add a transform to // first write a transform (or import it from somewhere)
const transformer = (_) => (transformationContext) => (sourceFile) => {
function visitNode(node) {
if (shouldMutateModuleSpecifier(node)) {
if (typescript.isImportDeclaration(node)) {
const newModuleSpecifier = typescript.createLiteral(node.moduleSpecifier.text.replace(/\.js$/, ''))
return typescript.updateImportDeclaration(node, node.decorators, node.modifiers, node.importClause, newModuleSpecifier)
} else if (typescript.isExportDeclaration(node)) {
const newModuleSpecifier = typescript.createLiteral(node.moduleSpecifier.text.replace(/\.js$/, ''))
return typescript.updateExportDeclaration(node, node.decorators, node.modifiers, node.exportClause, newModuleSpecifier)
}
}
return typescript.visitEachChild(node, visitNode, transformationContext)
}
function shouldMutateModuleSpecifier(node) {
if (!typescript.isImportDeclaration(node) && !typescript.isExportDeclaration(node)) return false
if (node.moduleSpecifier === undefined) return false
// only when module specifier is valid
if (!typescript.isStringLiteral(node.moduleSpecifier)) return false
// only when path is relative
if (!node.moduleSpecifier.text.startsWith('./') && !node.moduleSpecifier.text.startsWith('../')) return false
// only when module specifier has a .js extension
if (path.extname(node.moduleSpecifier.text) !== '.js') return false
return true
}
return typescript.visitNode(sourceFile, visitNode)
}
require('ts-node').register({
// ... other options ...
// then give the transform to ts-node
transformers: {
before: [transformer]
},
}) |
Interesting approach @trusktr. I'm assuming you add |
@MicahZoltu Yeah, or in Webpack apps. |
FWIW, another option is to install a service worker to make the browser automatically add back the
The downside is that you need to wait until after the service worker loads to start the initial module load. EDIT: This just doesn't work reliably enough. This is really a problem and I would argue it's on ts-node to fix, since it's the one piece of tooling that is inconsistent with TypeScript's and everything else's behavior here. |
@shicks The transformer should work fine with |
That's probably the case - I completely forgot about In any case, I ended up just wrapping |
Unfortunately |
Just a thought: If TS is already performing module resolutions during typechecking, can we cache those results and use them when Potential problems:
|
Given that Microsoft has put their foot down (closing and locking microsoft/TypeScript#16577) and will not change course on this, could we consider reopening this to fix it in ts-node? I'd rather not be forced to use |
A few thoughts:
There are 2x possible ways to implement
The latter matches what already happens via The latter also supports dynamic module loading, where the compiler does not have an import statement to rewrite. For example |
# Discussion `ts-node`, for 'reasons', doesn't currently support the TypeScripts import "magic" extension search logic. Discussion and experimentation is on-going and may be added in the future.[^1] For now, as a working replacement, reference the CJS compiled/distributed code directly instead of the TypeScript source code in the TS example. # refs [1]: TypeStrong/ts-node#783
# Discussion `ts-node`, for 'reasons', doesn't currently support the TypeScripts import "magic" extension search logic. Discussion and experimentation is on-going and may be added in the future.[^1] For now, as a working replacement, reference the CJS compiled/distributed code directly instead of the TypeScript source code in the TS example. # refs [1]: TypeStrong/ts-node#783
@cspotcode Running
Is that expected? |
Yeah, it's a node bug. I think if you search around you'll find the
tickets and comments; I know I've explained it a few times before both here
and on node's issue tracker.
…On Fri, Apr 9, 2021 at 1:48 PM Maik Riechert ***@***.***> wrote:
@cspotcode <https://github.com/cspotcode> Running node --loader
ts-node/esm node_modules/mocha/bin/_mocha test/**/*.test.ts throws:
Unknown file extension "" for .../node_modules/mocha/bin/_mocha
Is that expected?
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#783 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAC35OGMMWMCLC4BVPA64FTTH44WRANCNFSM4GX3S4FQ>
.
|
Reference: mochajs/mocha#4267 |
This fixes two problems with the ESM build of this module. 1. The `package.json` that contains `{ "type": "module" }` wasn't being included in the npm tarball 2. When running in an ESM environment, `import foo from './bar'` does not work, you have to specify the extension The fix for the first is simple, add the cjs/esm `package.json` files to the `files` array in the project `package.json`. The second fix is harder. If you just add the `.js` extension to the source files, typescript is happy but ts-node is not, and this project uses ts-node to run the tests without a compile step. Typescript does not support importing `*.ts` and will not support adding `*.js` to the transpiled output - microsoft/TypeScript#16577 ts-node thought this was a bug in Typescript but it turns out not. Their suggestion to use `ts-node/esm` breaks sourcemap support because `source-map-support/register` is not esm - TypeStrong/ts-node#783 There is a PR against ts-node to add support for resolving `./foo.js` if `./foo.ts` or `./foo` fails but it seems to have stalled - TypeStrong/ts-node#1361 Given all of the above, the most expedient way forward seemed to just be to add a shell script that rewrites the various `import` statements in the esm output to add the `.js` extension, then if the ts-node PR ever gets merged the script can be backed out. Fixes beaugunderson#147
per [ts-node issue783](TypeStrong/ts-node#783)
I think I'm hitting a problem like this but its because I'm using BullMQ to create a Node Worker thread for my BullMQ worker it requires a file path as input for the worker implementation which is just a default function. ts-node does not appear to compile the .ts file I have for this worker as its not used anywhere. Attempts to invoke it in the .ts file do not appear to help either. I cannot load a .ts file unless I migrate from CommonJS to ESM modules. Jest does not fully support ESM modules yet and all my existing unit test break when I do that. I think my best bet for now will be to move off of ts-node and onto using tsc watch or something. |
FWIW, I've found that using TS and Node together is just more trouble than it's worth. There's some rumblings about native support coming at some point for at least a subset of TS syntax, but overall I've had much better success with VMs that can support full TS directly (e.g. deno and bun), and then using tsc with the noEmit setting to get proper typechecking. |
Expected Behavior:
This should compile by resolving the import to
./foo.ts
.Actual Behavior:
Errors complaining that it cannot find
./foo.js
../foo.ts
from./foo.js
during compilation.In order to write TypeScript that is compatible with ES2018 module loaders, you must therefore do
import ... from './foo.js'
. TypeScript compiler will see this import and guess that you meanfoo.ts
orfoo.d.ts
. However, ts-node does not and instead will error saying it cannot findfoo.js
.While my personal opinion on this matter is that the inability for ES2018 module loaders to append a default file extension to packages that have no extension is unfortunate, it is none the less the situation we are currently in. The hope is that eventually the TypeScript compiler will be able to be given a flag that tells it to add
.js
when none is present, thus allowing us to writeimport ... from './foo'
and have that generate JS likeimport ... from './foo.js'
, but that day is not yet here.Related issues:
microsoft/TypeScript#16577
microsoft/TypeScript#28288
The text was updated successfully, but these errors were encountered: