TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

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

Open niemyjski opened this issue 9 years ago • 41 comments
trafficstars

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

niemyjski avatar May 03 '16 13:05 niemyjski

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

kitsonk avatar May 03 '16 14:05 kitsonk

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

niemyjski avatar May 03 '16 15:05 niemyjski

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.

mhegazy avatar May 03 '16 16:05 mhegazy

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.

Arnavion avatar May 03 '16 17:05 Arnavion

//cc: @RyanCavanaugh

mhegazy avatar May 03 '16 18:05 mhegazy

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 avatar Jul 14 '16 13:07 nippur72

@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 avatar Jul 14 '16 15:07 niemyjski

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

nippur72 avatar Jul 14 '16 17:07 nippur72

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

niemyjski avatar Jul 14 '16 17:07 niemyjski

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

unional avatar Dec 21 '16 05:12 unional

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

unional avatar Dec 21 '16 05:12 unional

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

drudru avatar Feb 19 '17 04:02 drudru

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?

frankabbruzzese avatar Feb 22 '17 14:02 frankabbruzzese

I ran into this issue today.

I really need an else block as this ;-)

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

huan avatar May 06 '17 10:05 huan

@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

frankabbruzzese avatar May 06 '17 18:05 frankabbruzzese

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!

huan avatar May 07 '17 00:05 huan

I just found another hacky workaround for rollup at https://github.com/rollup/rollup/issues/494#issuecomment-268243574

huan avatar May 12 '17 08:05 huan

@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 avatar May 12 '17 09:05 frankabbruzzese

@frankabbruzzese Awesome, thank you very much!

huan avatar May 12 '17 12:05 huan

@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 avatar May 12 '17 14:05 frankabbruzzese

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

frankabbruzzese avatar Jun 01 '17 17:06 frankabbruzzese

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.

phaux avatar Aug 20 '17 11:08 phaux

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

aluanhaddad avatar Aug 20 '17 12:08 aluanhaddad

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

zheeeng avatar Jan 27 '18 16:01 zheeeng

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.

JasonKaz avatar Feb 08 '18 19:02 JasonKaz

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 😐

mindplay-dk avatar Mar 15 '18 10:03 mindplay-dk

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 avatar May 13 '18 13:05 saschanaz

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.

saschanaz avatar May 27 '18 02:05 saschanaz

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.

outofthisworld avatar Jun 13 '18 23:06 outofthisworld

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 avatar Aug 09 '18 09:08 njleonzhang