microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
99.85k stars 12.36k forks source link

TypeScript and <script type="module"></script> #13422

Closed cyrilletuzi closed 5 years ago

cyrilletuzi commented 7 years ago

A first implementation of <script type="module"><script> just landed in Safari Technology Preview 21, so I tried to use it on a TypeScript / Angular project, to finally get rid of system.js.

First issue, resolved but unconvenient : ES6 modules paths must include the '.js' extension. Fortunately, TS 2.0+ knows how to resolve import { Something } from './test.js' into import { Something } from './test.ts' while in dev. But it's a huge change to common practices and a lot of refactoring : official TypeScript docs, Angular, RxJS and so on have all encouraged until know to use the no extension form (import { Something } from './test').

Second issue, unresolved : TS provides many options to tell the compiler how to resolve paths like import { Component } from '@angular/core'. But it never transforms the imported path in the compiled files. '@angular/core' will stay '@angular/core' in the transpiled files. After investigation, I've read in many issues it is by design.

It's OK with loaders like system.js and others, which provide similar path mapping options. But with <script type="module"><script>, as far as I know, there is no configuration, so import { Component } from '@angular/core' will always fail.

I am missing something, or does that really mean that we won't be able to use the native loader with TypeScript projects ?

If I'm right, is it possible to add an option to force path transformation in compiled files ?

aluanhaddad commented 7 years ago

Just a comment. It seems ironic that you wish to

to finally get rid of system.js

When it is about the only option comes that close to what you seem to be trying to achieve.

But it's a huge change to common practices and a lot of refactoring : official TypeScript docs, Angular, RxJS and so on have all encouraged until know to use the no extension form (import { Something } from './test').

Personally, I think extensionless imports are far better, they make your code far more portable.

I suspect the native loader will eventually support configuration. Of course the specification is in major flux, but there is likely to be a configurable API to perform abstracted name resolution for imports like

import {Component} from '@angular/core';

because it is clearly needed and because the issue has been discussed at least to some extent. I've heard discussion of how to resolve

import $ from 'jquery';

You may find this repository interesting https://github.com/whatwg/loader/

If I'm right, is it possible to add an option to force path transformation in compiled files ?

No this is not possible at present.

cyrilletuzi commented 7 years ago

I don't see what's ironic in wanting to depend on native and standard features, instead of librairies...

aluanhaddad commented 7 years ago

