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 · 59 comments

Comments

Projects
None yet
@QuantumInformation
Copy link

QuantumInformation 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

cyrilletuzi 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

QuantumInformation 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

DanielRosenwasser 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

DanielRosenwasser commented Jun 17, 2017

@QuantumInformation

This comment has been minimized.

Copy link

QuantumInformation 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

justinfagnani 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

justinfagnani 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

QuantumInformation 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

QuantumInformation 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

aluanhaddad 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

AviVahl 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

cyrilletuzi 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

AviVahl 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

cyrilletuzi 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

aluanhaddad 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

AviVahl 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

QuantumInformation commented Dec 30, 2017

Any movement on this issue?

@SalathielGenese

This comment has been minimized.

Copy link

SalathielGenese 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

matthewp 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

benlesh 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

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

@AviVahl

This comment has been minimized.

Copy link

AviVahl commented Oct 29, 2018

How is this even a thing? The typescript compiler knows that the target output is a JS file. I've been browsing these threads for 15 minutes and I still don't understand why it omits the extension.

There are common use cases, which people use, where the lack of extension allows for a more flexible infrastructure. Node require hook and webpack loader are two such cases.

@QuantumInformation

This comment has been minimized.

Copy link

QuantumInformation commented Oct 29, 2018

How is this even a thing? The typescript compiler knows that the target output is a JS file. I've been browsing these threads for 15 minutes and I still don't understand why it omits the extension.

There are common use cases, which people use, where the lack of extension allows for a more flexible infrastructure. Node require hook and webpack loader are two such cases.

None of which the browser modules care about.

@oising

This comment has been minimized.

Copy link

oising commented Oct 29, 2018

Would it just kill the typescript team to add an opt-in flag to emit a .js extension? We do know what we're doing here, or there wouldn't be a dozen threads (both open and closed) still garnering replies and confused questions. We understand it's not a deficiency with TS, but if TS is here to solve our JS woes, then add this woe to the list please.

DISCLAIMER:

Yes, I understand this may lead to a ton of people posting issues here that "you b0rked webpack" but really, tough titties to those folks. It should be opt-in.

Btw, digging through the TypeScript source, I see importModuleSpecifierEnding -- can this be (ab)used to cause the emitter to use .js endings?

@oising

This comment has been minimized.

Copy link

oising commented Oct 29, 2018

Maybe mush this proposal with the tsconfig schema? https://github.com/domenic/package-name-maps

@fatcerberus

This comment has been minimized.

Copy link

fatcerberus commented Nov 4, 2018

At this point I would be happy if TypeScript could only automatically rewrite imports using the paths specified in tsconfig. Even without browser support that would probably solve most of the pain points for me.

@Draccoz

This comment has been minimized.

Copy link

Draccoz commented Dec 11, 2018

Any updates? Any attempts to solve this issue? We have all the major browsers supporting es modules by default, the standard for those require the extension to be there. I understand we can add it manually and tsc will understand that, but this should not be mandatory, why can't it just work?
Sorry for whining but this issue is very old now and nothing has yet been done...

@notaphplover

This comment has been minimized.

Copy link

notaphplover commented Dec 19, 2018

How is this even a thing? The typescript compiler knows that the target output is a JS file. I've been browsing these threads for 15 minutes and I still don't understand why it omits the extension.

There are common use cases, which people use, where the lack of extension allows for a more flexible infrastructure. Node require hook and webpack loader are two such cases.

Then, maybe the Typescript compiler could accept an option to add the extensions or not. Maybe it could even accept a set of regex in order to add (or not) the extension, like the include option (https://www.typescriptlang.org/docs/handbook/tsconfig-json.html).

@Draccoz

This comment has been minimized.

Copy link

Draccoz commented Dec 19, 2018

Yea, it's been proposed at least once before already, why not take that into consideration? TS already has few experimental features that might change in the future once implemented in the browser, but TS already has flags for those unstable specs. Why not just have even an experimental flag addImportsExtensions where it would just do the module += '.js', that's it! No funky logic, no extra features. It could be a different target if you prefer to keep it as a separate target. More than that, if you accept any of the above I will personally dig through tsc code and make a PR for that, just don't want to lose my time if it wont be accepted anyway.

@ajafff

This comment has been minimized.

Copy link
Contributor

ajafff commented Dec 19, 2018

You could simply use a custom transform to rewrite imports in the emitted code (adding '.js' or using the resolved path of module imports).

How to Write a TypeScript Transform
ttypescript: wrapper for tsc that applies transforms during compilation

Maybe there's already an existing transform that does what you need.

@Draccoz

This comment has been minimized.

Copy link

Draccoz commented Dec 19, 2018

I know that is possible, I've played around with compiler API quite a bit (though not as much as I'd want to). The thing is, it adds additional tool to the project and additional tools have additional maintainers which might or might not respond to issues fast enough if at all (often not by their fault, we all have lives and work). Small projects are often abandoned, and doing everything ourselves just moves the responsibility to us so instead of taking care of the business code we spend time on the tooling.
Taking the above into account, a lot of projects will not even consider that as a solution and instead add additional build steps like rollup etc, which adds additional config overhead and get us back to additional maintenance cost.
To sum up: I prefer to spend my time on a PR to the official TS, where both I and the community would benefit from it, than spend it on a custom solution that I could eventually drop (by not having time or many other reasons), or that could not be used by anybody else in the world.

