Open niemyjski opened 8 years ago
There is no specification for UMD and any interfaces with the global/window object require some specific decisions, like you would have to determine a module name for each module. What names on the global scope would you give these modules, all part of the same project?
src/index.ts
src/feature/index.ts
document.ts
If I'm outputting to a single file (like a library like jquery) it becomes very easy to say the export should be namespaced under the file name or module namespace.
https://github.com/umdjs/umd/blob/master/templates/commonjsStrictGlobal.js
We have to support a fallback to globals in our library and I've had to work around this like: https://github.com/exceptionless/Exceptionless.JavaScript/blob/master/dist/exceptionless.js#L1236-L1264
You can find a discussion on why UMD implementation does not support the global in https://github.com/Microsoft/TypeScript/pull/2605.
mainly, what is the variable name to use, and how to manage dependencies.
One possibility is to use the new export as namespace <id>
syntax added in https://github.com/Microsoft/TypeScript/pull/7264, but we will need a proposal for that.
//cc: @RyanCavanaugh
As discussed in #9678, if we use export as namespace
for setting the global name, then the export clause needs to be allowed in normal code other than declarations (.d.ts
), otherwise library authors that are deriving their declarations from the --declaration
flag have to add the clause at each recompile because it's overwritten.
So merging the two needs, my proposal is:
When --module
is umd
and there's an export as namespace
in module code, TypeScript should:
1) emit declaration files .d.ts
with the same clause export as namespace
2) provide a fallback for global namespace case.
Example: developer is writing a library "myreact" to be consumed in modules and in global:
myreact.ts
:
export function createElement() { return 42; };
export as namespace React;
Typescript output
myreact.d.ts
:
export declare function createElement() {};
export as namespace React;
myreact.js
:
(function (root, factory) {
if (typeof module === 'object' && typeof module.exports === 'object') {
var v = factory(require, exports); if (v !== undefined) module.exports = v;
}
else if (typeof define === 'function' && define.amd) {
define(["require", "exports"], factory);
}
else {
root.React = factory(require, exports);
}
})(this, function (require, exports) {
"use strict";
function createElement() {
return 42;
}
exports.createElement = createElement;
});
@nippur72 something like that could work but you also have to consider that require
and exports
won't always exist when doing a fallback to browser global and is the exact reason I had to do a hack:
https://github.com/exceptionless/Exceptionless.JavaScript/blob/master/dist/exceptionless.js#L1236-L1264 (which we may not what to do.
@niemyjski yes the missing require
is a problem. Also I don't think your hack covers all the cases, consider for example:
import _ from "lodash";
which translates into the now global code
var _ = require("lodash");
which is nothing else than
var _ = window["lodash"];
but which is also undefined
because lodash is published globally as window._
, not window.lodash
.
So, we can fake require
but still can't do nothing for modules whose global name is different than the respective module name. Is my understanding correct?
idk, it's something that has to be looked into...
I don't see why the require
would be a problem:
https://github.com/umdjs/umd/blob/master/templates/returnExports.js
By the way, I did this two years ago, maybe it can be simplified and used in the compiler.
👍 it would be good if there was a tsConfig option for this. I ran into this today.
Any news on this issue? At moment what is "bes tway" (or just an acceptanle one) to implement an UMD module with TypeScript? I mean a module that falls back on globals i needed.
I need to implement a library composed of several modules that user may select accordint to its need. I would like each module be UMD, so user may decide how and if bundling the modules it needs.
It appears that my plan at moment is very difficult to implement in TypeScript. The only way I can think of is:
The above appears to me the only way to get TRUE UMD modules from TypeScript.
Any thought about this?
Better proposals?
I ran into this issue today.
I really need an else
block as this ;-)
} else {
root.exceptionless = factory();
}
@zixia, Actually, extending to global namespace is not so simple. Popular global libraries have a one-level namespace, such as jQuery, ko, etc. However, propietary libraries usually have multi-level namespaces. Something like this: companyname.libraryname.librarymodule. So, in general, modules doesnt map easily and univocally to namespaces.
I wrote an article on how to author multi-platform (amd, commonjs, es6, global namespace) libraries with Typescript, and some simple software to process automatically all source files to get both .d.ts and compiled .js for all platforms. The article should appear in the upcoming May magazine of the DotNetCurry magazine
Hi @frankabbruzzese,
Thank you very much for replying me!
In order to make my UMD bundle work directly by <script src='...'>
(and also work with any problem with Angular/Node.js import
), I made a dirty fix yesterday:
Firstly, I had to switch to use rollup
instead of tsc --module umd
, because tsc does not compatible with Angular AOT compiler.(https://github.com/zixia/brolog/issues/52)
Secondly, I had to use global.ModuleName
instead of global.namespace.ModuleName
for my need, and remove other pollution such like add __esModule
property.
The modification is like this one:
4c4
< (factory((global.brolog = global.brolog || {})));
---
> (factory(global));
227d226
< Object.defineProperty(exports, '__esModule', { value: true });
My repo: https://github.com/zixia/brolog
I know it's not a good solution for all, but it really works very nice for my tiny module. ;-)
I'll keep trying to find a better solution to replace this method, and I'm looking forward to reading your article of compile .js for all platforms. Please post the link to this thread after it publishes, so I can read it at the first time.
Thanks!
I just found another hacky workaround for rollup at https://github.com/rollup/rollup/issues/494#issuecomment-268243574
@zixia , My article is out. It is in the May-June issue of DotNetCurry magazine. You may download it from the main page of the magazine . At the end of the aricle there is also the link to a GitHub repos containing the whole software.
@frankabbruzzese Awesome, thank you very much!
@zixia , Please notice that my proposal is a pre-processing of TypeScript sources. After having run my script you will get 3 different distributions: one for AMD+CommonJs, one for ES6, and one for globals, from an unique source. This way you may decide yourself the wrapper to put around the core code for each of the three platforms. I think this is the more general solution, sicne the library struture may be quite different in each of the three platforms.
Now my article on mult-platform support for TypeScript libraries has been published also here.
How about this solution:
Example input source/index.js
import stuff from './my-sub-module'
import _ from 'lodash'
export const theAnswer = 42
When following options are specified in tsconfig.json
, it enables code generation for global object exports:
{
"globalName": "MyModule", // global var for this module
"globalMap": { // map of modules and their global names
"lodash": "_"
}
}
Generated compiled/index.js
:
(function (root, factory) {
// stuff that already works
if (typeof module === "object" && typeof module.exports === "object") {
module.exports = factory(require, exports)
}
else if (typeof define === "function" && define.amd) {
define(["require", "exports", "./my-sub-module", "lodash"], factory)
}
// Use globals (interesting stuff starts here)
else {
// init exports object
root.MyModule = {}
// for sub-module it would be root.MyModule.MySubModule
// insert globalMap from config here
const globalMap = {…}
function require(mod) {
if (mod[0] == '.') { // if module is relative
// insert logic for resolving relative module here
// e.g. for ./foo-bar/thing it should return <current>.FooBar.Thing
return …
}
else { // if module is not relative
// do the mapping
if (mod in globalMap) mod = globalMap[mod]
// return name from global object
return root[mod]
}
}
factory(require, root.MyModule)
}
})(this, function (require, exports) {
const stuff = require("./my-sub-module") // returns root.MyModule.MySubModule
const _ = require("lodash") // returns root._
exports.theAnswer = 42 // assigns root.MyModule.theAnswer
});
It does generate a lot of overhead code for every file, but it's completely optional.
Edit: For this to work, every library imported must either be contained in one file (i.e. all the libs that use UMD with globals currently) or use the same scheme. It's a pretty big win IMO.
If you are defining a new module format, consider adding an __esModule
property with a value of true
to your output to support interop with SystemJS, Babel, and other tool chains.
TypeScript adds this when transpiling a module using import
and export
unless it contains an export assignment (export = value
).
Any progress on this? For simple using and lightweight building procedure reason, wish some config to emit export on root.
I also would like this. I'm currently achieving this by doing a similar method as @frankabbruzzese suggested, which was preprocessing the TS and generating files for global and UMD and then running tsc
on those files. It works fine, but it's a really hacky way just to get around not having window.myNamespace = myNamespace
.
This no clear reason why you'd need hacks or additional tools to accomplish such a small thing.
Why can't we have something like "moduleGlobal": "var_name"
, which would add a global variable fallback for module types like amd
, commonjs
and umd
? That makes it opt-in and doesn't interfere with existing projects that might not want it.
I really don't want hacks (or big tools like webpack) for a simple small library 😐
However, propietary libraries usually have multi-level namespaces. Something like this: companyname.libraryname.librarymodule.
Is this the only blocker here? How about allowing export as namespace A.B.C
, with the behavior similar with namespace A.B.C
?
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
define(factory);
} else if (typeof exports === 'object') {
module.exports = factory(require, exports, module);
} else {
var A = root.A;
(function (A) {
var B;
(function (B) {
B.C = factory(...);
})(B = A.B || (A.B = {}));
})(A || (A = {}));
root.A = A;
}
}(this, function(require, exports, module) {}
One more example that uses multi-level namespace (twttr.txt
): https://www.npmjs.com/package/twitter-text https://github.com/twitter/twitter-text/blob/34dc1dd9f10e9171100cdff0cb2b7a9ed9ea2bd6/js/src/index.js
IMO it's not straightforward as it uses export default
.
Is it not possible for the typescript compiler to incorporate a bundle process, either one that already exists - webpack/rollup or one that theoretically shouldn't be too difficult to implement? I mean there are solutions to this problem, namely export as CommonJS and have a bundler re-bundle as UMD. However that kind of defeats the purpose of having the UMD option in typescript.
You wouldn't really have to change anything for the current UMD process, just add an extra config option so previous code is still maintained in the same way.
If the umd
module can not support global variable, why typescript call it umd
? So I suggest remove this option if it can not support.
@njleonzhang, UMD is a convention, not a standard and global modification is not entirely consistent or common in implementations. It is also a bit too late to remove it.
global modification is not entirely consistent or common in implementations
Maybe we can accept some popular behavior, for example from rollup.js . https://github.com/twitter/twitter-text/blob/master/js/rollup.config.js#L36-L37
@saschanaz I am not saying that solving the problem isn't warranted. I am just pointing out that there is no UMD "standard" to adhere to and it makes it non-sensical to suggest removing the feature as @njleonzhang suggested.
Of course once you start dealing with deep creation of the global var, you start making the emit pretty darn ugly and starts really encroaching on contravening non-goal:
- Provide an end-to-end build pipeline. Instead, make the system extensible so that external tools can use the compiler for more complex build workflows.
If you need a bundler, use a bundler.
@kitsonk,
Of course once you start dealing with deep creation of the global var, you start making the emit pretty darn ugly
Why? TS is already doing that and it isn't too ugly.
namespace A.B {
var x = 0;
}
// emits:
var A;
(function (A) {
var B;
(function (B) {
var x = 0;
})(B = A.B || (A.B = {}));
})(A || (A = {}));
If you need a bundler, use a bundler.
We don't need a bundler, instead we need a proper emission for export as namespace
.
@kitsonk Although I have some words to refute you, I shut up at this time to avoid meaningless argument in this thread. I just want typescript
to fix this, or if you really want to keep it, at least document it explicitly. The UMD, as a convention, usually supports global variable. current situation really makes users confused.
Hello, any news on this? I am writing an umd module but i dont want a namespace. e.g.
export * from 'Person';
export * from 'Student';
export as no namespace;
that way when i reference using global i can just call
new Person();
new Student();
is there a way to do that?
Wow, just ran into this today. It's 2021. This started in 2016. tsc
is the easiest way to compile, but I can't use it. All I want is a simple application that runs in the browser without having to load something else. 😬
I was disappointed when I saw my UMD module did not work in browser. Standard or not, UMD means something to web developers and Typescript is just misleading when it says UMD but does not output proper UMD.
A core part of "Universal" is to include browser support if no other module loader is found - note that this means it's only doing this when there is no other option - not all of the time. That is - the library would only do it if:
<script src="./path/to/jquery.js"></script>
and make an appropriate namespace.I'd say that's not ugly at all.
here is no UMD "standard" to adhere to
There is, actually a standard. It's been around since 2012. I see where you might have misunderstood, however. It was common practice for years to just copy code from their templates into your JavaScript file, and edit as needed, or just make code vaguely inspired off of the last time one saw a UMD module that does more-or-less the same thing - but perhaps not as good. Simply stated, there is no standard tooling but there is a standard - even if it isn't followed 100% of the time.
Or, an alternative idea that's been bouncing around my head: in the documentation for the "umd" option it could explain that it doesn't have a global namespace fallback, and one would have to post-process the compiled typescript to add that. (No link to specific approach would be needed - but it would be nice to see at least one, like the ts->es6->rollup->umd one I've adopted)
fallback will pollutes the global namespace. for example
// a.ts
export function foo() {
console.log('foo a.ts')
}
// b.ts
export function foo() {
console.log('foo b.ts')
}
These two files will fallback to the global namespace and conflict, This produces some unexpected behavior.
But sometimes a simple function is needed, without any module loader or bundler, it can be run directly from the Githubissues.
Most umd patterns have a third fallback that allows exporting to the window.namesapace = export; As such the current umd module export is pretty broken when a huge number of users / library developers need to support all three.