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

umd module compiler option doesn't have a fallback for global namespace. #8436

Open
niemyjski opened this Issue May 3, 2016 · 35 comments

Comments

Projects
None yet
@niemyjski

niemyjski commented May 3, 2016

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.

(function(root, factory) {
  if (typeof define === 'function' && define.amd) {
    define(factory);
  } else if (typeof exports === 'object') {
    module.exports = factory(require, exports, module);
  } else {
    root.exceptionless = factory();
  }
}(this, function(require, exports, module) {}
@kitsonk

This comment has been minimized.

Contributor

kitsonk commented May 3, 2016

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
@niemyjski

This comment has been minimized.

niemyjski commented May 3, 2016

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

@mhegazy

This comment has been minimized.

Contributor

mhegazy commented May 3, 2016

You can find a discussion on why UMD implementation does not support the global in #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 #7264, but we will need a proposal for that.

@Arnavion

This comment has been minimized.

Contributor

Arnavion commented May 3, 2016

There is precedent [1] [2] for compilers that output UMD to take a separate parameter for the global name. But I guess since you already have export as namespace you might as well use it.

@mhegazy

This comment has been minimized.

Contributor

mhegazy commented May 3, 2016

@nippur72

This comment has been minimized.

nippur72 commented Jul 14, 2016

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;
});
@niemyjski

This comment has been minimized.

niemyjski commented Jul 14, 2016

@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.

@nippur72

This comment has been minimized.

nippur72 commented Jul 14, 2016

@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?

@niemyjski

This comment has been minimized.

niemyjski commented Jul 14, 2016

idk, it's something that has to be looked into...

@unional

This comment has been minimized.

Contributor

unional commented Dec 21, 2016

I don't see why the require would be a problem:
https://github.com/umdjs/umd/blob/master/templates/returnExports.js

@unional

This comment has been minimized.

Contributor

unional commented Dec 21, 2016

By the way, I did this two years ago, maybe it can be simplified and used in the compiler.

@drudru

This comment has been minimized.

drudru commented Feb 19, 2017

👍 it would be good if there was a tsConfig option for this. I ran into this today.

@frankabbruzzese

This comment has been minimized.

frankabbruzzese commented Feb 22, 2017

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:

  1. processing each .js produced by the TypeScript compiler with a bundler to transform it into an TRUE UMD (one that falls back on globals if needed)
  2. After TypeScript compilation, process each d.ts ouput file to "add an export as namespace ..." statement.

The above appears to me the only way to get TRUE UMD modules from TypeScript.

Any thought about this?

Better proposals?

@huan

This comment has been minimized.

huan commented May 6, 2017

I ran into this issue today.

I really need an else block as this ;-)

  } else {
    root.exceptionless = factory();
  }
@frankabbruzzese

This comment has been minimized.

frankabbruzzese commented May 6, 2017

@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

@huan

This comment has been minimized.

huan commented May 7, 2017

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.(huan/brolog#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!

@huan

This comment has been minimized.

huan commented May 12, 2017

I just found another hacky workaround for rollup at rollup/rollup#494 (comment)

@frankabbruzzese

This comment has been minimized.

frankabbruzzese commented May 12, 2017

@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.

@huan

This comment has been minimized.

huan commented May 12, 2017

@frankabbruzzese Awesome, thank you very much!

@frankabbruzzese

This comment has been minimized.

frankabbruzzese commented May 12, 2017

@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.

@frankabbruzzese

This comment has been minimized.

frankabbruzzese commented Jun 1, 2017

Now my article on mult-platform support for TypeScript libraries has been published also here.

@phaux

This comment has been minimized.

phaux commented Aug 20, 2017

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.

@aluanhaddad

This comment has been minimized.

Contributor

aluanhaddad commented Aug 20, 2017

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

@zheeeng

This comment has been minimized.

zheeeng commented Jan 27, 2018

Any progress on this? For simple using and lightweight building procedure reason, wish some config to emit export on root.

@JasonKaz

This comment has been minimized.

JasonKaz commented Feb 8, 2018

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.

@mindplay-dk

This comment has been minimized.

mindplay-dk commented Mar 15, 2018

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 😐

@saschanaz

This comment has been minimized.

Contributor

saschanaz commented May 13, 2018

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) {}
@saschanaz

This comment has been minimized.

Contributor

saschanaz commented May 27, 2018

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.

@outofthisworld

This comment has been minimized.

outofthisworld commented Jun 13, 2018

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.

@njleonzhang

This comment has been minimized.

njleonzhang commented Aug 9, 2018

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.

@kitsonk

This comment has been minimized.

Contributor

kitsonk commented Aug 9, 2018

@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.

@saschanaz

This comment has been minimized.

Contributor

saschanaz commented Aug 9, 2018

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

@kitsonk

This comment has been minimized.

Contributor

kitsonk commented Aug 9, 2018

@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:

  1. 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.

@saschanaz

This comment has been minimized.

Contributor

saschanaz commented Aug 10, 2018

@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.

@njleonzhang

This comment has been minimized.

njleonzhang commented Aug 10, 2018

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment