microsoft / TypeScript

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

How to use ES6 modules on the browser? #2743

Closed tinganho closed 8 years ago

tinganho commented 9 years ago

Are there any ways to program using ES6 modules and compile everything down to one file? I guess you treat ES6 modules as external modules and therefore not doing any bundling.

I have tried using --out option to compile everything down to one file and using ES6 syntax. But so far, it seems like TypeScript doesn't support it.

I'm just curious how you expect people ot use ES6 import syntax on the browser? I was hoping something like browserify's bundling would happened.

ahejlsberg commented 9 years ago

To use ES6 modules in the browser you currently need to include a module loader such as RequireJS or SystemJS or use a bundling tool such as Browserify.

We're actively discussing implementing a simple bundling feature in the TypeScript compiler itself. Similar to --out it would allow you to bundle the output of a compilation of multiple ES6 modules into a single file with an included micro-loader that resolves references between the modules. The thinking is that you'd be able to designate one of the modules as the external interface and we'd expose only the exports of that module from the compiled bundle (of course that module could use export * to export from several of the other modules). It would be possible to package the compiled bundle as as a CommonJS or AMD module, or as a "self executing" module with no external dependencies.

tinganho commented 9 years ago

@ahejlsberg are there any rough plans on when this will be implemented?

I'm not so fond of the idea of adding a post-build using requirejs or browserify.

ahejlsberg commented 9 years ago

If we decide to do it it would probably be in the 1.6 release.

EisenbergEffect commented 9 years ago

I have a thought related to this. Just throwing this out...

For Aurelia, we are using SystemJS for loading. I'd like to keep that option at the app level, but I'm wondering if we could ship our individual libraries a little differently. Right now, one of the draw backs for module loaders is that resolving dependencies involves lookup in a registry, etc I'm wondering if the micro loading could take a slightly different approach. Could the compiler be configured to instead generate ES5 code that simply uses object references to connect modules to their imports without any sort of registry or lookup mechanism? This could improve performance, particularly around startup time.

For example, each library in Aurelia is composed of a number of modules, but really the library only exports a subset of that as its public interface. Could there be an option to compile each of these libraries to a single file, with all the internal import/export semantics being mapped to object references. Then, could it simply export the the final set of exports for use with a module loader? In other words, statically linked internally, but dynamically linked to consumers.

This is bringing more of the concept of a "library" to TypeScript as something composed of multiple ES6 modules but that is transformed into a single file with an explicit set of public exports.

csnover commented 9 years ago

I would really prefer TS team leave module bundling to the many other projects that already have done it successfully instead of repaving this particular path. AMD is the correct intermediate format for emitting directly to browsers without ES module support, as it was specifically designed for this purpose (modules in ES3/5 browsers).

@EisenbergEffect Web community already went down the module loading micro-performance rabbit hole years and years ago. Go search for some of the research that Dojo and SproutCore teams did, or if you want to experience some extreme code loading pedantry, try reading some of getify’s old blog posts. The right thing to do is to split code and only load what you need at startup, ideally avoiding execution of factory functions until they are required. The AMD format is the result of a lot of research on the best way to do modules in browsers without requiring changes to the language itself and is as ideal as you can get without violating CSP (and probably as ideal as you can get even then with modern JIT). There is no need to expend more energy on this solved problem. (Get off my lawn, etc etc. :))

EisenbergEffect commented 9 years ago

@csnover I've used AMD successfully for a number of years and done very well with that. However, it's important to note that AMD doesn't correctly represent ES6 module semantics. It is possible to write ES6 code that, when transpiled to AMD modules, will result in incorrect dependency resolution. Specifically, I'm referring to how circular references are handled. The only format I know that expresses the exact same semantics as ES6 is the system.register format.

That is a bit beside the point for what I am talking about though. I'm simply throwing out a compilation idea for libraries. Since we distribute our library as a single unit, but it's authored as many ES6 modules, it might be interesting to consider a compilation option which would intelligently combine these into a single module and perhaps use object references rather than a more costly module registry internally. There's nothing stopping the entire thing from being exported as an AMD module though, or any other format.

And I would note, I'm happy for anyone to tell me that is a bad idea. I'm just thinking out loud here...

cmichaelgraham commented 9 years ago

@csnover recently i ran into an issue bundling aurelia amd modules.

the issue was caused by three separate files A, B, and C. A imports a type from B, B imports a type from C, and C imports a type from A.