Sorry I am a dyed in the wool SystemJS user (actually I'm not sorry 😜). But the whole point of SystemJS is to get out of the way, when native loaders are available, but until then polyfill the loader specification (and provide other features such as CommonJS interop). In other words it has built in obsolescence, by design, so it does not interfere with the future it seeks to enable today.

Seriously SystemJS is great. Not every framework uses it well, for example Angular 2, made poor use of it and is now removing it. Look at Aurelia as an example framework that, while not requiring SystemJS, uses it by default and plays to its strengths showing how elegant it can be.

cyrilletuzi commented 7 years ago

It's not a charge against SystemJS. Yes, it is a nice tool. But as you said it very well, it was designed as a temporary tool until the native loader is finally here.

So now the central part of it is there (<script type="module"></script>), but unusable in a real case : I can't see the point if we can just load a few local scripts, and no librairies. So I'm confused.

Hopefully configuration is coming. Or hopefully TypeScript will manage it.

aluanhaddad commented 7 years ago

Check this out. https://github.com/guybedford/isomorphic-browser-modules

Avol-V commented 7 years ago

Personally, I think extensionless imports are far better, they make your code far more portable.

SystemJS didn't recommend to use default extension as well.

mhegazy commented 7 years ago

i am not sure I see why this is a TS issue. If you put the full file path in the module, it should work. Or putting it diffrentelly, how could you do the same with a plain .js file with the typescript compiler completely out of the picture?

Avol-V commented 7 years ago

Whether TypeScript transforms paths or not, it could be an option to add extension to the names of modules (or it could convert .ts to .js).

mhegazy commented 7 years ago

import .. from "./foo.ts" is not allowed so it is either "./foo.js" or just "./foo". I would recommend using consistent extension and always specify .js.

cyrilletuzi commented 7 years ago

i am not sure I see why this is a TS issue. If you put the full file path in the module, it should work

As far as I know, TypeScript doesn't support absolute paths. So no, it doesn't work.

After a deeper thinking, I think the current behavior is really not normal. As a transpiler, TS should produce standard and ready-to-work JavaScript. TS also choose the very good design option to be "just" enhanced JS, ie. backward compatible with normal JS.

import { Something } from './something' is clearly not standard JS.

mhegazy commented 7 years ago

As far as I know, TypeScript doesn't support absolute paths. So no, it doesn't work.

what do you mean absolute path? can you share an example of an app with a module that is absolute? a CDN path should work with a path mapping entry.

import { Something } from './something' is clearly not standard JS.

We do not know what node will do yet. but if node considers this invalid filename, the compiler can flag it as an error. either ways, you should just write:

import { Something } from './something.js'
cyrilletuzi commented 7 years ago

We do not know what node will do yet. but if node considers this invalid filename

It's already an invalid filename for browsers, which is the main use case of TypeScript.

you should just write: import { Something } from './something.js'

I agree, that's what I do now to be future proof. Problem is, like I said on first message, that it's a huge change to common practices and a lot of refactoring : official TypeScript docs, Angular, RxJS and so on have all encouraged until know to use the no extension form (import { Something } from './test').

For absolute paths, I mean a code like this :

import { Component } from '/node_modules/@angular/core/index.js';

Otherwise, I need to do things like this :

import { Component } from '../../../node_modules/@angular/core/index.js';

First, it's a mess, second, it doesn't work if outDir is not at the same path level.

cyrilletuzi commented 7 years ago

you should just write: import { Something } from './something.js'

And most importantly, I can refactor my code, but I can't refactor librairies I use...

mhegazy commented 7 years ago

It's already an invalid filename for browsers, which is the main use case of TypeScript.

No it is not if you are using requireJS, Browserify, Webpack, SystemJs, etc.. i.e. any thing other than a browser that supports native ES6 modules, and as i far as i know, there are not many of these around today.

mhegazy commented 7 years ago

For absolute paths, I mean a code like this :

The loader spec does not really talk about how these absolute paths are allowed. When we have a clear description of that, allowing such should not be hard.

cyrilletuzi commented 7 years ago

there are not many of these around today

Implementations have started, it is now in Safari Technology Preview, in Edge Preview and it's coming soon in Chrome Canary. That was the starting point of this issue. TS is supposed to produce standard JS, not JS that need specific tools. Maybe it's too soon to decide, but for now common usage of TypeScript is incompatible with native JS.

cyrilletuzi commented 7 years ago

I don't understand why it's so difficult for this issue to be acknowledged.

TypeScript is supposed to be JS compatible, and it is also a transpiler. The point of a transpiler is to produce native JS. And it's not right anymore with <script type="module"></script>, so it's a serious issue.

OK, maybe there are still some points to be clarified (loader spec and Node choices are not final yet), but if navigators have started implementations (even Safari ^^), it's because they think the spec is now stable enough.

So now that it's possible in some beta versions to use <script type="module"></script>, it's impossible to make it work with a TypeScript project, like an Angular app, because :

And even if native loaders just started to appear, ES6 import is here for some time now, and it was always clear that the .js extension is required. Maybe I've missed something, but I've seen no sign that it could become optional.

That's precisely why tools like Systemjs offer a defaultJSExtension option from the beginning : because the .js extension has always been required. In the last Systemjs version (0.20) which aligns to the last loader spec, the global defaultJSExtension option has even been removed. Now you can just use it by package, meaning that you shouldn't rely on it, but it's just here to support compatibility with some packages that don't follow the standard, like TypeScript projects.

mhegazy commented 7 years ago

absolute paths are required (I can use them in my code, but then dev tools like Intellisense don't work anymore as TS doesn't support them) ;

this we can solve. and probably should. possibly with a flag to consider the / as baseUrl

.js extension are required (I can use them in my code, but I can't rewrite Angular files where there is no .js extension anywhere, like in every current TypeScript projects as it was not the common usage, so all imports inside Angular fail).

I would say this is an issue with the Angular code base, and not TS. there are going to be dependencies that do not use full URI, the TS compiler can not go touch all your node_modules to make them ES6 ready.

Avol-V commented 7 years ago

I would say this is an issue with the Angular code base, and not TS.

But you should fix TypeScript documentation — currently there is no .js extension in examples of using modules.

And, for strict code convention, it can be an option of TypeScript to require .js extension in modules.

cyrilletuzi commented 7 years ago

I would say this is an issue with the Angular code base, and not TS.

It's not an Angular issue. It's a possibility, and the current common usage in all TypeScript projects, not just Angular. It's even the indicated way in official TypeScript documentation. And as valid TypeScript which is here to stay for compatibility reasons (next point), it should be managed and transpiled to valid JavaScript.

Of course TypeScript won't rewrite existing projects and files in my node_modules. But when I transpile my project, all my final transpiled files (my own and the ones imported from librairies) must be ES6 compliant.

And for current librairies to become ES6 compliant and ES6 ready-to-use, TypeScript must start to transpile to valid JS, which is not currently possible.

it can be an option of TypeScript to require .js extension in modules.

It won't happen as it would break compatibility. '.js' extension works only since TypeScript 2. As we can't revert things now, transpilation to valid JS must be managed.

Avol-V commented 7 years ago

It won't happen as it would break compatibility. '.js' extension works only since TypeScript 2. As we can't revert things now, transpilation to valid JS must be managed.

It's true that we have a problem with compatibility and I think it can be an option to choose backward-compatible mode (with adding .js) and strict mode (with requiring .js).

Why we need that strict mode? The main idea of TypeScript is to be more strict than JavaScript, but currently in modules JavaScript is more strict than TypeScript. TypeScript should be as strict as JavaScript but with additional restrictions.

cyrilletuzi commented 7 years ago

Now in Firefox Nightly https://twitter.com/evilpies/status/829604616216125442

Meaning all major browsers are now implementing this. I think it's time for TypeScript to be ready.

cyrilletuzi commented 7 years ago

Another point : current TypeScript loaders (ts-loader, awesome-ts-loader and @ngtools/webpack) for webpack only work with extension-less imports. So if I need webpack for production, I won't be able to use <script type="module"> in development.

mjackson commented 7 years ago

@cyrilletuzi Where did you read that support for <script type="module"> is coming to Chrome Canary?

Edit: Nevermind, found it https://www.chromestatus.com/feature/5365692190687232

aluanhaddad commented 7 years ago

@Avol-V

@aluanhaddad wrote: Personally, I think extensionless imports are far better, they make your code far more portable.

SystemJS didn't recommend to use default extension as well.

That's true, but only for JavaScript code. SystemJS TypeScript transpilation doesn't work with a .js extension because there's no file on disk with that name.

quantuminformation commented 7 years ago

Related to https://github.com/Microsoft/TypeScript/issues/16577

quantuminformation commented 7 years ago

SystemJS has become irrelevant to me now Chrome 60 supports ES6 modules.

tolu commented 6 years ago

@cyrilletuzi thanks for pushing for this feature! I have the exact same issue that unfortunately prevents me from using typescript.

I'm working on a PWA that uses native JS modules for browsers that handle them and a fallback bundle for everyone else. To accomplish this I run tsc on my /src folder and output to /js with target esnext.
The bundle is created with webpack with ts-loader using target es2016.

That means that the browser fails to load imported modules if the extension is missing since my server can't know that a specific route should resolve as a *.js file. This can of course be solved with a specific server setup but I want this to be a static website that can run on GitHub pages and that's where I run into the issue of dropping TypeScript.

This is my (simplified) setup:

index.html

        <!-- html, head, body etc... -->
        <script type="module" src="dist/main.js"></script>
        <script nomodule src="dist/bundle.js"></script>
    </body>
</html>

src/main.ts

// different loading strategies and why they fail

import dep from './dep'; // network request fails if ".js" is not specified

import dep from './dep.ts'; // transpile fails if ".ts" is specified

import dep from './dep.js'; // bundle fails is ".js" is specified

If I'm not missing some simple way of solving this (please let me know!) I would think that this could be common use case that others will run into when building and playing with the newest browser and javascript features.

ref https://github.com/Microsoft/TypeScript/issues/11901#issuecomment-323570882

DanielRosenwasser commented 6 years ago

@tolu if you're using ts-loader, have you tried using the appendTsSuffixTo option?

For example:

{ test: /\.ts$/, loader: 'ts-loader', options: { appendTsSuffixTo: [/\.js$/] } }

As a heads up, I haven't tried it myself for .js files.

tolu commented 6 years ago

thanks for the suggestion @DanielRosenwasser, I'll look into that!

xyzdata commented 6 years ago

https://jakearchibald.com/2017/es-modules-in-browsers/

https://html.spec.whatwg.org/multipage/scripting.html#the-script-element

https://blog.whatwg.org/js-modules

https://matthewphillips.info/posts/loading-app-with-script-module

<script type="module">

// utils.js
export function addTextToBody(text) {
    const div = document.createElement('div');
    div.textContent = text;
    document.body.appendChild(div);
}

<script type="module">
    import {addTextToBody} from './utils.js';
    addTextToBody('Modules are pretty cool.');
</script>

dynamic module imports

http://2ality.com/2017/01/import-operator.html#async-functions-and-import

raould commented 6 years ago

+1 is there anywhere a concise, full, complete, straight-forward answer that tells me how to get relative TS imports working, with FF 60+ imports?

I was really excited to have a cleaner slate with respect to modules! I can never suppress my distaste long enough to grok anything about all the different packagers. Being able to have FF 60+ working with es6/es2015 modules generated from TypeScript is like the holy freaking grail of not-completely-suckful-js-ux. I feel like the world should be so excited about the possibility. I know that even in the new world there will be many configuration possibilities which means we'll likely still be stuck in crappy confusion land, but I haven't yet even seen one full example of anything working at all in this regard?

But then I can't seem to get it work. There's too much half-informed docs/blogs, and too much old and out of date docs/blogs, I have never found The Simple Answer.

Right now I have the moral equivalent of:

  1. ./scr/.ts -> tsc -> ./out/.js
  2. ./index.html with
  3. where src/Game.ts wants to import { Boot } from 'Boot' with baseUrl in tsconfig.json set to "./src"
  4. ...and it fails i think due to path issues.
notaphplover commented 5 years ago

I´m trying to create a carousel library using TypeScript. I use Browserify to generate a bundle, but I need to calculate my code coverage. I am using puppeteer and Jasmine to test my library, so my code coverage could be generated with a single run on puppeteer without a bundle process. But I can not trust in the Typescript compiler to generate valid es6 source code, so I have to bundle the code, generate source maps, generate the coverage results and map everything again. It could be so simple :(. It´s really frustrating guys, do you have any plans to fix this?

Sorry for my english

cyrilletuzi commented 5 years ago

I'll close this issue as the discussion has ended a long time ago.

styfle commented 5 years ago

There is a proposal about how to solve the web's "bare import specifier" problem.

https://github.com/domenic/package-name-maps

Once this standard gets sorted out, I'm sure the TS team can implement.

In the meantime, we'll have to make due with a replace all script to get modules working in the browser.

HughPH commented 5 years ago

Oh, come in. If it doesn't work out of the box, it's broken. Is it getting fixed or not?

Developers should be able to concentrate on writing awesome code, not finding workarounds for broken tooling.

trusktr commented 5 years ago

I agree, TS should support this case with a simple compiler option (f.e. maybe "module": "es2015", "moduleFileExtension": true.

In case it helps anyone, I made a simple middleware for my Node.js HTTP server to handle the cases:

const fs = require('fs')
const path = require('path')

module.exports = function(req, _res, next) {
    // If we encounter a file request in /ts, and it if the request doesn't have
    // a `.js` extension, then it's an import statement generated by TypeScript.
    // For example:
    //
    // import {foo} from './foo'
    //
    // So we must configure out static HTTP server to automatically add the
    // `.js` extension, as if we had written:
    //
    // import {foo} from './foo.js'
    //
    if (req.url.startsWith('/ts/')) { // In my case files in `/ts/` are always `.js` or `.map.js` files
        const filePath = path.resolve(process.cwd(), 'build' + req.url.replace('/', path.sep))

        fs.access(filePath, function(err) {
            if (err) {
                if (err.code === 'ENOENT') {
                    req.url += '.js'
                    console.log('Added .js extension to file:', req.url)
                } else {
                    console.error('Unable to read file at ' + req.url)
                    throw err
                }
            }

            next()
        })
    } else next()
}
mflux commented 5 years ago

Hello. It's 2019, is there a workaround for this?

Just casually writing some typescript on the weekend. Out of the box, broken, because I'm trying to use type="module" as per standard, except tsc does not fix import { Vector } from './vector'; to import { Vector } from './vector.js'; and thus breaks. Googling the issue lead me here, which says this the discussion has ended. What?

balupton commented 5 years ago

@mflux in your typescript code just append .js to the imported paths, everything will work as normal and your compiled code will be generated with imports that have .js

You can refer to https://github.com/bevry/billing for a project that does this successfully in practice

viT-1 commented 5 years ago

I don't understand, if tsc can resolve paths (even eslint can) like this

// index.ts
import SomeModule from '@common/SomeModule'

// tsconfig.json compilerOptions
...
"paths": { "@common/*": ["./common.blocks/*"] },
"outDir": "dist"

// common.blocks/SomeModule/index.ts
export { default } from './SomeModule';

// file structure
tsconfig.json
index.ts
common.blocks/SomeModule/index.ts
common.blocks/SomeModule/SomeModule.ts

and correctly transpiled, why I haven't possibility to add in tsconfig one flag to have transpiled path common.blocks/SomeModule/index.js?

kaisellgren commented 4 years ago

I just came across this myself. TSC is outputting invalid ES module code, which does not work in NodeJS v12 nor the modern web browsers.

Now that NodeJS v12 is LTS, shouldn't this be fixed? Or at the very least add a config option to produce file extensions to import paths.

one-github commented 4 years ago

Is this still an open issue in 2020? I also expect tsc to generate valid ES6 style-import statements with .js in transpilation while still sticking to extension-less imports while programming in .ts files.

viT-1 commented 4 years ago

@one-github Related to #16577, which is still open discussion

one-github commented 4 years ago

So, while we wait for Godot, here's my quick and dirty solution to this seemingly unsolvable problem: find www/js -type f -name '*.js' -print0 | xargs -0 sed -i '' -E 's/from "([^"]+)";$/from "\1.js";/g'

viT-1 commented 4 years ago

@one-github Your solution can resolve folders as /index.js links?

one-github commented 4 years ago

Probably not, but for my needs it's sufficient.

Starcounter-Jack commented 4 years ago
  1. Tsc clearly resolves the module import. Meaning it is sound using Typescript semantics.
  2. Tsc generates ES6+.
  3. No environment resolves the resulting imports, meaning tsc generated unsound code according to ES6+ semantics.

This should clearly get fixed.

Tsc should either a) provide a compilation error (tsc knows that the module it resolved to is not the module that the resulting code will resolve to); or b) generate code that resolves to the very same module that the compiler resolved to

