Skip to content
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 · 41 comments
Open
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript

Comments

@niemyjski
Copy link

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
Copy link
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
Copy link
Author

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

@mhegazy mhegazy added Suggestion An idea for TypeScript Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels May 3, 2016
@Arnavion
Copy link
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
Copy link
Contributor

mhegazy commented May 3, 2016

//cc: @RyanCavanaugh

@nippur72
Copy link

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
Copy link
Author

@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
Copy link

@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
Copy link
Author

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

@unional
Copy link
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
Copy link
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
Copy link

drudru commented Feb 19, 2017

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

@frankabbruzzese
Copy link

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
Copy link

huan commented May 6, 2017

I ran into this issue today.

I really need an else block as this ;-)

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

@frankabbruzzese
Copy link

@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
Copy link

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
Copy link

huan commented May 12, 2017

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

@frankabbruzzese
Copy link

@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
Copy link

huan commented May 12, 2017

@frankabbruzzese Awesome, thank you very much!

@frankabbruzzese
Copy link

@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
Copy link

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

@phaux
Copy link

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
Copy link
Contributor

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
Copy link

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
Copy link

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
Copy link

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
Copy link
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
Copy link
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
Copy link

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
Copy link

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
Copy link
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
Copy link
Contributor

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
Copy link
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
Copy link
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
Copy link

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

@ghost
Copy link

ghost commented Jan 16, 2021

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?

@Djeisen642
Copy link

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

@KurtGokhan
Copy link

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.

@Lazerbeak12345
Copy link

Lazerbeak12345 commented Feb 20, 2022

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:

  1. The user expects the library to act like jquery does when you <script src="./path/to/jquery.js"></script> and make an appropriate namespace.
  2. The developer intends their library to do that.

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)

@axetroy
Copy link

axetroy commented May 1, 2023

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

Consider adding something directive like this, explicitly exporting to global namspace

// a.ts

// @ts-umd-global-namespace foo
export function foo () { // ☑️ fallback to globalThis.foo
  // do somethinig
}

// @ts-umd-global-namespace bar
export function bar () { // ☑️ fallback to globalThis.bar
 // do something
}

export default function DefaultIsNotAllow() {}

@Lazerbeak12345
Copy link

Lazerbeak12345 commented May 3, 2023

typically, UMD style fallbacks would be a global named after the library name. This global would be an object containing all of the exported functions. If the file needs a fallback and it's not supposed to be standalone, then it would be a sub-object underneath the globally exposed library name object. This recurses down with imports within the library.

My understanding is that this would still be using the export _ syntax in source.

Granted, misunderstandings aside, I'm not as convinced this needs to be a feature in typescript in 2023 anymore, seeing current progress in ESM. Progress that used to by halted by typescript - by not properly supporting ESM until very recently - ironically

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests