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

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

About this issue

  • Original URL
  • State: open
  • Created 8 years ago
  • Reactions: 87
  • Comments: 41 (13 by maintainers)

Commits related to this issue

Most upvoted comments

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

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.

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

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?

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.

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 ran into this issue today.

I really need an else block as this 😉

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

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.

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.

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

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.

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

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

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

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

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 😐

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