-
Notifications
You must be signed in to change notification settings - Fork 2
Home
Labeled Modules is an extension specification to CommonJS modules which borrows from the ECMAScript.next modules proposal to bring us closer to that model.
It's a syntax specifically designed for small JavaScript library authors, however large projects and individual app developers will find the same features appealing for their project.
require: "Math"
function length(a, b){
return abs(a - b);
}
exports: function area(x1, y1, x2, y2){
return length(x1, x2) * length(y1, y2);
}
This is valid ECMAScript 3 syntax. These files are compatible with plain old <script src="..."> loading - without boilerplate shims. I.e. you don't necessarily need a loader to run them in the browser.
EMCAScript 6 (or whatever version) introduces three new keywords: export, import and module. These allow linking variables between isolated scopes and between files. The module
statement in ECMAScript 6 is not covered by the Labeled Modules specification.
exports:
Labeled Modules specifies the semantic meaning of a JavaScript label named exports:
. It must have the same semantics as the export
statement in ECMAScript 6. It allows you to export variables or function declarations. However, it does not allow you to export module
statements since those are not covered by Labeled Modules.
exports: var x, y = function(){};
exports: function foo(){};
var z = 123;
exports: z;
is equivalent to this ECMAScript.next code:
export var x, y = function(){};
export function foo(){};
var z = 123;
export z;
require:
Labeled Modules specifies the semantic meaning of a JavaScript label named require:
followed by one or more string literals. The string literals are optionally separated by a comma. This label must have the same semantics as the import * from "..."
statement defined in ECMAScript 6, for each string specified.
require: "A", "../B", '/C';
is equivalent to this ECMAScript.next code:
import * from "A";
import * from "../B";
import * from '/C';
The import name from "..."
form is not supported. If you need to require only a specific name, you may have a naming conflict and require a module loader. You should then use the CommonJS syntax var name = require("...").name;
.
The import * from ModuleName
form is not supported since module
statements are not covered by Labeled Modules.
Labeled Modules does not allow you to export the same variable links that you get from a require statement. In those cases the locally exported variable is overriding the require:d variable.
Since ECMAScript 6 introduces new keywords for modules, you can't target both ES.old and ES.next modules with the same file. Even if you're not using any other ECMAScript.next features.
We recommend that you write your code that targets both ECMAScript 3/5 and ECMAScript 6 using Labeled Modules. That way you can test and develop in the old version to ensure you're not using any new language features. You can easily convert it into ECMAScript 6 to make two different distributions to your users. Since they're semantically equivalent, they'll look almost the same.
By placing exports: infront of a top level variable or function declaration, those variables are automatically linked to a property with the same name in the "exports" object.
exports: A, B;
This is roughly equivalent to this CommonJS code:
exports.A = A;
exports.B = B;
Since Labeled Modules are not a replacement but a CommonJS extension, the exports object is still available. However, conforming JavaScript files SHOULD not use both the exports object and the exports:
or require:
labels.
Exported top level variables are NOT copied to the exports object. They're bound directly to it.
exports: var A = 5;
exports.A = 10;
A; // === 10;
By placing require: in front of one or more string literals, the exports of those modules are automatically imported as variables in the top scope of this module.
// Labeled Modules
require: "Foo";
This is roughly equivalent to this CommonJS code:
// CommonJS
var A = require("Foo").A;
var B = require("Foo").B;
Unlike the above CommonJS sample, variables are linked directly to the module. It's actually closer to the equivalent to wrapping your CommonJS code in a couple of with statements:
with(require("ModuleID"))
with(exports){
...
}
CommonJS introduces a new API that requires a script loader even for the simplest scripts; traditional browser libraries, however, want to remain compatible with simple script tag loading. These libraries have to include bloated boilerplate code to conform to CommonJS.
Labeled Modules introduces the require: and exports: statements. These are just no-op labels and they're valid ECMAScript.old syntax! If there are no conflicts at the top level scope, these files can be easily loaded by simple script tags, as they are.
This means that your simple demos and open source repository doesn't need to embed a script loader. They can use your source files, as they are.
If you have a bigger project you can use a Labeled Modules compatible script loader to run the same modules in isolation, as they are.
The CommonJS syntax does not enforce a form that can be statically analyzed to provide early warnings or package modules together. The following is discouraged but valid syntax in most loaders:
var id = "Foo", r = require; var Foo = r(id);
for (var key in Foo) exports[key] = Foo[key];
Labeled Modules enforces a syntax that can be statically analyzed without executing or pseudo-executing the module. This enables easy packaging and optimization. If an imported variable is undeclared because it's no longer imported. You can get an early warning message.
Since Labeled Modules is compatible with the classic global scope, traditional IDEs can infer that a variable name in one file is the same object as the variable name in a name. This can provide a better tooling experience with existing tools.
The require:
and exports:
statements enforce a syntax that can be statically analyzed
The next version of JavaScript will require export properties to be statically defined. Since CommonJS export objects can be passed through functions and down a complex code path. It is not easy to figure out what exports will be included without running the code. The exports may even be dynamic depending on the environment. If you also use module.exports
or module.setExports()
to export a function object, you have to be able to track the properties exported by any given object.
To do this reliably it requires complex program analysis. No existing tool can reliably convert CommonJS modules to the static export syntax required by ECMAScript.next. To do this reliably, you have to execute your module and then scan the exported objects.
Labeled Modules' exports can be easily converted to ECMAScript.next modules without executing the code.
CommonJS's require function is non-deterministic and conditional. That means that a module may need to execute only in certain circumstances and at a specific point in the program. A CommonJS program cannot be reliably converted into the ECMAScript.next model where execution is not conditional.
Labeled Modules are always executed before entering the dependent module. This is more restrictive and can therefore be safely converted into the ECMAScript.next model.
Because CommonJS modules are non-deterministic and conditional, it is difficult to optimize the boilerplate code around a module that wants to be loaded in the browser. You have to include a CommonJS runtime shim.
The overhead of packing a CommonJS runtime shim for a single file may be negligible. However, if every micro-library does this, you will end up with a very large code base.
Labeled Modules can be efficiently packed together and optimized for use in the browser with minimal overhead. You can safely use many such packages without overhead.
CommonJS identifiers have to be named both in the module itself and in the module requiring these identifiers. This often causes the same name to be repeated many times:
// CommonJS
exports.A = A;
exports.B = B;
exports.C = C;
var A = require("Foo").A;
var B = require("Foo").B;
var C = require("Bar").C;
In Labeled Modules, all the dependency's exports can be imported at once.
// Labeled Modules
exports: A, B;
exports: C;
require: "Foo", "Bar";
The same identifier can be moved from one dependency (Foo) to another (Bar) without updating any of the many modules using both packages.
If you have to import multiple modules that export the same name, you can still use a classic CommonJS require expression to resolve any conflicts. Labeled Modules doesn't replace CommonJS, it extends it.
CommonJS can't give a global alias to something that may be a cyclic dependency at some point.
// CommonJS
var A = require("Foo").A;
exports.B = function(){
return A;
};
If at some point, Foo has a dependency on something that depends on Bar, you'll have a cyclic dependency. That means Bar is not guaranteed to execute before Foo. You'll have to refer to module object instead. This creates more code and makes your alias strictly bound to a specific module object.
// CommonJS with Cyclic Dependencies
var Foo = require("Foo");
exports.B = function(){
return Foo.A;
};
Using Labeled Modules, global names are linked to the required module. You can use the same pattern consistently regardless of Cyclic Dependencies.
// Labeled Modules
require: "Foo";
exports: function B(){
return A;
};
Asynchronous Module Definition (AMD) is a convenient format. As a small library author you probably want to support user that need AMD formatted files. We recommend that you write your code as Labeled Modules or strictly formatted CommonJS. A tool that supports Labeled Modules can easily convert your source files into AMD for distribution. That way you can offer your user that download option as well as other module formats.
It's very complicated to optimize AMD modules for use without an AMD loader. You will always end up with a small AMD shim in each package. Therefore, we don't recommend that you use AMD as your source format. Use Labeled Modules and convert it into AMD.
For your production code we recommend that you use a compiler tool that can optimize your module loading either way.
AMD works with cross-domain hosting during development and production. However, you can load Labeled Modules cross-domain as well. The details and requirements are implementation specific.
That depends on the primary target of your code.
If your library code primarily targets Node.js exclusively, it might be a good idea to use plain CommonJS until Node.js implements Labeled Modules.
If your library code is primarily loaded from a file:// protocol, it might be best to use AMD to enable quick testing from source in all browsers.
If your library code want to target traditional browser apps that doesn't require a loader, or if you want to be future compatible with ES.next modules. Then it might be a good idea to use Labeled Modules syntax to enable script tag loading from source.
If your code is only intended for your own application, it's up to your preference. We prefer the lightweight syntax of Labeled Modules.
ECMAScript.next Modules Proposal
This project is not associated with neither the ECMA TC-39 commitee (ECMAScript) nor CommonJS.