this scenario is supported just fine by jspm and systemjs ES6 modules.

i'm an old school .NET guy. it seems to me like it should be ok for this type of scenario, particularly within a repo, much like types within an assembly.

my hack solution to the problem was to move the three files into a single file, which worked fine.

it seems to me like this issue proposes to mechanize what i did manually.

does any of that make sense?

csnover commented 9 years ago

@EisenbergEffect It’s a bad idea :) Or, at least, it’s solving a problem that doesn’t exist. Hiding library internals typically causes more problems than it solves; as an end-user of a lib you can operate defensively against other things on the page ruining your dependencies, but as a lib author doing the same thing just causes a little bug in your library to be a big problem for your users since they can’t patch it by just replacing the impact API with a fixed one without copying and modifying the entire thing. I have worked on both jQuery and the Dojo Toolkit so have a lot of experience in the JS lib area to understand this problem. :)

When you use AMD format as an intermediate export format for ES modules circular references still work. You can compile to AMD format with ES modules format in TypeScript today and it works fine, I made sure of that.

cmichaelgraham commented 9 years ago

@csnover maybe i was just doing it wrong somehow. i was using r.js to bundle up the aurelia amd libraries. when the bundle loaded, it received an undefined for the circular case instead of the actual type.

http://requirejs.org/docs/api.html#circular describes that and suggests that you declare require as a dependency and use it to obtain the problem undefined type within the method.

to do that, i'd have to go back to the aurelia source and create a special case.

i'm relatively new to all of this, so i could easily be missing something... :)

tinganho commented 9 years ago

@csnover I'm not sure if it makes sense from a workflow view. Compile TS(ES6 modules) -> JS(AMD modules) and then use requirejs to bundle everything. Of course on dev mode you just add a <script> tag.

One bad thing with requirejs is that for a large project with a lot of modules. In the unbundled mode or dev mode. The requirejs script that is importing and resolving all the dependencies takes maybe over 10s to load all the modules and that being on a local computer not a remote server.

Requirejs also requires people to learn AMD and how to setup a requirejs project. This is a pain point for many devs, especially for non-experienced ones. Just to configure the requirejs config file is pain for many devs.

I guess many think browserify being the solution to these problems.

The right thing to do is to split code and only load what you need at startup, ideally avoiding execution of factory functions until they are required.

I guess with factory functions you mean function(require, exports) { ... module code ... }? Doesn't a reasonable project runs all the factory functions? Why import/load something you don't need? And if all the factory functions are being run. I don't see any big difference having them run at the beginning(browserify) as opposed to having them run spread out(requirejs).

csnover commented 9 years ago

I'm not sure if it makes sense from a workflow view. Compile TS(ES6 modules) -> JS(AMD modules) and then use requirejs to bundle everything.

Sorry, which part does not make sense?

Of course on dev mode you just add a , which is all you need to do in most cases. Inexperienced devs can use a boilerplate.

And then in app/main you need to write your requirejs configurations and figure out the initial import of your main and also call some functions or methods of the imported modules. And then in production you would need to replace the <script src="requirejs/require.js" data-main="app/main"></script> with the bundle <script src="bundle.js"></script>. Again not so trivial.

Hm, you might want to check your local server configuration, 10s is a crazy amount of time! Where I work we routinely develop apps with hundreds of modules using AMD and a full reload takes only a few seconds at worst.

In my experience a bare minimum setup and a decent amount of modules for a large app like 30 modules. Takes maybe 3-4s. You lose your programming context just waiting for these extra seconds.

Yes, a reasonable one does, but a project that compiles everything including optional functionality to one file replacing module imports with object references as described herein would not be so reasonable. :)

I'm not sure I follow this?

tinganho commented 9 years ago

@csnover I'm just curious how many extra ms do we lose just running a few extra factory functions? Isn't it negligible?

csnover commented 9 years ago

Sorry, I think we got way off track here. I do want to just respond to a couple small things:

In my experience a bare minimum setup and a decent amount of modules for a large app like 30 modules. Takes maybe 3-4s. You loose your programming context just waiting for these extra seconds.

Seriously, check your configuration, something is not right! Things should be better for you! I just ran a test from my LAN (not localhost) server with disabled cache and this is the timing result from Chrome:

screen shot 2015-04-15 at 00 47 58

101 modules, 45 stylesheets, loaded in just over 750ms.

I'm just curious how many extra ms do we lose just running a few extra factory functions? Isn't it negligible?

Yes, it is negligible. My point was that it won’t meaningfully help runtime startup performance to avoid running extra factory functions. In any case, in implementing this feature one simply shuffling your slowness around, from runtime lookups to forcing the compiler to re-emit your entire codebase, source map, etc. on every save. So, I don’t think that TS should reinvent this wheel.

csnover commented 9 years ago

@jbondc I don’t know what you are referring to or talking about.

RyanCavanaugh commented 9 years ago

Changing this to Discussion -- FYI if anyone has something concrete or hits a bug, etc, please log a separate issue as those things can get lost in these long threads.

basarat commented 9 years ago

When you use AMD format as an intermediate export format for ES modules circular references still work

@csnover I didn't test amd but for commonjs it doesn't work:

image

cmichaelgraham commented 9 years ago

in my testing, an r.js bundle of amd modules doesn't work when circular references exist.

csnover commented 9 years ago

@basarat @cmichaelgraham My understanding of ES6 modules based on my reading of the specification is that they work this same way.

Import and export declarations in ES modules are resolved to ImportEntry or ExportEntry records, which act as mediators for values within the lexical scope of the module body. They don’t hold values themselves. For import { foo } from './test1, the ImportEntry simply says “local name foo refers to export name foo on module './test1'”. For export var foo = 123, the ExportEntry simply says “export name foo refers to local name foo”. During module evaluation, the code in the module body is actually executed and local values are populated according to the normal way EcmaScript executes.

So, until the module body is evaluated and those local names are populated with values, any access of an imported binding results in undefined, just like CJS/AMD modules. The only difference is that the use of bindings allows you to get the correct value later from an import entry, once all module bodies have been evaluated, as opposed to CJS/AMD modules, where trying to do this with a var would cause the value to be fixed to undefined. The TypeScript compiler never converts ES imports to variables in the emitted JavaScript, so it works like an ES6 binding.

basarat commented 9 years ago

@csnover so circular references are not a problem OR are they still a problem?

csnover commented 9 years ago

What do you mean by “problem”?

basarat commented 9 years ago

The only difference is that the use of bindings allows you to get the correct value later from an import entry, once all module bodies have been evaluated, as opposed to CJS/AMD modules

The TypeScript compiler never converts ES imports to variables in the emitted JavaScript, so it works like an ES6 binding

^ I don't get this. I think we would still get these random undefineds (aka problem) if using JS from TS emit. But its my lack of research. So leave it for now :rose:

csnover commented 9 years ago

^ I don't get this.

ES5:

var foo = require('foo').foo;

function later() {
  foo; // undefined
}

ES6:

import { foo } from 'foo';

function later() { 
  foo; // 123
}

TS ES6 compiled to ES5:

var foo_1 = require('foo');

function later() {
  foo_1.foo; // 123
}
basarat commented 9 years ago

Thank you!

I didn't get it because we didn't have destructured imports in import/require and that just worked and the new thing here is that ES6 with destructuring preserves this later connection (which I felt like the natural thing to do and completely overlooked the innovation). I was looking for some other magic which I feel doesn't exist.

My new question:

The TypeScript compiler never converts ES imports to variables in the emitted JavaScript, so it works like an ES6 binding

With ES6 in the presence of circular references what is the expected behavior here:

import { foo } from 'foo';
console.log(foo);

undefined or 123? I am not talking about es5 transpilation, we've established that its undefined in those cases.

csnover commented 9 years ago

With ES6 in the presence of circular references what is the expected behavior here:

The ES6 spec is not the easiest thing to read but I am like 99% sure from my reading, along with just thinking logically about things, it would be undefined.

For example, think about this valid ES6 code:

var result = function () { return 123; };
export var foo = result();

In order for foo to be defined correctly the earlier var result needs to have been evaluated so it can be called. The only way that can happen is if the module body is also evaluated. So a compilation to an ES5 CJS or AMD module will work exactly the same as a real ES6 module in this context.

The only way this would not be the case in your original code example would be if the ES spec contained some exception for literal values since those could be resolved during the static analysis part, but I haven’t seen anything suggesting it is so (and it would be a very weird and confusing thing to do).

EisenbergEffect commented 9 years ago

This is off topic now but...

AMD does not support full ES6-spec circular reference behavior. This is one of the reasons that the System.register module format was invented. It is the only module format I know of that correctly matches the ES6 spec behavior. You can read a bit about that spec here: https://github.com/ModuleLoader/es6-module-loader/wiki/System.register-Explained

My team has encountered issues, at least when compiling with Babel, where the same ES6 code compiled to AMD and System.register formats will work correctly when loaded using the System.register modules, but will result in undefined symbols when loading via AMD modules.

If further clarification on System.register or ES6 module loading semantics is needed, I would refer to @guybedford

tinganho commented 9 years ago

@EisenbergEffect I'm just wondering what kind of use cases there are for defining circular references?

basarat commented 9 years ago

@tinganho some information here : http://www.2ality.com/2014/09/es6-modules-final.html

image

tinganho commented 9 years ago

Just reading the SystemJS and RequireJS docs. It seems like only value exports are not working in circular dependecies? I think one could always rewrite value export to binding exports in order to support circular references in CJS and AMD.

// a.js
import * as a from './b.js';
export var foo = 123;
console.log(a.bar);
// b.js
import * as b from './a.js';
export var bar = 123;
console.log(b.foo);

will be transpired to CommonJS:

// a.js
var a = require('./b.js');
exports.__value__foo = function() { return 123 };
console.log(a.__value__bar());
// b.js
var b = require('./a.js');
exports.__value__bar = function() { return 123 };
console.log(b.__value__foo());
csnover commented 9 years ago

@EisenbergEffect Thanks for the link. I thought maybe this would be the case while reading (given the multi-stage module evaluation process) but it seemed like such an unlikely edge case to try to allow I discounted it prematurely. I had (and continue to have) a hard time thinking of what practical value there is in making exported function and generator declarations (not expressions, not classes, not static values) available early. That’s a lot of mental and implementation complexity to carve out such a small exception, especially given if one actually calls one of these functions and it tries to reference anything through closure (other than another function or generator), that value will still be undefined so the function will fail if it is called early. (There would also seem to be an issue where if the function sets one of the export variables, that new value would be overwritten by the default export value once the actual ModuleEvaluation step was executed.)

In the case of @basarat’s example, ES6 provides no benefit. But, as it turns out, I overlooked Section 15.2.1.16.4 Step 16.a.iv (…) that says these functions and generators are evaluated early. So, you’re right, any AMD loader would need to be updated to have a two-stage module evaluation process and TypeScript would need to output a whole different structure, just to handle this one edge case.

(As an aside, while trying to figure out what the intended difference of VarScopedDeclarations vs LexicallyScopedDeclarations was supposed to be I ran across this thread. I felt pretty down on myself for missing this until I saw other more experienced people have had similar problems with the way this latest edition is written. Now I just feel annoyed that eternal confusion is what we have to look forward to :))

EisenbergEffect commented 9 years ago

@csnover I wouldn't ever feel bad personally about having trouble figuring out some of the spec. It's not the most approachable document, even in terms of technical specs. Mostly I knew these edge cases existed because I've worked with jspm for a while and had a bit of additional insight into the module loader stuff, especially system.register format. We also hit one of these edge cases in Aurelia just in the last two weeks after some refactoring. We discovered (the hard way) that we had a circular reference across three files, internal to one of our libraries. Everything worked fine with system.register format, but broke when using AMD. Since we support customers via system.js, require.js and dojo, it was a bit of a problem when we unknowingly broke our AMD customers, even when all of our tests were passing.

cmichaelgraham commented 9 years ago

@EisenbergEffect @csnover - i like where this is heading. also wanted to send some props to @basarat because the atom-typescript extension can now spot cycles like this one. i tested it and it found the aurelia cycle :+1:

basarat commented 9 years ago

circular

Thanks :rose: docs : https://github.com/TypeStrong/atom-typescript/blob/master/docs/dependency-view.md#circular

cmichaelgraham commented 9 years ago

digging into the babel compiler amd output, i now believe that it has changed its approach and doesn't pull destructured import out until it is later used, which if i understand the above, fixes the amd cycle misbehavior.

basarat commented 9 years ago

@cmichaelgraham Without looking at code and going just from your comment I am assuming that instead of

var foo_1 = require('foo');
console.log(foo_1.foo);

It does:

console.log(require('foo').foo);

If so ... it would still be undefined at the root.

If its not at the root, we don't have an undefined issue with what we have currently anyways (no need to look at babeljs solution)

cmichaelgraham commented 9 years ago

i believe that the issue has been resolved by a change to the way the babel compiler is generating amd modules. https://github.com/babel/babel/issues/30 seems related - special thanks to @colindembovsky for identifying it

paleo commented 9 years ago

i believe that the issue has been resolved by a change to the way the babel compiler is generating amd modules. babel/babel#30 seems related - special thanks to @colindembovsky for identifying it

Won't work with ES6 classes. ES6 classes are not hoisted.

cmichaelgraham commented 9 years ago

i'm a bit out of my element here, but before, babel was making a destructuring assignment at the top of the compiled module code, but now it actually uses the import variable later on, pulling out the destructuring property. does that make any sense? i think that's why the circular references work now.

daslicht commented 8 years ago

Any progress on this ? Do we really need browserify, SystemJs, jspm or Webpack to use multimple ts modules in the browser ?

DanielRosenwasser commented 8 years ago

At this point I think the fact that you can bundle your immediate compilation as a single AMD/SystemJS file should suffice. I'm going to go ahead and close this.

EisenbergEffect commented 8 years ago

@DanielRosenwasser Can you link to some docs on this?

mhegazy commented 8 years ago

Can you link to some docs on this?

Here is the feature announcement: https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript#concatenate-amd-and-system-modules-with---outfile

EisenbergEffect commented 8 years ago

Ah, thank you. Not exactly what I was hoping for unfortunately ;( I was interested in an option that would take multiple TS modules and merge them into a single TS module, aggregating all exports and de-dupinging imports.

AlexGalays commented 8 years ago

Too bad out of these 2 options one is a completely outdated format (AMD) and the other is needlessly complicated and has a big runtime cost.

Is there an issue with compiling to a CJS format with a tiny loader included? Webpack does that... But it's also 3-4 times slower to compile than just using tsc ! :(

bitjson commented 7 years ago

For anyone finding this issue in the future: I had some trouble understanding how to do this – hopefully this project will help others: es7-typescript-starter browser example

It's a sample typescript browser project importing and using an ES6 module (also exported from another typescript project with bundled type definitions), which gets tree-shaken with Rollup for the final browser bundle.

bitjson commented 7 years ago

For anyone finding this issue in the future, here's a simple example where we import an ES6 module with Typescript, and later tree-shake with Rollup:

Both projects are written in Typescript (and the dependency exports its type definitions), so the type definitions are available to the browser project. When it's built, unused functions are not included in the final bundle.

markusmauch commented 7 years ago

Would it not be possible to transpile the ES6 module syntax to internal modules (namespaces)? Here's an example with three modules (App/Lib/ModuleA.ts, App/Lib/ModuleB.ts, App/Index.ts):

App/Lib/ModuleA.ts:

export function foo() {}

App/Lib/ModuleB.ts:

import * as A from "App/Lib/ModuleA";
export function bar() { return A.foo(); }

App/Index.ts

import * as B from "App/Lib/ModuleB"
B.bar();

This could be 'transformed' into internal module (namespace) syntax:

namespace App.Lib.ModuleA
{
  export function foo() {}
}

namespace App.Lib.ModuleB
{
  export function bar() { return App.Lib.ModuleA.foo() }
}

namespace App.Index
{
  App.Lib.ModuleB.bar();
}

Resulting in this output that can be executed in the browser:

"use strict";
var App;
(function (App) {
    var Lib;
    (function (Lib) {
        var ModuleA;
        (function (ModuleA) {
            function foo() { }
            ModuleA.foo = foo;
        })(ModuleA = Lib.ModuleA || (Lib.ModuleA = {}));
    })(Lib = App.Lib || (App.Lib = {}));
})(App || (App = {}));
(function (App) {
    var Lib;
    (function (Lib) {
        var ModuleB;
        (function (ModuleB) {
            function bar() { return App.Lib.ModuleA.foo(); }
            ModuleB.bar = bar;
        })(ModuleB = Lib.ModuleB || (Lib.ModuleB = {}));
    })(Lib = App.Lib || (App.Lib = {}));
})(App || (App = {}));
(function (App) {
    var Index;
    (function (Index) {
        App.Lib.ModuleB.bar();
    })(Index = App.Index || (App.Index = {}));
})(App || (App = {}));
tolu commented 7 years ago

@bitjson thanks for the examples!

The thing I'm still missing is how to to do a combination of transpiled ts-sources using modules in browser (<script type="module" src="...">) AND a bundle fallback. I.e. how to setup ts flow so that files can be transpiled individually and bundled together.

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

Any ideas or solutions?