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

Provide a way to add the '.js' file extension to the end of module specifiers #16577

Open
QuantumInformation opened this issue Jun 16, 2017 · 91 comments

Comments

@QuantumInformation
Copy link

commented Jun 16, 2017

In order to use es6 modules in the browser, you need a .js file extension. However output doesn't add it.

In ts:
import { ModalBackground } from './ModalBackground';
In ES2015 output:
import { ModalBackground } from './ModalBackground';

Ideally I would like this to be output
import { ModalBackground } from './ModalBackground.js';

That way I can use the output in Chrome 51

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Webpack boilerplate</title>
  <script type="module" src="index.js"></script>
</head>
<body></body>
</html>

image

Related to #13422

@cyrilletuzi

This comment has been minimized.

Copy link

commented Jun 16, 2017

It's not just related to #13422, it's the same issue. But responses have been quite negatives, despite the fact I think it's a important issue, so hope your issue will be better received.

@QuantumInformation

This comment has been minimized.

Copy link
Author

commented Jun 16, 2017

Well, I hope its added, we were really looking forward to discussing my POC using this in my next TypeScript podcast, but looks like we will have to wait to use TypeScript with no build tools.

@DanielRosenwasser

This comment has been minimized.

Copy link
Member

commented Jun 17, 2017

At the moment TypeScript doesn't rewrite paths. It's definitely annoying, but you can currently add the .js extension yourself.

@DanielRosenwasser DanielRosenwasser changed the title Add .js file extensions to import declarations output for use in Chrome 51 ES6 module imports Provide a way to add the '.js' file extension to the end of module specifiers Jun 17, 2017

@DanielRosenwasser

This comment has been minimized.

Copy link
Member

commented Jun 17, 2017

@QuantumInformation

This comment has been minimized.

Copy link
Author

commented Jun 17, 2017

Thanks for the tip, I'll write a shell/node script to do this.

@justinfagnani

This comment has been minimized.

Copy link

commented Jun 18, 2017

@DanielRosenwasser would it make sense to collect the native ES6 module issues under a label?

@justinfagnani

This comment has been minimized.

Copy link

commented Jun 18, 2017

Also, to generalize this issue a bit, I don't think it's actually about adding a .js extension, but resolving the module specifier to an actual path, whatever the extension is.

@QuantumInformation

This comment has been minimized.

Copy link
Author

commented Jun 19, 2017

I've come across another issue which isn't really the domain of typescript but it's for my use case.

I'm not sure how to handle node_modules. Normally webpack bundles them into the code via the ts-loader but obviously, this is not understood by the browser:

import { KeyCodes } from 'vanilla-typescript;
https://github.com/quantumjs/vanilla-typescript/blob/master/events/KeyCodes.ts#L3

Adding a js extension here is meaningless.

I guess there would have to be a path expansion by typescript or a url resolver running on the server.

I appreciate its a rather niche case, but I think it would be a way TS could shine early in this area. Maybe it could be a plugin to the tsc compiler?

@QuantumInformation

This comment has been minimized.

Copy link
Author

commented Jun 22, 2017

For anyone coming to this and wants an interim solution I wrote a script to add a js file extension to import statements:

"use strict";

const FileHound = require('filehound');
const fs = require('fs');
const path = require('path');

const files = FileHound.create()
  .paths(__dirname + '/browserLoading')
  .discard('node_modules')
  .ext('js')
  .find();


files.then((filePaths) => {

  filePaths.forEach((filepath) => {
    fs.readFile(filepath, 'utf8', (err, data) => {


      if (!data.match(/import .* from/g)) {
        return
      }
      let newData = data.replace(/(import .* from\s+['"])(.*)(?=['"])/g, '$1$2.js')
      if (err) throw err;

      console.log(`writing to ${filepath}`)
      fs.writeFile(filepath, newData, function (err) {
        if (err) {
          throw err;
        }
        console.log('complete');
      });
    })

  })
});

I might make this into a cli tool..

@aluanhaddad

This comment has been minimized.

Copy link
Contributor

commented Aug 9, 2017

@justinfagnani's comment hits the nail on the head.

Also, to generalize this issue a bit, I don't think it's actually about adding a .js extension, but resolving the module specifier to an actual path, whatever the extension is.

when you write

import { KeyCodes } from 'vanilla-typescript';

or for that matter

import { KeyCodes } from 'vanilla-javascript';

you are importing from an module specifier, it may or may not be a file but adding .js to the end in this case is not likely to result in a valid resolution.

If you are writing a NodeJS application then the NodeJS Require algorithm will attempt various resolutions but it will likely not attempt to resolve it to 'vanilla-typescript.js' because it references an abstract name and will, by convention and by configuration, be resolved (perhaps over various attempts) to something like '../../../node_modules/vanilla_typescript/index.js'.

Other environments, such as AMD have differences as to how they perform this resolution but one thing that all of these environments have in common is some notion of an abstracted module specifier.

I bring this up because the ES Module implementations shipping in various browsers implement something that is incomplete. If we consider even our simplest dependencies, and as soon as we broach the subject of transitive ones, it becomes clear that there will need to be a way to configure the doggone thing.

That may be far off, but as you are discovering, it is not realistic to write to this (politely) proof of concept implementation we have been given.

Furthermore, I do not see how TypeScript could possibly help here since the issue is environment specific.

@QuantumInformation your program for adding .js to paths looks nice, lightweight, elegant even, but you are ultimately implementing your own module bundler. That is fun and interesting work but it demonstrates the deficiencies in the current implementations available in browsers. Even if you write in pure JavaScript, you still need something to compile and package your transitively imported dependencies.

I am basically just ranting about the fact that the implementation of ES Modules that was released is tremendously far from adequate.

Again NodeJS, RequireJS AMD, Dojo AMD, Sea Package Manager, CommonJS, Browserify, Webpack, SystemJS, all have their own differing ways of doing things but they all provide abstract name resolution. They have to provide it because it is fundamental.

Thank you for reading my rant.

@AviVahl

This comment has been minimized.

Copy link

commented Nov 10, 2017

Not sure which version of TS added it, but imports such as './file.js' now work (even if the file is actually file.ts).
TypeScript resolves the file fine, and outputs the complete .js import to the target.
lit-html use it: https://github.com/PolymerLabs/lit-html/blob/master/src/lib/repeat.ts#L15

@cyrilletuzi

This comment has been minimized.

Copy link

commented Nov 11, 2017

It's possible since TS 2.0. But tools like webpack don't support it so in the end it's useless.

@AviVahl

This comment has been minimized.

Copy link

commented Nov 11, 2017

It's useless if one is using ts-loader on sources (the most common use-case).
It is still possible to bundle the target (usually "dist" folder), as the actual js file exists there and can be found by the resolution process.

I wonder if I could implement a quick transformation in ts-loader that strips .js extensions from the target code, allowing one to bundle directly from sources.

@cyrilletuzi

This comment has been minimized.

Copy link

commented Nov 11, 2017

Feel free to do so, that would be great. I posted the issue on main webpack ts loaders, like ts-loader, a few months ago, and I've been quite badly received...

For information, there is no problem with the rollup typescript plugin, as a proof it's doable.

@aluanhaddad

This comment has been minimized.

Copy link
Contributor

commented Nov 11, 2017

I fail to see what good this does until browser loader implementations and the WGATWG loader spec support at least some configuration because most dependencies won't load correctly.

From my point of view, none of this matters until it is practical to use the native loader on an import that refers to an arbitrary string literal specifier, something that may not yet be a URL, and have that go through a transformation that yields the actual URL.

Until then we will remain dependent on tools like SystemJS and Webpack.

@AviVahl

This comment has been minimized.

Copy link

commented Nov 11, 2017

I created a tiny transformer that strips the '.js' from import/export statements.
I used tsutils type guards, so yarn add tsutils --dev. (the package is usually installed anyway if you have tslint in your project, so so extra dependency)

https://gist.github.com/AviVahl/40e031bd72c7264890f349020d04130a

Using this, one can bundle ts files that contain imports from files that end with .js (using webpack and ts-loader), and still transpile sources to esm modules that can load in the browser (using tsc).

There is probably a limited number of use-cases where this is useful.

EDIT: I updated the gist to work with exports as well. it's naive and not optimized, but works.

@QuantumInformation

This comment has been minimized.

Copy link
Author

commented Dec 30, 2017

Any movement on this issue?

@SalathielGenese

This comment has been minimized.

Copy link

commented Dec 30, 2017

This matter of extension take us back to the very begining of TypeScript and why a tsconfig.json was needed and why a module option was added to the compilerOptions setting.

Since that matter of extension of extension matters only for ES2015+ as require is able to resolve quite well, let it be added by the compiler when targeted code is ES2015+.

  1. .js for .ts
  2. .jsx for .tsx
@matthewp

This comment has been minimized.

Copy link

commented Jan 12, 2018

Hello, I'm coming at this late but would like to help. I am having trouble understanding what the issue is here. From the OP example it is:

import { ModalBackground } from './ModalBackground';

Is the issue that we don't know what './ModalBackground' is? It could be a folder or something else?

If we run tsc on the entire project and we know that ModalBackground.ts exists, then we would know that it is safe to add the extension, no?

@benlesh

This comment has been minimized.

Copy link

commented Jan 12, 2018

This issue is also something the RxJS community is very interested in. What is the timeline on a solution for this? Is it even prioritized? Are there any third party transformations that would help?

@TheLarkInn

This comment has been minimized.

Copy link
Member

commented Jan 12, 2018

I'm not really sure if this is a problem if the output target is ES2015 is it? This could maybe fall into the domain of a ES2015browser capability. Even more so, @justinfagnani can't we push for this as a platform goal to worry about? (Maybe need to fork into separate thread).

@MicahZoltu

This comment has been minimized.

Copy link
Contributor

commented Jun 29, 2019

Setting the imports to .js causes ts-node to cease to work properly for the file. This is because ts-node only compiles one file at a time and if the emitted file contents ends up with require('./foo.js') then when nodejs processes that file it will try to load ./foo.js which doesn't exist anywhere on disk and thus it will fail. When the code does require(./foo) on the other hand, ts-node's handler will be called at which point it can compile ./foo.ts into JS and return that.

If TypeScript emits .js extensions in all cases, ts-node will run into the same problem. However, if there is an option to toggle whether an extensions is automatically added or not then ts-node can set that compiler option to off, which will allow the current system to continue to work.

@guybedford

This comment has been minimized.

Copy link
Contributor

commented Jun 29, 2019

Since Node.js --experimental-modules requires mandatory file extensions, the API for this is straightforward without needing dependency analysis - an option like --jsext can rewrite any .ts extension to a .js extension. JSON / other format imports can then work fine with this too, provided the user has already explicitly included all their extensions. The only problematic case is a package name that ends in .ts like import 'npmpkg.ts'. This case is extremely rare, but to comprehensively handle it in the resolution the rule could be to make an exception for bare specifiers along the lines of - if the bare specifier (not a URL, or relative path) is a valid npm package name (matching /^(@[-_\.a-zA-Z\d]+\/)?[-_\.a-zA-Z\d]+$/, from https://github.com/npm/validate-npm-package-name), then ignore the .ts extensions in the rewriting.

@MicahZoltu

This comment has been minimized.

Copy link
Contributor

commented Jul 2, 2019

I have created a TypeScript compiler transformer that will append a .js file extension onto any relative import path. This means if your .ts file has import { Foo } from './foo' it will emit import { Foo } from './foo.js'. It is available on NPM, and instructions for how to use it can be found in the project readme.

https://github.com/Zoltu/typescript-transformer-append-js-extension

If something like this were to be integrated into TypeScript compiler (as a compiler option) it should probably be smarter about deciding when to append the .js extension and when not to. Right now it looks for any path starting with ./ or ../ and that doesn't have a . anywhere else in the path. This means it will not do the right thing in a number of edge case scenarios, though I question whether anyone actually will run into those edge cases or not.

@guybedford

This comment has been minimized.

Copy link
Contributor

commented Jul 2, 2019

@MicahZoltu great to see userland solutions to this. I think it is quite important though that the mental model becomes always include file extensions, such that the TypeScript extension option can become turn .ts extensions into .js extensions on compile. That avoids the resolution edge cases, leaving just the case of npm package names that happen to end in ".ts", which can be handled as I discussed in my previous comment.

@MicahZoltu

This comment has been minimized.

Copy link
Contributor

commented Jul 2, 2019

@guybedford Including .js extension in a .ts file makes it so the file cannot execute in ts-node. Solving the problem in ts-node is far from non-trivial due to the way NodeJS does file resolution. This means that if you publish a library with hard coded .js extensions, the library will not work for anyone using ts-node. See discussion on this over at TypeStrong/ts-node#783.

@guybedford

This comment has been minimized.

Copy link
Contributor

commented Jul 2, 2019

@MicahZoltu I mean including .ts extension in a .ts file rather.

@justinfagnani

This comment has been minimized.

Copy link

commented Jul 2, 2019

@guybedford Including .js extension in a .ts file makes it so the file cannot execute in ts-node.

This is yet another reason why auto-executing TypeScript in node and browsers is a bad idea.

@justinfagnani

This comment has been minimized.

Copy link

commented Jul 2, 2019

@MicahZoltu I mean including .ts extension in a .ts file rather.

I think the problem with this is that it breaks the equivalence between a .ts file and a .js/.d.ts pair. This means that if you're importing a file that's compiled with the current project you use .ts, and if you move the file to a different project you need to change imports to it to use .js.

It also means that the output doesn't work in standard environments without the transformer. Given that there's no way to install a transformer from a .tsconfig file, that means you always have to use a custom compiler. That's a pretty big barrier to put in place for the small benefit of using .ts instead of .js.

@guybedford

This comment has been minimized.

Copy link
Contributor

commented Jul 2, 2019

This means that if you're importing a file that's compiled with the current project you use .ts, and if you move the file to a different project you need to change imports to it to use .js.

External / internal import boundaries are well-defined things. If a dependency moves from being an internal .ts file of the current same-build, to being an external .js file of another package import that has its own separate build or perhaps even isn't TypeScript, then yes, you have to change the extension as it's a completely different concept.

It also means that the output doesn't work in standard environments without the transformer.

While a transformer can help us explore this, a --ts-to-js or similar compiler flag / option is very much needed to solve this issue.

@MicahZoltu

This comment has been minimized.

Copy link
Contributor

commented Jul 3, 2019

@guybedford You convinced me that putting in .ts was the right thing to do for the plugin. However, upon trying to implement it I learned that TypeScript doesn't actually allow it!

// foo.ts
export function foo() { console.log('foo') }
// bar.ts
import { foo } from './foo.ts' // Error: An import path cannot end with a '.ts' extension. Consider importing './foo' instead
foo()
@magic-akari

This comment has been minimized.

Copy link
Contributor

commented Jul 3, 2019

Hello, What about this?


input:

// src/lib.js.ts
export const result = 42;
// src/index.js.ts
import { result } from "./lib.js";

console.log(result);

output:

// build/lib.js
export const result = 42;
// build/index.js
import { result } from "./lib.js";

console.log(result);

issue #30076

@MicahZoltu

This comment has been minimized.

Copy link
Contributor

commented Jul 3, 2019

A sole .ts extension on a TypeScript file feels most appropriate to me. Often you don't know until compile time what the target is. For example, in many of my library projects I have multiple tsconfig.json files that target different environments, such as targeting modern browsers which should emit .js files and map import paths to .js, or targeting modern Node, which should emit .mjs files and map import paths to .mjs.

@guybedford

This comment has been minimized.

Copy link
Contributor

commented Jul 3, 2019

@MicahZoltu that importing files ending in .ts is an error is possibly to our benefit actually. Because this means the .ts to .js extension rewriting on compile can be added whenever .ts extensions are used without needing a flag :)

Perhaps a PR to enable trailing .ts extensions being supported and rewritten to .js extensions (possibly under a flag to enable this feature, but a flag that can be removed in time) would be a great way to start along this path.

//cc @DanielRosenwasser

@TomasHubelbauer

This comment has been minimized.

Copy link

commented Jul 10, 2019

This issue is really long already so someone might have already said this, so at risk of repeating what has already been said:

TypeScript should allow developers to specify extensions in imports because that it valid JavaScript. And this is not about JS/TS extension only! In ESM, any extension is valid as long as the MIME type is correct.

import actuallyCode from './lookLikeAnImage.png';

…should be valid as long as the server serves valid JavaScript in that file and sets the correct MIME type. This goes further as the same could be true for TS! A TS file could be just a JS source code served with JS MIME type and so a valid path for an ESM module import.

IMO TypeScript should only consider imports without an extension (like it wants you today) and leave the ones with alone, no error, warning etc. Otherwise it rejects valid JavaScript code which is pretty unfortunate for something that claims to be a superset of JS.

Sorry if this is not the right place to raise this, I saw a few other issues: #18971, #16640, #16640 but issues on this topic seems to be getting closed left and right so I assume this is the "main" one since it was allowed to stay open.

SignpostMarv added a commit to SignpostMarv/mapapi.js that referenced this issue Jul 21, 2019
@SalathielGenese

This comment has been minimized.

Copy link

commented Jul 25, 2019

Playing with NodeJS v12.7.0

salathiel@salathiel-genese-pc:~/${PATH_TO_PROJECT}$ node --experimental-modules dist/spec/src/ioc
(node:15907) ExperimentalWarning: The ESM module loader is experimental.
internal/modules/esm/default_resolve.js:59
  let url = moduleWrapResolve(specifier, parentURL);
            ^

Error: Cannot find module '${PROJECT_ROOT}/dist/spec/src/ioc' imported from ${PROJECT_ROOT}/
    at Loader.resolve [as _resolve] (internal/modules/esm/default_resolve.js:59:13)
    at Loader.resolve (internal/modules/esm/loader.js:73:33)
    at Loader.getModuleJob (internal/modules/esm/loader.js:149:40)
    at Loader.import (internal/modules/esm/loader.js:133:28)
    at internal/modules/cjs/loader.js:830:27
    at processTicksAndRejections (internal/process/task_queues.js:85:5) {
  code: 'ERR_MODULE_NOT_FOUND'
}
salathiel@salathiel-genese-pc:~/${PATH_TO_PROJECT}$ node --experimental-modules dist/spec/src/ioc.js
(node:16155) ExperimentalWarning: The ESM module loader is experimental.
internal/modules/esm/default_resolve.js:59
  let url = moduleWrapResolve(specifier, parentURL);
            ^

Error: Cannot find module '${PROJECT_ROOT}/dist/spec/src/observe' imported from ${PROJECT_ROOT}/dist/spec/src/ioc.js
    at Loader.resolve [as _resolve] (internal/modules/esm/default_resolve.js:59:13)
    at Loader.resolve (internal/modules/esm/loader.js:73:33)
    at Loader.getModuleJob (internal/modules/esm/loader.js:149:40)
    at ModuleWrap.<anonymous> (internal/modules/esm/module_job.js:43:40)
    at link (internal/modules/esm/module_job.js:42:36) {
  code: 'ERR_MODULE_NOT_FOUND'
}

Now, I'm not sure why this is annoying...

At the moment TypeScript doesn't rewrite paths. It's definitely annoying, but you can currently add the .js extension yourself.

@DanielRosenwasser #16577 (comment)

What about implementing behind an option? Like --rewrite-paths (rewritePaths: true) ?

@viT-1

This comment has been minimized.

Copy link

commented Aug 14, 2019

@MicahZoltu

This comment has been minimized.

Copy link
Contributor

commented Aug 14, 2019

@viT-1 Here is a workaround for the time being: #16577 (comment)

@viT-1

This comment has been minimized.

Copy link

commented Aug 14, 2019

@MicahZoltu I just solve my usecase by SystemJS bundling (tsconfig outFile option)

@richardkazuomiller

This comment has been minimized.

Copy link

commented Sep 5, 2019

I haven't read every comment people have written about this but I don't know understand why I have to write .js a gazillion times. I want the computer to do it for me.

@Draccoz

This comment has been minimized.

Copy link

commented Sep 10, 2019

@richardkazuomiller We all do, but @DanielRosenwasser stated in #16577 (comment) that they are not willing to do that for now (I am surprised this issue is still misleadingly open for such a long time after the above statement). If you want the computer to do that for you, consider using a bundler or some 3rd party tool to handle import paths rewrites.

@QuantumInformation

This comment has been minimized.

Copy link
Author

commented Sep 10, 2019

Who wants me to close this issue?

@TomasHubelbauer

This comment has been minimized.

Copy link

commented Sep 10, 2019

Please don't close, I'm still waiting to see how will the TypeScript team respond to how are they going to approach ESM imports which allow for any extension to be used and rely on MIME type instead. At the moment this is possible in JavaScript but not in TypeScript. (Please see my previous comment for details.)

@Draccoz

This comment has been minimized.

Copy link

commented Sep 10, 2019

@TomasHubelbauer This is why I proposed a flag in config file, it could indicate that all extension-less imports should have .js (or any other configured) extension added. This would be opt-in, so default could be false, making existing projects or projects with different needs work just as is.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.