@notaphplover

This comment has been minimized.

Copy link

notaphplover commented Dec 19, 2018

@ajafff It could be a solution, but I think that the main problem is the following one: the Typescript transpiler generates wrong source code in the browsers. It's good to have transformations, but I see that as a workaround. I wonder if I have to care about transpilation details and need to implement my own transform or is something that should be managed by the compiler.

Transformations are a really poweful tool, we could write an entire new transpiler as a transformation, but I think this is not the right way.

@DanielRosenwasser

This comment has been minimized.

Copy link
Member

DanielRosenwasser commented Dec 19, 2018

Yea, it's been proposed at least once before already, why not take that into consideration? TS already has few experimental features

TypeScript has exactly one experimental flag that was added 3 years ago, and it was for an ECMAScript proposal which is no longer compatible with the current proposal.

the Typescript transpiler generates wrong source code in the browsers

I understand your expectations, but given that the compiler doesn't rewrite the paths that you wrote, I'm surprised you don't consider your own code's imports being "wrong". 😉

@Draccoz

This comment has been minimized.

Copy link

Draccoz commented Dec 19, 2018

I'm sorry @DanielRosenwasser , have you heard of a thing called npm? People post their packages there so we all can use it. I understand you might think very highly of me, but sadly I am not an author of great majority of those so I cannot edit them adding the extensions.
If my projects only used my code, I'd be more thank thankful for what typescript brings, as it's truly my right hand in coding (aside of WebStorm). I am however using 3rd party packages like I think most of us and those not necessarily contain the extensions. Some projects, like rxjs, literally wait with hope that typescript will provide the way to add extensions, otherwise it would require them to change the whole build process, so we are back to spending extra time on tools instead of product.
Now please, could you just answer 3 questions?

  1. Is typescript team willing to ship this feature?
  2. If yes, would you accept a PR?
  3. If no, when are you planning on releasing this?

If the answer for the 1st question is 'no', please, close this issue declaring officially that you are NOT willing to ship this feature, don't make others wait if it's not to come.

@notaphplover

This comment has been minimized.

Copy link

notaphplover commented Dec 19, 2018

Yea, it's been proposed at least once before already, why not take that into consideration? TS already has few experimental features

TypeScript has exactly one experimental flag that was added 3 years ago, and it was for an ECMAScript proposal which is no longer compatible with the current proposal.

the Typescript transpiler generates wrong source code in the browsers

I understand your expectations, but given that the compiler doesn't rewrite the paths that you wrote, I'm surprised you don't consider your own code's imports being "wrong". 😉

It's a good point @DanielRosenwasser , I just considered my own code right because it seems to be right reading the Typescript spec (https://github.com/Microsoft/TypeScript/blob/master/doc/spec.md#11.3).

I'm pretty sure you've read the ECMAScript specs. In the spec it's not determined if a module should end with an extension or not. In the specs, the modules import are resolved using the HostResolveImportedModule algorithm, but the definition is ambiguous. The problem is not the ECMAScript spec. The problem is that browsers resolve modules as if the [ModuleRequest] as defined in the specs was a path to the resources.

Keeping this in mind, just go to the home page of the language: https://www.typescriptlang.org/.

In the footer of the page you can read the following lines:

Starts and ends with JavaScript

TypeScript starts from the same syntax and semantics that millions of JavaScript developers know today. Use existing JavaScript code, incorporate popular JavaScript libraries, and call TypeScript code from JavaScript.

TypeScript compiles to clean, simple JavaScript code which runs on any browser, in Node.js, or in any JavaScript engine that supports ECMAScript 3 (or newer).


You promise a code that runs on every browser, but it doesn't seem to be true, thats why this issue remains for more than a year.

As @Draccoz points, we just want to know what are you doing with this issue. But it's a bit frustrating to read one thing in your home page and the opposite in this issue.

@DanielRosenwasser

This comment has been minimized.

Copy link
Member

DanielRosenwasser commented Dec 19, 2018

No, we're not willing to ship anything related to this until there's at least clarity around things like ES interop in Node and a reasonable strategy for shipping transitive dependencies you'd use from npm in the first place. If you weren't using TypeScript, you'd have the same problem around dependency management and resolution, so it makes no sense for us to come up with something that the JS ecosystem at large might develop independently. But even if those issues were resolved, I can't make guarantees that we'd make any change here period.

You promise a code that runs on every browser, but it doesn't seem to be true, thats why this issue remains for more than a year.

I think this interpretation is too literal. var fs = require('fs') doesn't work when you run it in the browser, HTMLDivElement is not defined in Node, and String.prototype.startsWith doesn't work on older browsers. To solve this, people have made tools and libraries/polyfills outside of TypeScript because they apply to the broader JavaScript ecosystem.

@Draccoz

This comment has been minimized.

Copy link

Draccoz commented Dec 19, 2018

