heya-globalize

Make a browser version of JS files using globals from a Heya-style UMD, or a simple AMD.

Usage no npm install needed!

<script type="module">
  import heyaGlobalize from 'https://cdn.skypack.dev/heya-globalize';
</script>

README

heya-globalize

Build status Dependencies NPM version

This utility is a simple source transformer for JavaScript modules written using either a Heya-style UMD prologue, or a simple AMD prologue. It can produce JavaScript modules, which use browser globals as their dependences and exports. Such modules can be directly including into HTML with <script>, or concatenated and minified by a builder of your choice. Additionally it can convert to AMD, CommonJS, or ES6 modules.

If your project uses grunt, consider using grunt-transform-amd, which is based on this project.

Install

npm install --save-dev heya-globalize

Usage

For simplicity heya-globalize does not install a global command opting to be called directly:

node node_modules/heya-globalize/index.js
node node_modules/heya-globalize/index.js --amd
node node_modules/heya-globalize/index.js --cjs
node node_modules/heya-globalize/index.js --es6

This command will convert all files that is detected as Heya-style UMD or simple AMD to globals copying them to a folder of your choice (dist by default). Alternative versions with an explicit option will generate AMD, CommonJS, or ES6 modules. Additional options allow to specify a source directory for files to be copied, and a target directory for transformed files.

Full list of available options:

  • Format of generated modules:
    • --amd — generate simple AMD prologue. This option is useful to remove UMD prologues to conserve space.
    • --cjs — generate CommonJS prologue using static require() calls, and assigning the module result to module.exports.
    • --es6 — generate ES6 module prologue using static import statements, and declaring the module result as export default.
    • Otherwise, if no above options are specified, an optimized prologue is generated, which relies on browser globals, and can assign the module result to a global as well.
  • Directories:
    • --source=src — process files from src directory, and its sub-directories. If specified, it overrides a value specified by browserGlobal["!from"] variable of package.json described below.
    • --target=trg — save processed files in trg directory retaining the original sub-directories. If specified, it overrides a value specified by browserGlobal["!dist"] variable of package.json described below.
    • --config=cfg — use configuration files (package.json, bower.json) from cfg directory. Default: "." (the current directory).

It is advisable to add it to a package.json file of a project in question in scripts section, so it is always available:

{
  // ... package.json settings ...
  "scripts": {
    // ... project-specific scripts ...
    "dist": "node node_modules/heya-globalize/index.js"
  },
  // ... more package.json settings ...
}

This script can be invoked like that:

npm run dist

It is possible to run the script on at lifecycle events, e.g., after installing that package, or integrate with existing project tooling, such as grunt or gulp runners. See npm-scripts for more details on scripts.

Configuration

The converter takes its configuration from following sources:

  • package.js with following sections used:
    • main can be used indirectly by browser section.
    • name to provide a default for a global variable that will host package modules.
    • browser to rename/skip files, while preparing a distribution for a browser.
    • browserGlobals to define how modules mapped to globals. This section is described in details below.
      • AMD/CommonJS/ES6 modes ignore the mapping itself, but still respect directory settings, like !dist and !from.
  • bower.json with following sections used:
    • ignore to skip files, while preparing a distribution for a browser.

browserGlobals

This section of package.json can contain a simple key/value pairs as an object, where keys are module names, and values are corresponding globals. If a module is not listed there, its parents will be checked. If a parent is specified, it will be used to form an accessor.

There are two special keys:

  • !root — a root variable to resolve all local modules. For example, if !root is heya.example, ./a will be resolved as heya.example.a. Default: name of the package taken from package.json.
  • !dist — a folder name where to copy all transformed files. Default: dist.
  • !from — a folder name to serve as a root for source files. Default: the project's top folder.

Some modules modify existing packages by augmenting their exports. They do not create their own globals using existing ones. In this case, a value of such module should be a global variable to use when referring to this module, but it should be prefixed with '!'. This prefix means that modules result is not assigned anywhere on its definition, the rest defines how to access it.

External modules should be always resolved explicitly in browserGlobals.

Example #1

We have five modules:

  1. ./box, which defines the main functionality,
  2. ./boxExt, which extends the main functionality,
    • Depends on ./box.
  3. ./wrench is a simple module.
  4. ./belt/utils, which provides some additional functionality,
  5. ./belt/utils/hammer/small, which is a specialized algorithm.
    • Depends on ./boxExt, and modules from an external package anvil.

We know that anvil uses a global variable window.anvil. We want our package to be anchored at window.heya.box, our main module ./box should map to that variable as well, as ./boxExt, and all modules below ./belt/utils should be anchored at window.toolbox. This is how our browserGlobals should look like:

{
  // ... package.json settings ...
  "browserGlobals": {
    "!root":        "heya.box",
    "./box":        "heya.box",
    "./boxExt":     "!heya.box",
    "./belt/utils": "toolbox",
    "anvil":        "anvil"
  },
  // ... more package.json settings ...
}

With this configuration our modules are mapped to globals like that:

./box                     => heya.box
./boxExt                  => heya.box
./wrench                  => heya.box.wrench
./belt/utils              => toolbox
./belt/utils/hammer/small => toolbox.hammer.small
anvil/x                   => anvil.x
anvil/y/z                 => anvil.y.z

Example #2: dcl

A possible map for the main part of dcl to accommodate existing (as of 1.1.3) globals:

{
  // ... package.json settings ...
  "browserGlobals": {
    "!root":       "dcl",
    "./mini":      "dcl",
    "./legacy":    "dcl",
    "./dcl":       "!dcl",
    "./debug":     "dclDebug",
    "./advise":    "advise",
    "./inherited": "!dcl.inherited"
  },
  // ... more package.json settings ...
}

Example #3: super simple

For a simple mapping of all local files to a single root variable, we don't need to specify anything. For example, if our module is called our-core, following modules will be mapped like that:

./a     => window["our-core"].a
./b     => window["our-core"].b
./b/c   => window["our-core"].b.c
./d/e/f => window["our-core"].d.e.f

Note that our-core is used as an anchor variable for all modules, but it is not an identifier in a JavaScript sense, so it is used with [] notation, rather than a dot notation.

Let's fix it, and assign a simple root variable:

{
  // ... package.json settings ...
  "browserGlobals": {
    "!root": "kore"
  },
  // ... more package.json settings ...
}

Now our modules will be mapped like that:

./a     => kore.a
./b     => kore.b
./b/c   => kore.b.c
./d/e/f => kore.d.e.f

Algorithm

The precise algorithm works like that:

  1. package.json and bower.json are read from the current directory. The latter is optional.
  2. All *.js files are collected from the current directory recursively.
  3. Certain directories are always excluded:
    1. node_modules
    2. bower_components
    3. The dist directory (can be overridden in !dist value of browserGlobals section of package.json).
  4. Directories and files from ignore section of bower.json, if any, are excluded too.
  5. The remaining files are processed one by one. The result of a successful transformation is copied to dist directory (can be overridden in !dist value of browserGlobals section of package.json) preserving the directory structure.

The latter step means that files are copied like that:

./a.js    => ./dist/a.js
./b.js    => ./dist/b.js
./b/c.js  => ./dist/b/c.js
./d/e/f.js => ./dist/d/e/f.js

When files are processed they are checked against a standard Heya-style UMD header (it covers both AMD and CommonJS-style modules, but no globals), or a simple AMD header (the very first line starts with define(, and lists all dependencies as an array of strings). If a file is not identified as one of those, it is ignored and skipped.

While resolving module names, the directory structure is preserved as well, and reflected as subobjects using a dot or [] notation (whichever is more appropriate). Important details:

  • All local modules are assumed to be anchored at !root variable specified in browserGlobals.
  • All modules are checked against browserGlobals, and if it is there, the specified variable is used.
  • Otherwise all parents are checked agaist browserGlobals, and the closest parent's variable is used for the rest as an anchor.

If a module depends on a special module called module, a new object is generated and two its properties id and filename is set to a name of the current module. That way a module may report its name in errors and exceptions. Example #5 below shows a generated code.

Example #4: multiple parents

{
  // ... package.json settings ...
  "browserGlobals": {
    "!root":   "kore",
    "./b":     "kore.base",
    "./b/c/d": "kore.bcd"
  },
  // ... more package.json settings ...
}

Given this map we can resolve following modules like that:

./a         => kore.a
./b/a       => kore.base.a
./b/c       => kore.base.c
./b/c/d/e   => kore.bcd.e
./b/c/d/e/f => kore.bcd.e.f

External modules are resolved the same way as local modules, but they require that at least top-level package names were defined, because they cannot use !root to form a name.

Example #5: transforms

This is complete example, which shows original and transformed sources. The config is:

{
  // ... package.json settings ...
  "browserGlobals": {
    "!root": "heya.example",
    "boom":  "BOOM",
    "./d":   "!heya.D",
    "./f":   "!heya.F"
  },
  // ... more package.json settings ...
}

a.js was copied to dist/a.js:

// before
/* UMD.define */ (typeof define=="function"&&define||function(d,f,m){m={module:module,require:require};module.exports=f.apply(null,d.map(function(n){return m[n]||require(n)}))})
(["./b", "./c"], function(b, c){});

// after
(function(_,f){window.heya.example.a=f(window.heya.example.b,window.heya.example.c);})
(["./b", "./c"], function(b, c){});

b.js was copied to dist/b.js:

// before
/* UMD.define */ (typeof define=="function"&&define||function(d,f,m){m={module:module,require:require};module.exports=f.apply(null,d.map(function(n){return m[n]||require(n)}))})
(["./c"], function(c){});

// after
(function(_,f){window.heya.example.b=f(window.heya.example.c);})
(["./c"], function(c){});

c.js is copied to dist/c.js:

// before
/* UMD.define */ (typeof define=="function"&&define||function(d,f,m){m={module:module,require:require};module.exports=f.apply(null,d.map(function(n){return m[n]||require(n)}))})
([], function(){});

// after
(function(_,f,g){g=window;g=g.heya||(g.heya={});g=g.example||(g.example={});g.c=f();})
([], function(){});

d.js is copied to dist/d.js (note that this file includes module object, two modules from a declared external module boom, and an undeclared one wham! — the undeclared one will generate a warning):

// before
define(['module', 'boom', 'boom/Hello-World', 'wham!'], function(module, boom, hello, wham){});

// after
(function(_,f,m){m={};m.id=m.filename="./d";f(m,window.BOOM,window.BOOM["Hello-World"],window["wham!"]);})
(['module', 'boom', 'boom/Hello-World', 'wham!'], function(module, boom, hello, wham){});

e.js is copied to dist/e.js:

// before
define(['./d'], function(d){});

// after
(function(_,f){window.heya.example.e=f(window.heya.D);})
(['./d'], function(d){});

f.js is copied to dist/f.js:

// before
define(["./b", "./c"], function(b, c){});

// after
(function(_,f){f(window.heya.example.b,window.heya.example.c);})
(["./b", "./c"], function(b, c){});

As can be seen, the same module functions are used with new prologues, which replaces define() or an Heya-style UMD prologue, which itself approximates define() as well. New prologues form identical arguments using globals, and assign their results to correct global variables.

Converting to AMD

This mode behaves just like the browser globals mode, but produces AMD modules. It is invoked like that:

node node_modules/heya-globalize/index.js --amd

Example: AMD

Using a.js above:

a.js was copied to dist/a.js:

// before
/* UMD.define */ (typeof define=="function"&&define||function(d,f,m){m={module:module,require:require};module.exports=f.apply(null,d.map(function(n){return m[n]||require(n)}))})
(["./b", "./c"], function(b, c){});

// after
define
(["./b", "./c"], function(b, c){});

Converting to CommonJS

This mode behaves just like the browser globals mode, but produces CommonJS modules. It is invoked like that:

node node_modules/heya-globalize/index.js --cjs

Example: CommonJS

Using a.js above:

a.js was copied to dist/a.js:

// before
/* UMD.define */ (typeof define=="function"&&define||function(d,f,m){m={module:module,require:require};module.exports=f.apply(null,d.map(function(n){return m[n]||require(n)}))})
(["./b", "./c"], function(b, c){});

// after
(function(_,f){module.exports=f(require("./b"),require("./c"));})
(["./b", "./c"], function(b, c){});

Converting to ES6 module

This mode behaves just like the browser globals mode, but produces ES6 modules compatible with Babel. It is invoked like that:

node node_modules/heya-globalize/index.js --es6

Example: ES6 module

Using a.js above:

a.js was copied to dist/a.js:

// before
/* UMD.define */ (typeof define=="function"&&define||function(d,f,m){m={module:module,require:require};module.exports=f.apply(null,d.map(function(n){return m[n]||require(n)}))})
(["./b", "./c"], function(b, c){});

// after
import m0 from "./b";import m1 from "./c";export default (function(_,f){return f(m0,m1);})
(["./b", "./c"], function(b, c){});

Versions

  • 1.2.1 — Bugfix: more conservative ES6 module prologue.
  • 1.2.0 — Added command-line parameters to override configuration.
  • 1.1.0 — Added new prologue generators: AMD, CommonJS, ES6 modules.
  • 1.0.3 — Bugfixes: following sym links, and normalizing module names.
  • 1.0.2 — More internal restructuring.
  • 1.0.1 — Internal restructuring to accommodate grunt-transform-amd.
  • 1.0.0 — The initial public release.

License

BSD