Starcounter-Jack commented 4 years ago

I believe that the string literal syntax is fooling the compiler vendor here. The use of a string literal would normally dictate that this is a string, any string, that should be preserved during transpilation. It would not be a part of the language/compiler semantics.

In the Ecmascript language grammer this is a ModuleSpecifier. In the Ecmascript language specification, it comes with semantics. As such, the semantics should be mapped/translated as much as any other part of the two languages. That it looks like any string literal is irrelevant. It is not a value to preserve. It is a construct to be translated from Typescript semantics (i.e. in some cases, a reference to a typescript module) to Ecmascript semantics (i.e. a reference to a generated .js module).

To expain. A ModuleSpecifier is a reference to a Ecmascript module. In this case, an ES6 module that has been both generated by Typescript and referenced from Typescript. Pretend that the syntax and semantics of a ModuleSpecifier was vastly different in Typescript and Ecmascript. Pretend that it was not disguised as a string literal. Then ask your self how you should codegen from a code graph that had a reference to a module. It would then be obvious that the compiler/transpiler should not protect "the string".

The stomach feeling that to protect "the string" or the "file path" is right is a naive approach as both the the .js module and the Javascript import statement is generated by Typescript from a sound resolved semantic representation.

  1. It is easy to fix
  2. It is NOT a dirty fix
  3. Typescript should not resort to misguided gut feelings over soundness, integrity and correctness (we have Javascript semantics for that)
tobias-huebner commented 4 years ago

I cant believe this is still an issue.

when you use "baseUrl": ".", "paths": { "/*": ["./*"] },

you can do an absoulte module import like so: `import ws from "/hey/connection";

but when adding the ".js" extension, all of the sudden the compiler no longer finds the connection.ts declaration and says:

Could not find a declaration file for module '/hey/connection'. '/home/tobi/Documents/JITcom/Code/Libs/Test_Browser/hey/connection.js' implicitly has an 'any' type.

using a relative path it all works fine.

Please fix this

softwareCobbler commented 4 years ago

import foo from "./bar.js" appears to work as one would hope, when bar is a TS file in the CWD. The output is a module that will run in the browser, and all the types get checked. Is this documented anywhere?