So can you please close this issue? This is a blocker for other projects that await for TS to do it or declare you are not doing it. If you can't just add a simple flag, close this issue and make others aware of your decisions.

@notaphplover

This comment has been minimized.

Copy link

notaphplover commented Dec 19, 2018

@DanielRosenwasser Thank you for your quick response, I really appreciate that. I think it's a wise choice.

I think this interpretation is too literal. var fs = require('fs') doesn't work when you run it in the browser, HTMLDivElement is not defined in Node, and String.prototype.startsWith doesn't work on older browsers. To solve this, people have made tools and libraries/polyfills outside of TypeScript because they apply to the broader JavaScript ecosystem.

Of course it is, but ¿what else could think anyone that knows nothing of Typescript? The fact that my interpretation is too literal is as true as the fact that this text can "confuse" anyone that knows nothing about Typescript 😉.

Maybe you could update your docs (https://www.typescriptlang.org/docs/handbook/modules.html) in order to reflect the actual behavior.

@DanielRosenwasser thank you again for your response. I have been waiting for something similar for a year.

@QuantumInformation

This comment has been minimized.

Copy link

QuantumInformation commented Dec 19, 2018

The reason I wanted this feature was so that I could see what sort of web app I could create without any build tools. Until then I can use that script I wrote earlier.

@MicahZoltu

This comment has been minimized.

Copy link
Contributor

MicahZoltu commented Jan 8, 2019

For people like me looking for a solution to being able to use ES modules and TypeScript in the browser today, I found https://github.com/guybedford/es-module-shims. It acts as a sort-of polyfill for package name maps while we wait for spec finalization and browser implementation. It solves @QuantumInformation's problem of wanting to not use any build tools (my problem as well) when authoring a simple web app in TypeScript (aside from the TS compiler).

import 'knockout'

export class MyViewModel {
	greeting: KnockoutObservable<string>
	target: KnockoutObservable<string>
	constructor() {
		this.greeting = ko.observable('hello')
		this.target = ko.observable('world')
	}
}
<!DOCTYPE html>
<script type='module' src='vendor/es-module-shim/es-module-shim.js'></script>
<script type='packagemap-shim'>
	{
		"packages": {
			"index": "/output/index.js",
			"knockout": "/node_modules/knockout/build/output/knockout-latest.js"
		}
	}
</script>
<main>
	<span data-bind='text: `${greeting()} ${target()}`'></span>
	<script type='module-shim'>
		import 'knockout'
		import { MyViewModel } from 'index'
		ko.applyBindings(new MyViewModel())
	</script>
</main>

In theory, once package name maps are supported by browsers you can just find/replace type='module-shim' with type='module' in your HTML files and change the packagemap script to whatever ends up being finalized for packagemap inclusion in the spec.

@weswigham

This comment has been minimized.

Copy link
Member

weswigham commented Jan 11, 2019

It's probably also worth noting that the .js extension isn't mandated by browsers or anything - you can always configure your webserver to be more unpkg-like and serve .js files from extensionless request URLs. It's all configurable on the webserver side.

@justinfagnani

This comment has been minimized.

Copy link

justinfagnani commented Jan 11, 2019

@weswigham that can be very problematic though, because tsc (and classic node module resolution) will know that ./foo and ./foo.js refer to the same file, but browsers will treat them different, even if the webserver redirects. You'd have to be sure that you refer to a file in exactly the same way at every import.

@justinfagnani

This comment has been minimized.

Copy link

justinfagnani commented Jan 11, 2019

By the way, this issue doesn't prevent TypeScript generated modules from being used in browsers today. Just always import files with the .js extension. tsc does the right thing and resolves types from the .ts file, and you get browser compatibility.

@MicahZoltu

This comment has been minimized.

Copy link
Contributor

MicahZoltu commented Jan 11, 2019

@weswigham Keep in mind that there are situations where you are serving files without having access to the webserver. GitHub Pages, IPFS, S3, etc.. With the advent of single page app frameworks, it is becoming more and more common to run "serverless" (where serverless here means without a server you control/configure serving your assets), so you can't rely on server side filters to serve apple.js when a request is made for apple.

@weswigham

This comment has been minimized.

Copy link
Member

weswigham commented Jan 11, 2019

You'd have to be sure that you refer to a file in exactly the same way at every import.

Can always make one or the other 404 depending on your authoring preference. If you used symlinks within your project they'd cause a similar issue that you'd need to have to choose how to deal with, too.

Just always import files with the .js extension

Ya, that works fine, too.

@distante

This comment has been minimized.

Copy link

distante commented Jan 11, 2019

@QuantumInformation Is your snippet used here is for open use? Can I use it in a project? 😃

@QuantumInformation

This comment has been minimized.

Copy link

QuantumInformation commented Jan 11, 2019

@distante yes you can use it

@MrAntix

This comment has been minimized.

Copy link

MrAntix commented Jan 18, 2019

FYI if you are using Jest to test and change to extensions to .js in the source - it breaks
but you can add the following to your jest config to fix that

  "jest": {
    ...
    "moduleNameMapper": {
      "(.*)\\.js": "$1"
    }
  }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment