Repository navigation
Proposal: Bundling TS modules for consumption and execution #4434
Description
Relates to #3089, #2743, and #17.
Goals
- Bundle js emit for TS projects while preserving internal modular structure (complete with ES6 semantics and despite being concatenated into a single file), but not exposing it publicly and wrapping the chosen entrypoint with a loader for the desired external module system.
Proposal
When all of --bundle, --module and --out are specified, the TS compiler should emit a single amalgamated js file as output. --bundle is a new compiler flag, indicating that the modules used should be concatenated into the output file and loaded with a microloader at runtime. The semantics of this microloader should preserve ES6 module semantics, but may not support TS-specific ES6 module semantics, such as import a = require or exports =. This is a major point of discussion, as to if these TS-specific syntax extensions are worth continuing to support with new language features. Additionally, this should wrap the result file with --module-specific declaration wrappers, if need be.
Supporting ES6 semantics is some effort - some rewriting/moving and declaration hoisting needs to be done to support circular references between internal modules.
Most of the effort involved here involves the design of the microloader and emit for said microloader - since this is compile time, and we don't want to register any new external dependencies at runtime (with this loader), I expect we can make some optimizations compared to, say, using an internal copy of systemjs.
Additionally, we need to decide if we want to make further optimizations based on the chosen --module - if the system chosen supports asynchronous module resolution (so not commonjs), then we can potentially use a more lazy, async dependency resolver internally, too.
So, to give an example of the emit (let's say we use a system-like thing internally and are emitting a system module):
Given:
a.ts:
export * from './b';
export * from './c';b.ts:
export interface Foo {}
export class Bar {
constructor() {
console.log('');
}
do(): Foo { throw new Error('Not implemented.'); }
}c.ts:
export class Baz {}
export interface Foo {}Hypothetically, with --out appLib.js, --bundle a.ts and --module system arguments, we should get something like
appLib.js:
System.register([], function (exports_1) {
var __system = /* loader code */;
//a.ts
__system.register('./a', ['./b', './c'], function(exports_1) {
function exportStar_1(m) {
var exports = {};
for(var n in m) {
if (n !== "default") exports[n] = m[n];
}
exports_1(exports);
}
return {
setters:[
function (_b_1) {
exportStar_1(_b_1);
},
function (_c_1) {
exportStar_1(_c_1);
}],
execute: function() {
}
}
});
//b.ts
__system.register('./b', [], function(exports_1) {
var Bar;
return {
setters:[],
execute: function() {
Bar = (function () {
function Bar() {
console.log('');
}
Bar.prototype.do = function () { throw new Error('Not implemented.'); };
return Bar;
})();
exports_1("Bar", Bar);
}
}
});
//c.ts
__system.register('./c', [], function(exports_1) {
var Baz;
return {
setters:[],
execute: function() {
Baz = (function () {
function Baz() {
}
return Baz;
})();
exports_1("Baz", Baz);
}
}
});
function exportStar_1(m) {
var exports = {};
for(var n in m) {
if (n !== "default") exports[n] = m[n];
}
exports_1(exports);
}
return {
setters: [],
execute: function() {
__system.import('./a').then(function (_pub_1) {
exportStar_1(_pub_1);
});
};
});
Activity
DanielRosenwasser commentedon Aug 27, 2015
What are the expected semantics of default exports? I believe we should error if we encounter multiple default exports when flattening into a bundle.
weswigham commentedon Aug 27, 2015
A "default" export is literally an export named default. The keyword is just sugar for that (since default's a reserved word and all). It also has some special meaning with import statements... but that's less important here, given that the common point of conflict is when re-exporting things. Reexporting multiple
defaults acts the same way exporting multipleFoo's would.mhegazy commentedon Aug 28, 2015
What about other module systems, i.e. amd/umd/commonjs would we use System as well? is there a better way to avoid perf penality on exporting an reexporting? how about using amd style IIFE?
Do you need a loader implementation? given that you know the right order, and dependency, the compiler can emit each module as an object, and pass the right objects to satisfy dependencies..
so something like:
weswigham commentedon Aug 28, 2015
ES6 Modules can have circular dependencies, so we need a loader which can handle that (a la system). Simply ordering IIFEs isn't enough to retain ES6 semantics. If
bandchave a bunch of mutually recursive functions (and they're allowed to), andbis ordered first, it may execute a function which calls intocwhose definition isn't on the import object we've passed tobyet. The resolution to this is to hoist exported declarations out of the late execution block into an initialization area just like system.I'm not saying we should use system internally (we shouldn't) but our loader/rewriter (probably more of a rewriter with helper functions) is going to do a lot of the same things. Plus we don't need to support async operations.
mhegazy commentedon Aug 28, 2015
for that, you can do the same rewrite done today for loader, and execute all functions at the end. no need for a real loader though.
kitsonk commentedon Aug 28, 2015
I might be wrong, but circular dependencies and IIFE debates are what have made it so we have ES6 modules, but no ES6 loader... 😉 The "rewriter" will have to do some sort of introspection of the modules to resolve circular dependencies to make sure you get the order and instantiation of the modules correct.
For reference, in AMD land, there is r.js, Dojo Builder and CramJS/RaveJS.
Also most of the AMD builders allow bundling of other resources as well (like text and JSON). I don't know if that is a consideration for this "bundler".
weswigham commentedon Aug 31, 2015
Mohamed Hegazy (@mhegazy) We may still need some semblance of a loader if we want to support having a loader within the library at runtime so it can dynamically include dependencies - but it was removed in draft rev 28 and moved to an external spec so I'm not sure how much we support it?
We could have problems where users expect to be able to dynamically include their code with
Systemand then not be able to if we actually use an internal loader (or no loader)... mmm... Maybe this is too much of a corner case to worry about? Or should it be supported? Withcommonjsmodules dynamic loading was fairly common? To support this we'd actually have to teach theSystem(or internal module loader with all the semantics of a System loader) how to load the concatenated internal modules dynamically.mhegazy commentedon Aug 31, 2015
i am not sure i understand what you mean here.. can you elaborate.
weswigham commentedon Aug 31, 2015
Something fairly common in node/commonjs scenarios is the
requireDirfunction - a function which searches a directory for all JS files andrequire's them all (usually for side effects). This is a dynamic module load (sincerequireisn't resolved statically, it could always be dynamic) - it is impossible for us to know what could be included at runtime in the non-static cases and inline references to the right dependency - we simply don't know what that dependency is yet. (In a browser, it's more likely that they're lazy-loading less needed modules for performance, rather than trying to blanket include a directory, but the dynamic nature of the import is still there.) In ES6 module-land, dynamic module loads are analogously done viaSystem.import. (Versus theimportkeyword which is for static imports only.)These dynamic loads pose a problem for bunding modules internally. What if we bundle the modules, but then the consumer of those modules loads them lazily via
System.importrather than statically viaimport? We need to teach the availableSystemhow to import those internal modules.mhegazy commentedon Sep 1, 2015
but if i understand correctly, these all will be issues if we do provide a module loader implementation, if we do not, then these should just work. correct?
weswigham commentedon Sep 1, 2015
No, if we do not then we'll have serious problems with people having modules in their bundle that they can't access in the way they'd expect to (in the above dynamic ways, anyway). It's why we'd need an internal loader/module lookup/whathaveyou.
kitsonk commentedon Sep 1, 2015
Yes, lots of people have challenges understanding how CommonJS modules actually gets resolved. There is a lot of "magic". Many developers forget that when you move from the server side, where you have access to a file system and what is trivial resolution "magic" becomes wholly impossible on the client side. It is one of the many things which make people not like AMD, but I argue a lot of the "magic" we suffer to make lives easier for devs disadvantages the consumers of our solutions and actually we should bear a bit more of the burden.
I would be very cautious about introducing too much "magic" into module resolution. If there is to be any "magic" it is a configuration object which is utilised by the loader to map the final resolution. This of course can then be externalised our built in. Extending
tsconfig.jsonmight be the right way of expressing this.weswigham commentedon Sep 1, 2015
Just for clarification's sake, this is the essence of the troublesome dynamic case I'm considering:
main.ts:ui.ts:editor_lazy.ts:graph_lazy.ts:This is (roughly, their injection framework's way more complex and they use script tags and global namespacing rather than modules) how lazy UI elements get loaded in the chrome devtools.
Supporting bundling those lazy components into the bundle is troublesome - we can put the code into the bundle, but we need to teach the
System.importcall how to find it, either by replacing it with out own internal loader (so we don't expose the internal modules), or by exposing those modules to the global loader.We also can't eliminate the possibility that either
editor_lazyorgraph_lazyis loaded at runtime, despite them never being used in a static import.weswigham commentedon Sep 1, 2015
A reference for this feature:
systemjsactually has a bundler. For the 'self-extracting' bundle, it adds roughly 440 lines of 'microloader' code to the bundle. It can bundle TS output. It doesn't handle dynamic imports in the bundle, though. (So you get imports which fail after bundling)SetTrend commentedon Jan 12, 2016
May I add my two cents (coming from #6419): Why is a call to
System.import()done here anyway? Wouldn't that create a dependency toSystemJS?I would think that
ui.tsresembled something like:Then
TSCcan still decide whether the referenced component is included with the bundle (= no lazy loading necessary) or whether a--modulespecific loader may be invoked.It would be necessary for
editor_lazy.tsandgraph_lazy.tsto export the same object names, of course, to be found by the sameimport.weswigham commentedon Jan 12, 2016
import component from componentType+"_lazy.js";isn't valid ES6. In ES6, the spec says there is a module loader (as yet officially unspecified), and that module loader can handle dynamic imports such as this. (The ES6 spec just lists the semantics and syntax of static imports and modules in general)Systemisn't a reference tosystemjs, but rather the name of the global the module loader is supposed to be on, according to the current proposed spec.systemjsis simply based ofes6-module-loaderwhich is meant to be a polyfill for this spec, so the terms appear conflated, assystemjsprovides aSystem.My original point was the regardless of what module system the user is compiling to, for completenesses' sake, we should provide an implementation of a canonical
Systemwhich users can use for intra-bundle dynamic imports (whose registry isn't exposed to the external loader). My prototype implementation for this was actuallycommonjsbased in structure, and the provided loader tried its best to follow commonjs semantics, but, naturally, since the source code is ES6, this leads to some expectations (like those around circular imports) getting neglected.hackwaly commentedon Jan 13, 2016
Webpack seems done this well.
https://github.com/webpack/webpack/tree/master/examples/harmony
SetTrend commentedon Jan 13, 2016
Fully agree. As Mohamed Hegazy (@mhegazy) and Wesley Wigham (@weswigham) (and I) are argumenting pro - I'm curious: is there a roadmap for getting this implemented or is there perhaps a reason for not implementing it?
I'd like to add my personal point of view to:
TypeScript is not ES6. As long as there is no intention for having TypeScript sink into oblivion by becoming ES7 = TypeScript, I believe TypeScript should keep syntax peculiarities by its own.
I strongly believe that two different TypeScript constructs performing the same action should not be introduced. I'd prefer the team would either decide for using the
import * fromor theSystem.import()syntax - but not both of them with a non-congruent feature set.RyanCavanaugh commentedon Jun 23, 2021
This scenario seems well-served by many good external bundlers today; we don't need to rewrite esbuild