From 95c5757369e009803642c744ad49e079810f4dfc Mon Sep 17 00:00:00 2001 From: Bradley Farias Date: Fri, 26 Aug 2016 02:22:40 -0500 Subject: [PATCH 1/7] 002 - rewrite after discussions due to breakages / problems with VM support, timing, etc. --- 002-es6-modules.md | 651 ++++++++++++++++----------------------------- 1 file changed, 222 insertions(+), 429 deletions(-) diff --git a/002-es6-modules.md b/002-es6-modules.md index 8a824e4..9c6e711 100644 --- a/002-es6-modules.md +++ b/002-es6-modules.md @@ -1,20 +1,24 @@ -| Title | ES6 Module Interoperability | +| Title | ESM Interoperability | |--------|-----------------------------| | Author | @bmeck | | Status | DRAFT | | Date | 2016-01-07 | -**NOTE:** `DRAFT` status does not mean ES6 modules will be implemented in Node +**NOTE:** `DRAFT` status does not mean ESM will be implemented in Node core. Instead that this is the standard, should Node core decide to implement -ES6 modules. At which time this draft would be moved to `ACCEPTED`. +ESM. At which time this draft would be moved to `ACCEPTED`. --- +Abbreviations: +* `ESM` - Ecma262 Modules (ES Modules) +* `NCJS` - Node Modules (a CommonJS variant) + The intent of this standard is to: -* implement interoperability for ES modules and Node's existing module system +* implement interoperability for ESM and Node's existing module system * create a **Registry Object** (see WHATWG section below) compatible with - the [WHATWG Loader](http://whatwg.github.io/loader/) Registry + the [WHATWG Loader](http://whatwg.github.io/loader/) Registry and/or [WHATWG module map](https://html.spec.whatwg.org/multipage/webappapis.html#module-map) ## 1. Purpose @@ -26,7 +30,6 @@ The intent of this standard is to: ## 2. Related - [ECMA262](tc39.github.io/ecma262/) discusses the syntax and semantics of related syntax, and introduces: @@ -53,232 +56,85 @@ related syntax, and introduces: - A hook for when an import is exactly performed. This returns a `ModuleRecord`. Used as a means to grab modules from Node's loader/cache. -* **[CreateImportBinding] -(https://tc39.github.io/ecma262/#sec-createimportbinding)** - - A means to create a shared binding (variable) from an export to an - import. Required for the "live" binding of imports. - * **[ModuleNamespaceCreate] (https://tc39.github.io/ecma262/#sec-modulenamespacecreate)** - Provides a means of creating a list of exports manually, used so that - CommonJS `module.exports` can create `ModuleRecord`s that are + NCJS `module.exports` can create `ModuleRecord`s that are prepopulated. +## 3. Semantics + +### 3.1. Determining if source is an ES Module + +A new file type will be used for ES Modules, `.mjs`. Files with this extensions +will be treated using the loading semantics in this document, but retain a way +to load existing extensions of `.json`, `.node`, and `.js`. The file type for +JavaScript with IANA as an official file type needs to have this extensions +added. This comes from browsers being unable to implement a new MIME for the +differing parse goals. In order to make this change, contact the IESG. + +The `.mjs` file extension will be searched for prior to any `.js` file in any +algorithm that uses searching (`require` or `import`); e.g. once the Node +resolution algorithm reaches file expansion, `path + '.mjs'` would be searched +prior to `path + '.js'` when performing `require(path)`. + +# 3.1.1. Inter package loading using file extension breakage. + +There is knowledge of breakage for code that upgrades inner package +dependencies such as `require('foo/bar.js')`. As `bar.js` may move to +`bar.mjs`. Since `bar.js` is not the listed entry point this was considered +acceptable. + +### 3.2. Import Path Parsing + +`import` will parse its path using URLs. As such, encoding and decoding will automatically be performed. This may affect file paths containing any of the following characters: `:`,`?`,`#`, or `%`. Details of the parsing algorithm are at the [WHATWG URL Spec](https://url.spec.whatwg.org/) + +* paths with `:` face multiple variations of path mutation +* paths with `%` in their path segments would be decoded +* paths with `?`, or `#` in their paths would face truncation of pathname + +### 3.3. Import Path Resolution + +The `import` resolution algorithm for node is as follows: -[WHATWG Loader](https://github.com/whatwg/loader) discusses the design of -module metadata in a [Registry](https://whatwg.github.io/loader/#registry). All -actions regarding the Registry can be done -synchronously, though JS level API uses Promises. - -**NOTE:** It is not Node's intent to implement the asynchronous pipeline in the -WHATWG Loader specification. - -## 3. Additional Structures Required - -### 3.1. **DynamicModuleRecord** - -A Module Record that represents a view of an Object for its `[[Namespace]]` -rather than coming from an environment record. - -`DynamicModuleRecord` preserves the feature that exported values are known when -it comes time for `HostResolveImportedModule` to return. That means that they -are known after the -file is parsed, but before it is evaluated. This behavior is preserved by Node -synchronously executing CJS files when they are encountered during -`HostResolveImportedModule`. - -When creating a `DynamicModuleRecord` the [`[[Exports]]`] -(https://tc39.github.io/ecma262/#table-29) is frozen upon construction. No new -exports may be added. No exports may be removed. The values of the exports will -continue to be mutable however. - -### 3.1.1 DynamicModuleCreate(O) - -The abstract operation `DynamicModuleCreate` with arguments `namespace` is used -to allow creation of new `DynamicModuleRecord`s. It performs the following -steps: - -1. Let M be a newly created object. -2. Set M's essential internal methods to the definitions specified in - [15.2.1.15 Abstract Module Records] - (https://tc39.github.io/ecma262/#sec-abstract-module-records) -3. Set M's [[Realm]] internal slot to the current Realm Record. -4. Set M's [[Namespace]] internal slot to DelegatedModuleNamespaceObjectCreate - (`M`, `O`) -5. Set M's [[Environment]] internal slot to NewObjectEnvironment(`M`.[[Namespace]], **null**) -6. Set M's [[Evaluated]] internal slot to **true** -7. Return M - -### 3.2. **DelegatedModuleNamespaceObject** - -A `ModuleNamespaceObject` that performs delegation to an Object when accessing -properties. This is used for delegation behavior from CJS `module.exports` when -imported by ES modules. - -#### Table 1: Internal Slots of DelegatedModuleNamespaceObject Namespace Exotic -Objects - -Field Name | Value Type | Meaning ----| --- | --- -[[Delegate]] | Object | The Object from which to delegate access - -#### 3.2.1. `[[Get]] (P, Receiver)` - -When the [[Get]] internal method of a module namespace exotic object O is -called with property key P and ECMAScript language value Receiver, the -following steps are taken: - -1. Assert: IsPropertyKey(`P`) is true. -2. If Type(`P`) is Symbol, then -3. Return ? OrdinaryGet(`O`,`P`,` Receiver`). -4. Let exports be the value of `O`'s `[[Exports]]` internal slot. -5. If `P` is not an element of exports, return **undefined**. -6. Let m be the value of `O`'s `[[Object]]` internal slot. -7. If `P` equals **"default"**, return `m`. -7. Let `value` be ! `O`.`[[Get]]`(`P`,` O`) -8. Return `value` - -#### 3.2.2. `DelegatedModuleNamespaceObjectCreate(module, O)` - -The abstract operation `DelegatedModuleNamespaceObjectCreate` with arguments `O` -is used to create a `DelegatedModuleNamespaceObject`. It performs the following -steps: - -1. Assert: `module` is a Module Record. -2. Assert: `module`.[[Namespace]] is **undefined**. -3. Let `NS` be a newly created object. -4. Set `NS`'s essential internal methods to the definitions specified in [9.4.6 - Module Namespace Exotic Objects] - (https://tc39.github.io/ecma262/#sec-module-namespace-exotic-objects) -5. Set `NS`'s [[Module]] internal slot to `module` -6. Let `exports` be a new List -7. Let `p` be `O`. -8. Let `done` be **false**. -9. Repeat while `done` is **false**, - 1. If `p` is null, let `done `be **true**. - 2. Else, - 1. For each `property` in OwnPropertyKeys(`p`) - 1. If `property` does not equal **"default"** and `exports` does - not contain `property`, add `property` to exports - 2. If the [[GetPrototypeOf]] internal method of `p` is not the ordinary - object internal method defined in [9.1.1] - (https://tc39.github.io/ecma262/#sec-ordinary-object-internal-methods-and-internal-slots-getprototypeof) - , let `done` be **true**. - 3. Else, let `p` be the value of p's [[Prototype]] internal slot. -10. Set the value of the [[Exports]] internal slot of `NS` to `exports`. -11. Return `NS` - -## 4. Algorithm - -### 4.1. NodeModuleEvaluationJob(source, mode) - -The abstractjob operation `NodeModuleEvaluationJob` with parameters `source` and -`mode`. The results of this should be placed in the cache that -`HostResolveImportedModule` uses. - -1. If `mode` equals CJS - 1. Let `body` be the bootstraped form of `source` with necessary CJS - wrapper code. - 2. Call ! ScriptEvaluationJob(`body`, **undefined**) - 3. Return ! DynamicModuleCreate from `module.exports` -2. Else if `mode` equals ES - 1. Let `M` be ! ParseModule(`source`) - 2. Perform the algorithm listed in **4.** on `M`.[[RequestedModules]] while - respecting the semantics in **5.** - 3. Connect `import` bindings for all relevant submodules using - [ModuleDeclarationInstantiation from the Abstract Module Record M] - (https://tc39.github.io/ecma262/#table-37) - 4. Call ! `M`.[[ModuleEvaluation]] - 5. Return `M` - -**NOTE:** This still guarantees: - -* ES module dependencies are all executed prior to the module itself -* CJS modules have a full shape prior to being handed to ES modules -* CJS modules can imperatively start loading other modules, including ES modules - -## 5. Semantics - -### 5.1. Determining if source is an ES Module - -Require that Module source text has at least one `import` or `export` declaration. -A module with only an `import` declaration and no `export` declaration is valid. -Modules, that do not export anything, should specify an `export {}` to make -intentions clear and avoid accidental parse errors while removing `import` -declarations. The `export {}` is **not** new syntax and does **not** export an -empty object. It is simply the standard way to specify exporting nothing. - -A package opts-in to the Module goal by specifying `"module"` as the parse goal -field *(name not final)* in its `package.json`. Package dependencies are not -affected by the opt-in and may be a mix of CJS and ES module packages. If a parse -goal is not specified, then attempt to parse source text as the preferred goal -*(Script for now since most modules are CJS)*. If there is a parse error that -may allow another goal to parse, then parse as the other goal, and so on. After -this, the goal is known unambiguously and the environment can safely perform -initialization without the possibility of the source text being run in the wrong -goal. - -Note: While the ES2015 specification -[does not forbid](http://www.ecma-international.org/ecma-262/6.0/#sec-forbidden-extensions) -this extension, Node wants to avoid acting as a rogue agent. Node has a TC39 -representative, [@bmeck](https://github.com/bmeck), to champion this proposal. -A specification change or at least an official endorsement of this Node proposal -would be welcomed. If a resolution is not possible, this proposal will fallback -to the previous [`.mjs` file extension proposal](https://github.com/nodejs/node-eps/blob/5dae5a537c2d56fbaf23aaf2ae9da15e74474021/002-es6-modules.md#51-determining-if-source-is-an-es-module). - -#### 5.1.1 Goal Detection - -##### Parse (source, goal, throws) - - The abstract operation to parse source text as a given goal. - - 1. Bootstrap `source` for `goal`. - 2. Parse `source` as `goal`. - 3. If success, return `true`. - 4. If `throws`, throw exception. - 5. Return `false`. - -##### Operation - -1. If a package parse goal is specified, then - 1. Let `goal` be the resolved parse goal. - 2. Call `Parse(source, goal, true)` and return. - -2. Else fallback to multiple parse. - 1. If `Parse(Source, Script, false)` is `true`, then - 1. Return. - 2. Else - 1. Call `Parse(Source, Module, true)`. - - *Note: A host can choose either goal to parse first and may change their order - over time or as new parse goals are introduced. Feel free to swap the order of - Script and Module.* - -#### 5.1.2 Implementation - -To improve performance, host environments may want to specify a goal to parse -first. This can be done in several ways:
-cache on disk, a command line flag, a manifest file, HTTP header, file extension, etc. - -#### 5.1.3 Tooling Concerns - -Some tools, outside of Node, may not have access to a JS parser *(Bash programs, -some asset pipelines, etc.)*. These tools generally operate on files as opaque -blobs / plain text files and can use the techniques, listed under -[Implementation](#512-implementation), to get parse goal information. +* `request` should represent the requested path to load. +* `context_url` should represent the current script's absolute URL's directory + * for CLI usage, it is the absolute URL for the current working directory -### 5.2. ES Import Path Resolution - -ES `import` statements will perform non-exact searches on relative or -absolute paths, like `require()`. This means that file extensions, and -index files will be searched, +* if `request` parses via the [URL parser](https://url.spec.whatwg.org/#concept-url-parser) + * let `url` be the result +* else if `^[.]?[.]?[/]` prefixes `request` + * let `url` be the result of `new URL(request, context_url)` + * if `url` points to a directory + * let `url` be the result of searching the directory + * if `url` does not point to a file + * for the well known file extensions `extension` in `[ + .mjs', '.js', '.json', and '.node']` + * let `searchUrl` be a copy of `url` with `extension` added to the pathname + * if `searchUrl` points to a file + * let url be `searchUrl` + * break +* else + * let `url` be the result of searching node_modules using `request` and `context_url` + * NOTE: checks for escaping modules to `node_modules/` via `../` going to be added +* load url + * we should support `data:` and `file:` out of the box + * `file:` should use file type to determine dependency type (ES Module / JSON / C++ / NCJS). + * `data:` should interpret javascript MIME as ES Module. ES Modules share the MIME with NCJS, so we don't have enough data to differentiate, so just assume ES Module since that is [what the browser will assume](https://html.spec.whatwg.org/multipage/webappapis.html#hostresolveimportedmodule(referencingmodule,-specifier)). + + +`import` will perform non-exact searches on relative or +absolute paths, preserving the behavior in `require()`. This means that known +file extensions, and index files will be searched. In summary: ```javascript // looks at +// ./foo.mjs // ./foo.js // ./foo/package.json +// ./foo/index.mjs // ./foo/index.js // etc. import './foo'; @@ -286,8 +142,10 @@ import './foo'; ```javascript // looks at +// /bar.mjs // /bar.js // /bar/package.json +// /bar/index.mjs // /bar/index.js // etc. import '/bar'; @@ -295,12 +153,16 @@ import '/bar'; ```javascript // looks at: +// ./node_modules/baz.mjs // ./node_modules/baz.js // ./node_modules/baz/package.json +// ./node_modules/baz/index.mjs // ./node_modules/baz/index.js // and parent node_modules: +// ../node_modules/baz.mjs // ../node_modules/baz.js // ../node_modules/baz/package.json +// ../node_modules/baz/index.mjs // ../node_modules/baz/index.js // etc. import 'baz'; @@ -308,18 +170,22 @@ import 'baz'; ```javascript // looks at: +// ./node_modules/abc/123.mjs // ./node_modules/abc/123.js // ./node_modules/abc/123/package.json +// ./node_modules/abc/123/index.mjs // ./node_modules/abc/123/index.js // and parent node_modules: +// ../node_modules/abc/123.mjs // ../node_modules/abc/123.js // ../node_modules/abc/123/package.json +// ../node_modules/abc/123/index.mjs // ../node_modules/abc/123/index.js // etc. import 'abc/123'; ``` -#### 5.2.1. Removal of non-local dependencies +#### 3.3.1. Removal of non-local dependencies All of the following will not be supported by the `import` statement: @@ -327,10 +193,11 @@ All of the following will not be supported by the `import` statement: * `$HOME/.node_modules` * `$HOME/.node_libraries` * `$PREFIX/lib/node` +* `module/../../outside-of-module` Use local dependencies, and symbolic links as needed. -##### 5.2.1.1. How to support non-local dependencies +##### 3.3.1.1. How to support non-local dependencies Although not recommended, and in fact discouraged, there is a way to support non-local dependencies. **USE THIS AT YOUR OWN DISCRETION**. @@ -359,38 +226,102 @@ Transform to: And nest as many times as needed. -#### 5.2.2. Errors from new path behavior. +#### 3.3.2. Errors from new path behavior. -In the case that an `import` statement is unable to find a module, Node should +In the case that an `import` is unable to find a module, Node should make a **best effort** to see if `require` would have found the module and print out where it was found, if `NODE_PATH` was used, if `HOME` was used, etc. -### 5.3. `this` in ES modules +#### 3.3.3. Shipping both ESM and NCJS + +When a `package.json` main is encountered, file extension searches are used to +provide a means to ship both ESM and NCJS variants of packages. If we have two +entry points `index.mjs` and `index.js` setting `"main":"./index"` in +`package.json` will make Node pick up either, depending on what is supported. + +##### 3.3.3.1. Excluding main + +Since `main` in `package.json` is entirely optional even inside of npm +packages, some people may prefer to exclude main entirely in the case of using +`./index` as that is still in the Node module search algorithm. + +### 3.4. ESM Evaluation + +#### 3.4.1. Environment Variables + +ESM will not be bootstrapped with magic variables and will await upcoming specifications in order to provide such behaviors in a standard way. As such, the following variables are changed: + +| Variable | Exists | Value | +| ---- | ---- | --- | +| this | y | [`undefined`](https://tc39.github.io/ecma262/#sec-module-environment-records-getthisbinding) | +| arguments | n | | +| require | n | | +| module | n | | +| exports | n | | +| __filename | n | | +| __dirname | n | | + +Since ESM are always strict, errors may be thrown upon trying to use variables that do not exist in ESM. + +#### 3.4.2. Timing + +When loading ESM or using any form of `import`, the stack will unwind prior to performing any resolution or evaluation. -ES modules will have a `this` value set to `undefined`. This -is a breaking change. CJS modules have a `this` value set to their `module` -binding. +##### 3.4.2.1. Example -See ECMA262's [Module Environment Record]( -https://tc39.github.io/ecma262/#sec-module-environment-records-getthisbinding -) for this semantic. +```javascript +// entry.js +require('one.mjs'); +console.log('two'); +``` + +```javascript +// one.mjs +console.log('one'); +``` + +```sh +> node entry.js +two +one +``` + +### 3.5. Cross Module System Communication + +ESM and NCJS differ in many ways, as such they need a well defined means of converting between the two module types and systems. + +### 3.5.1. NCJS to ESM + +After *any* NCJS finishes evaluation, it will be placed into the same cache as ESM. +The value of what is placed in the cache will reflect a single default export pointing to the value of `module.exports` at the time evaluation ended. + +Essentially after any NCJS completes evaluation: -### 5.4. ES consuming CommonJS +1. if there was an error, place the error in the ESM cache and return +2. let `export` be the value of `module.exports` +3. if there was an error, place the error in the ESM cache and return +4. create an ESM with `{default:export}` as its namespace +5. place the ESM in the ESM cache -####5.4.1. default imports +### 3.5.2. ESM to NCJS -`module.exports` is a single value. As such it does not have the dictionary -like properties of ES module exports. In order to facilitate named imports for -ES modules, all properties of `module.exports` will be hoisted to named exports -after evaluation of CJS modules with the exception of `default` which will -point to `module.exports` directly. +ESM are *never* placed in the NCJS cache (`require.cache`). + +If `require` resolves to an ESM source text, the following steps are taken. + +1. let `ret` be a newly created Promise +2. queue a load job on the event loop + 1. if loading completes normally resolve `ret` to the ESM Module Namespace + 2. if an error occurs reject `ret` +3. put a "fetching" entry in the ESM cache +4. return `ret` ##### 5.4.1.1. Examples Given: ```javascript -// cjs.js +// NCJS.js module.exports = { default:'my-default', thing:'stuff' @@ -400,22 +331,21 @@ module.exports = { You will grab `module.exports` when performing an ES import. ```javascript -// es.js +// es.mjs // grabs the namespace -import * as baz from './cjs.js'; +import * as baz from './NCJS.js'; // baz = { -// get default() {return module.exports;}, -// get thing() {return this.default.thing}.bind(baz) +// default => module.exports; // } // grabs "default", aka module.exports directly -import foo from './cjs.js'; -// foo = {default:'my-default', thing:'stuff'}; +import foo from './NCJS.js'; +// foo = module.exports; // grabs "default", aka module.exports directly -import {default as bar} from './cjs.js'; -// bar = {default:'my-default', thing:'stuff'}; +import {default as bar} from './NCJS.js'; +// bar = module.exports; ``` ------ @@ -423,19 +353,19 @@ import {default as bar} from './cjs.js'; Given: ```javascript -// cjs.js +// NCJS.js module.exports = null; ``` You will grab `module.exports` when performing an ES import. ```javascript -// es.js -import foo from './cjs.js'; +// es.mjs +import foo from './NCJS.js'; // foo = null; -import * as bar from './cjs.js'; -// bar = {default:null}; +import * as bar from './NCJS.js'; +// bar = {default=null}; ``` ------ @@ -443,7 +373,7 @@ import * as bar from './cjs.js'; Given: ```javascript -// cjs.js +// NCJS.js module.exports = function two() { return 2; }; @@ -452,12 +382,12 @@ module.exports = function two() { You will grab `module.exports` when performing an ES import. ```javascript -// es.js -import foo from './cjs.js'; +// es.mjs +import foo from './NCJS.js'; foo(); // 2 -import * as bar from './cjs.js'; -bar.name; // 'two' +import * as bar from './NCJS.js'; +bar.name; // undefined bar.default(); // 2 bar(); // throws, bar is not a function ``` @@ -467,27 +397,27 @@ bar(); // throws, bar is not a function Given: ```javascript -// cjs.js +// NCJS.js module.exports = Promise.resolve(3); ``` You will grab `module.exports` when performing an ES import. ```javascript -// es.js -import foo from './cjs.js'; +// es.mjs +import foo from './NCJS.js'; foo.then(console.log); // outputs 3 -import * as bar from './cjs.js'; +import * as bar from './NCJS.js'; bar.default.then(console.log); // outputs 3 -bar.then(console.log); // throws, bar is not a Promise +bar.then(console.log); // throws, bar does not have a .then property ``` -### 5.5. CommonJS consuming ES +### 5.5. NCJS consuming ESM #### 5.5.1. default exports -ES modules only export named values. A "default" export is an export that uses +ESM only export named values. A "default" export is an export that uses the property named `default`. ##### 5.5.1.1. Examples @@ -495,7 +425,7 @@ the property named `default`. Given: ```javascript -// es.js +// es.mjs let foo = {bar:'my-default'}; // note: // this is a value @@ -505,15 +435,17 @@ foo = null; ``` ```javascript -// cjs.js -const es_namespace = require('./es'); -// es_namespace ~= { -// get default() { -// return result_from_evaluating_foo; -// } +// NCJS.js +const esm = require('./es'); +// esm ~= Promise => { +// default => result_from_evaluating_foo; // } -console.log(es_namespace.default); -// {bar:'my-default'} +es_namespace.then( + es_namespace => { + console.log(es_namespace.default); + // {bar='my-default'} + } +) ``` ------ @@ -521,7 +453,7 @@ console.log(es_namespace.default); Given: ```javascript -// es.js +// es.mjs export let foo = {bar:'my-default'}; export {foo as bar}; export function f() {}; @@ -529,38 +461,37 @@ export class c {}; ``` ```javascript -// cjs.js +// NCJS.js const es_namespace = require('./es'); -// es_namespace ~= { -// get foo() {return foo;} -// get bar() {return foo;} -// get f() {return f;} -// get c() {return c;} +// esm ~= Promise => { +// foo=foo; +// bar=foo; +// f=f; +// c=c; // } ``` ### 5.6. Known Gotchas -All of these gotchas relate to opt-in semantics and the fact that CommonJS is a -dynamic loader while ES is a static loader. +All of these gotchas relate to opt-in semantics and the fact that NCJS has a +dynamic loader while ESM has a static loader. No existing code will be affected. -#### 5.6.1. ES exports are read only +#### 5.6.1. ESM exports are read only -The objects create by an ES module are [ModuleNamespace Objects][5]. +The objects create by an ESM are [ModuleNamespace Objects][5]. -These have `[[Set]]` be a no-op and are read only views of the exports of an ES -module. Attempting to reassign any named export will not work, but assigning to +These have `[[Set]]` be a no-op and are read only views of the exports of an ESM. Attempting to reassign any named export will not work, but assigning to the properties of the exports follows normal rules. -### 5.7. CJS modules allow mutation of imported modules +### 5.7. NCJS modules allow mutation of imported modules -CJS modules have allowed mutation on imported modules. When ES modules are -integrating against CJS systems like Grunt, it may be necessary to mutate a +NCJS modules have allowed mutation on imported modules. When ES modules are +integrating against NCJS systems like Grunt, it may be necessary to mutate a `module.exports`. -Remember that `module.exports` from CJS is directly available under `default` +Remember that `module.exports` from NCJS is directly available under `default` for `import`. This means that if you use: ```javascript @@ -581,152 +512,14 @@ properties will be mutable. #### 5.7.1. ES will not honor reassigning `module.exports` after evaluation -Since we need a consistent time to snapshot the `module.exports` of a CJS +Since we need a consistent time to snapshot the `module.exports` of a NCJS module. We will execute it immediately after evaluation. Code such as: ```javascript -// bad-cjs.js +// bad-NCJS.js module.exports = 123; setTimeout(_ => module.exports = null); ``` Will not see `module.exports` change to `null`. All ES module `import`s of the -module will always see `123`. - -#### 5.7.2. ES export list for CJS are snapshot immediately after execution. - -Since `module.exports` is snapshot immediately after execution, that is the -point when hoisting of properties occurs, adding and removing properties later -will not have an effect on the list of exports. - -```javascript -// bad-cjs.js -module.exports = { - yo: 'lo' -}; -setTimeout(_ => { - delete module.exports.yo; - module.exports.foo = 'bar'; - require('./es.js'); -}); -``` - -```javascript -// es.js -import * as namespace from './bad-cjs.js'; -console.log(Object.keys(namespace)); // ['yo'] -console.log(namespace.foo); // undefined - -// mutate to show 'yo' still exists as a binding -import cjs_exports from './bad-cjs.js'; -cjs_exports.yo = 'lo again'; -console.log(namespace.yo); // 'yolo again' -``` - -#### 5.7.3. Circular Dep CJS => ES => CJS Causes Throw - -Due to the following explanation we want to avoid a very specific problem. -Given: - -```javascript -// cjs.js -module.exports = {x:0}; -require('./es'); -``` - -```javascript -// es.js -import * as ns from './cjs.js'; -// ns = ? -import cjs from './cjs.js'; -// cjs = ? -``` - -ES modules must know the list of exported bindings of all dependencies prior to -evaluating. The value being exported for CJS modules is not stable to snapshot -until `cjs.js` finishes executing. The result is that there are no properties -to import and you receive an empty module. - -In order to prevent this sticky situation we will throw on this case. - -Since this case is coming from ES, we will not break any existing circular -dependencies in CJS <-> CJS. It may be easier to think of this change as -similar to how you cannot affect a generator while it is running. - -This would change the ES module behavior to: - -```javascript -// es.js -import * as ns from './cjs.js'; -// throw new EvalError('./cjs is not an ES module and has not finished evaluation'); -``` - -## 6. Example Implementations - -These are written with the expectation that: - -* ModuleNamespaces can be created from existing Objects. -* WHATWG Loader spec Registry is available as a ModuleRegistry. -* ModuleStatus Objects can be created. - -The variable names should be hidden from user code using various techniques -left out here. - -### 6.1. CJS Modules - -#### 6.1.1. Pre Evaluation - -```javascript -// for posterity, will still throw on circular deps -ModuleRegistry.set(__filename, new ModuleStatus({ - 'ready': {'[[Result]]':undefined}; -})); -``` - -#### 6.1.2. Immediately Post Evaluation - -##### 6.1.2.1. On Error - -```javascript -ModuleRegistry.delete(__filename); -``` - -##### 6.1.2.2. On Normal Completion - -```javascript -let module_namespace = module.exports; -ModuleRegistry.set(__filename, new ModuleStatus({ - 'ready': {'[[Result]]':DynamicModuleCreate(module_namespace)[[Namespace]]}; -})); -``` - -### 6.2. ES Modules - -#### 6.2.1. Post Parsing - -```javascript -Object.defineProperty(module, 'exports', { - get() {return module[[Namespace]]}; - set(v) {throw new Error(`${__filename} is an ES module and cannot assign to module.exports`)} - configurable: false, - enumerable: false -}); -``` - -Parsing occurs prior to evaluation, and CJS may execute once we start to -resolve `import`. - -#### 6.2.2. Header - -```javascript -// we will intercept this to inject the values -import {__filename,__dirname,require,module,exports} from ''; -``` - -#### 6.2.3. Immediately Post Evaluation - -##### 6.2.3.1. On Error - -```javascript -delete require.cache[__filename]; -``` +module will always see `{default=123}`. From 82276cec5a9c9d7785b448e066230dc1a34a12a4 Mon Sep 17 00:00:00 2001 From: Bradley Farias Date: Fri, 26 Aug 2016 02:24:21 -0500 Subject: [PATCH 2/7] 002 - rename file --- 002-es6-modules.md => 002-es-modules.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename 002-es6-modules.md => 002-es-modules.md (100%) diff --git a/002-es6-modules.md b/002-es-modules.md similarity index 100% rename from 002-es6-modules.md rename to 002-es-modules.md From 732efc7ce1f063d6716077aba87788ecc824851f Mon Sep 17 00:00:00 2001 From: Bradley Farias Date: Fri, 26 Aug 2016 08:21:47 -0500 Subject: [PATCH 3/7] 002 - fix things mentioned by @Kovensky --- 002-es-modules.md | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/002-es-modules.md b/002-es-modules.md index 9c6e711..afb2a8f 100644 --- a/002-es-modules.md +++ b/002-es-modules.md @@ -261,7 +261,21 @@ ESM will not be bootstrapped with magic variables and will await upcoming specif | __filename | n | | | __dirname | n | | -Since ESM are always strict, errors may be thrown upon trying to use variables that do not exist in ESM. +Like normal scoping rules, if a variable does not exist in a scope, the outer scope is used to find the variable. Since ESM are always strict, errors may be thrown upon trying to use variables that do not exist globally when using ESM. + +##### 3.4.1.1. Workaround + +Although heavily advised against, you can have a NM module sibling for your ESM that can export these things: + +```js +// expose.js +module.exports = {__dirname}; +``` + +```js +// use.mjs +import {__dirname} from './expose.js'; +``` #### 3.4.2. Timing @@ -440,7 +454,7 @@ const esm = require('./es'); // esm ~= Promise => { // default => result_from_evaluating_foo; // } -es_namespace.then( +esm.then( es_namespace => { console.log(es_namespace.default); // {bar='my-default'} @@ -462,7 +476,7 @@ export class c {}; ```javascript // NCJS.js -const es_namespace = require('./es'); +const esm = require('./es'); // esm ~= Promise => { // foo=foo; // bar=foo; From 89e490215e1ad7503afe32a9084499d566c2722a Mon Sep 17 00:00:00 2001 From: Bradley Farias Date: Fri, 26 Aug 2016 10:19:25 -0500 Subject: [PATCH 4/7] 002 - fix bug in example --- 002-es-modules.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/002-es-modules.md b/002-es-modules.md index afb2a8f..dc54560 100644 --- a/002-es-modules.md +++ b/002-es-modules.md @@ -274,7 +274,8 @@ module.exports = {__dirname}; ```js // use.mjs -import {__dirname} from './expose.js'; +import expose from './expose.js'; +const {__dirname} = expose; ``` #### 3.4.2. Timing From e8663803f0e14d9ea8472d551112736c5b3ad6fa Mon Sep 17 00:00:00 2001 From: Bradley Farias Date: Sat, 27 Aug 2016 09:36:37 -0500 Subject: [PATCH 5/7] 002 - fix typo --- 002-es-modules.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/002-es-modules.md b/002-es-modules.md index dc54560..f2d7af1 100644 --- a/002-es-modules.md +++ b/002-es-modules.md @@ -66,7 +66,7 @@ related syntax, and introduces: ### 3.1. Determining if source is an ES Module -A new file type will be used for ES Modules, `.mjs`. Files with this extensions +A new file type will be used for ES Modules, `.mjs`. Files with this extension will be treated using the loading semantics in this document, but retain a way to load existing extensions of `.json`, `.node`, and `.js`. The file type for JavaScript with IANA as an official file type needs to have this extensions From d563b39d466b441ba91977578442d3978b7f80d2 Mon Sep 17 00:00:00 2001 From: Bradley Farias Date: Sun, 28 Aug 2016 15:29:10 -0500 Subject: [PATCH 6/7] update date --- 002-es-modules.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/002-es-modules.md b/002-es-modules.md index f2d7af1..b94c5aa 100644 --- a/002-es-modules.md +++ b/002-es-modules.md @@ -2,7 +2,7 @@ |--------|-----------------------------| | Author | @bmeck | | Status | DRAFT | -| Date | 2016-01-07 | +| Date | 2016-08-26 | **NOTE:** `DRAFT` status does not mean ESM will be implemented in Node core. Instead that this is the standard, should Node core decide to implement From b56ee228fc67b972daeba3df094e651bf4ede870 Mon Sep 17 00:00:00 2001 From: Jeremiah Senkpiel Date: Wed, 7 Sep 2016 20:53:51 -0400 Subject: [PATCH 7/7] Repurpose 002: ESM -> "Node v2.0 Modules" *sigh* rebased ontop of https://github.com/nodejs/node-eps/pull/39 --- 002-es-modules.md | 557 ++++++++++------------------------------------ 1 file changed, 112 insertions(+), 445 deletions(-) diff --git a/002-es-modules.md b/002-es-modules.md index b94c5aa..d803a45 100644 --- a/002-es-modules.md +++ b/002-es-modules.md @@ -1,32 +1,89 @@ | Title | ESM Interoperability | |--------|-----------------------------| -| Author | @bmeck | +| Author | @bmeck / @fishrock123 | | Status | DRAFT | | Date | 2016-08-26 | -**NOTE:** `DRAFT` status does not mean ESM will be implemented in Node +**NOTE:** `DRAFT` status does not mean ESM / NM2 will be implemented in Node core. Instead that this is the standard, should Node core decide to implement -ESM. At which time this draft would be moved to `ACCEPTED`. +ESM / NM2. At which time this draft would be moved to `ACCEPTED`. --- Abbreviations: * `ESM` - Ecma262 Modules (ES Modules) * `NCJS` - Node Modules (a CommonJS variant) +* `NM2` - Node v2.0 Modules (Node Modules using the Module parse goal) +* `RMR` - Reflective Module Record (`ModuleRecord` type) The intent of this standard is to: -* implement interoperability for ESM and Node's existing module system -* create a **Registry Object** (see WHATWG section below) compatible with - the [WHATWG Loader](http://whatwg.github.io/loader/) Registry and/or [WHATWG module map](https://html.spec.whatwg.org/multipage/webappapis.html#module-map) +* implement interoperability for the Module Parse Goal and Node's existing +module system ## 1. Purpose -1. Allow a common module syntax for Browser and Server. -2. Allow a common registry for inspection by Browser and Server - environments/tools. - * These will most likely be represented by metaproperties like - `import.context`, but the spec is not yet fully in place. +Allow Node.js modules to use "Module Mode" **without**: + +1. Hitting the Reflective Module Record. + - The Reflective Module Record is described below as the `ModuleRecord` type. +2. Using Async Module Resolution. +3. Maintaining [Lifetime Module Idempotency](https://tc39.github.io/ecma262/#sec-moduledeclarationinstantiation). + +### 1.1. Avoidance of the Reflective Module Record + +Note: The RMR is only a collection of direct pointers in a JIT'd environment. + +The RMR poses several interoperability and tooling problems: + +1. Importing from NCJS has no support for Named Imports. + - All core modules must stay as NCJS for the forseeable future due to interoperability constraints. + - As such, all core modules can only have a default export under the RMR. + - e.g. no `import { readfile } from 'fs';` +2. Inspecting the RMR is impossible + - Hooks to inspect only pointers is not possible. + - This renders APMs, tracing tools, test mocking tools, etc, impossible. +3. Wrapping the RMR is impossible. + - Wrapping only pointers is not possible. + - Most tooling vendors use this approach in current NCJS due to a lack of hooks. + - This, however, is not an option under the RMR. +4. Conditional imports & exports are impossible. + - (The RMR must be constructed at parse time.) + - This renders writing Native Modules in ESM impossible. +5. Avoids Sync Module Loading recursion problems. + - Stated in [15.2.1.17 Runtime Semantics: HostResolveImportedModule](https://tc39.github.io/ecma262/#sec-hostresolveimportedmodule) + +### 1.2. Avoidance of Async Module Resolution + +1. Allows modules to both use Module Mode and be `require()`'d as usual. + - Prevents any further interop issues related to timing. + - Allows existing modules to transition to NM2 while dependants can continue using NCJS to require them without any changes. + +Example of the unreasonable work required for dependants to transition to full +async loading: +(Required for `await` to be safe.) + +Before (synchronously): +```js +const m = require('module') +// do stuff with m... +exports.thing = m.something +``` + +After (asynchronously): +```js +// Must be done throughout the entirety of any downstream (user) NCJS dep tree +exports = async function() { + const m = await require('module') + // do stuff with m... + return { thing: m.something } +} +``` + +### 1.3. Avoidance of Lifetime Module Idempotency + +1. Allows REPLs to reload local code that may have been edited since a failure. + - This is also solvable (in a much less user-friendly way) by allowing access the module cache. ## 2. Related @@ -66,7 +123,7 @@ related syntax, and introduces: ### 3.1. Determining if source is an ES Module -A new file type will be used for ES Modules, `.mjs`. Files with this extension +A new file type will be used for Node v2.0 Modules, `.mjs`. Files with this extension will be treated using the loading semantics in this document, but retain a way to load existing extensions of `.json`, `.node`, and `.js`. The file type for JavaScript with IANA as an official file type needs to have this extensions @@ -74,7 +131,7 @@ added. This comes from browsers being unable to implement a new MIME for the differing parse goals. In order to make this change, contact the IESG. The `.mjs` file extension will be searched for prior to any `.js` file in any -algorithm that uses searching (`require` or `import`); e.g. once the Node +algorithm that uses searching (`require`); e.g. once the Node resolution algorithm reaches file expansion, `path + '.mjs'` would be searched prior to `path + '.js'` when performing `require(path)`. @@ -85,456 +142,66 @@ dependencies such as `require('foo/bar.js')`. As `bar.js` may move to `bar.mjs`. Since `bar.js` is not the listed entry point this was considered acceptable. -### 3.2. Import Path Parsing - -`import` will parse its path using URLs. As such, encoding and decoding will automatically be performed. This may affect file paths containing any of the following characters: `:`,`?`,`#`, or `%`. Details of the parsing algorithm are at the [WHATWG URL Spec](https://url.spec.whatwg.org/) - -* paths with `:` face multiple variations of path mutation -* paths with `%` in their path segments would be decoded -* paths with `?`, or `#` in their paths would face truncation of pathname - -### 3.3. Import Path Resolution - -The `import` resolution algorithm for node is as follows: - -* `request` should represent the requested path to load. -* `context_url` should represent the current script's absolute URL's directory - * for CLI usage, it is the absolute URL for the current working directory - -* if `request` parses via the [URL parser](https://url.spec.whatwg.org/#concept-url-parser) - * let `url` be the result -* else if `^[.]?[.]?[/]` prefixes `request` - * let `url` be the result of `new URL(request, context_url)` - * if `url` points to a directory - * let `url` be the result of searching the directory - * if `url` does not point to a file - * for the well known file extensions `extension` in `[ - .mjs', '.js', '.json', and '.node']` - * let `searchUrl` be a copy of `url` with `extension` added to the pathname - * if `searchUrl` points to a file - * let url be `searchUrl` - * break -* else - * let `url` be the result of searching node_modules using `request` and `context_url` - * NOTE: checks for escaping modules to `node_modules/` via `../` going to be added -* load url - * we should support `data:` and `file:` out of the box - * `file:` should use file type to determine dependency type (ES Module / JSON / C++ / NCJS). - * `data:` should interpret javascript MIME as ES Module. ES Modules share the MIME with NCJS, so we don't have enough data to differentiate, so just assume ES Module since that is [what the browser will assume](https://html.spec.whatwg.org/multipage/webappapis.html#hostresolveimportedmodule(referencingmodule,-specifier)). - - -`import` will perform non-exact searches on relative or -absolute paths, preserving the behavior in `require()`. This means that known -file extensions, and index files will be searched. - -In summary: - -```javascript -// looks at -// ./foo.mjs -// ./foo.js -// ./foo/package.json -// ./foo/index.mjs -// ./foo/index.js -// etc. -import './foo'; -``` +### 3.2. Importing Modules in NM2 -```javascript -// looks at -// /bar.mjs -// /bar.js -// /bar/package.json -// /bar/index.mjs -// /bar/index.js -// etc. -import '/bar'; -``` - -```javascript -// looks at: -// ./node_modules/baz.mjs -// ./node_modules/baz.js -// ./node_modules/baz/package.json -// ./node_modules/baz/index.mjs -// ./node_modules/baz/index.js -// and parent node_modules: -// ../node_modules/baz.mjs -// ../node_modules/baz.js -// ../node_modules/baz/package.json -// ../node_modules/baz/index.mjs -// ../node_modules/baz/index.js -// etc. -import 'baz'; -``` - -```javascript -// looks at: -// ./node_modules/abc/123.mjs -// ./node_modules/abc/123.js -// ./node_modules/abc/123/package.json -// ./node_modules/abc/123/index.mjs -// ./node_modules/abc/123/index.js -// and parent node_modules: -// ../node_modules/abc/123.mjs -// ../node_modules/abc/123.js -// ../node_modules/abc/123/package.json -// ../node_modules/abc/123/index.mjs -// ../node_modules/abc/123/index.js -// etc. -import 'abc/123'; -``` - -#### 3.3.1. Removal of non-local dependencies - -All of the following will not be supported by the `import` statement: - -* `$NODE_PATH` -* `$HOME/.node_modules` -* `$HOME/.node_libraries` -* `$PREFIX/lib/node` -* `module/../../outside-of-module` +Node v2.0 Modules do not expose `import` and `export`. +Instead, modules are "imported" as usual and as identically as possible as NCJS, +via `require()`. -Use local dependencies, and symbolic links as needed. +### 3.2.1 Function Wrap Approach -##### 3.3.1.1. How to support non-local dependencies +To achieve this, modules would be wrapped in a function like a regular script +file, but evaluated as a Module under the Module Parse Goal. However, that +wrapper _may_ be exported with an `export` statement under the hood. As such, +it _may_ look like the following: -Although not recommended, and in fact discouraged, there is a way to support -non-local dependencies. **USE THIS AT YOUR OWN DISCRETION**. - -Symlinks of `node_modules -> $HOME/.node_modules`, `node_modules/foo/ -> -$HOME/.node_modules/foo/`, etc. will continue to be supported. - -Adding a parent directory with `node_modules` symlinked will be an effective -strategy for recreating these functionalities. This will incur the known -problems with non-local dependencies, but now leaves the problems in the hands -of the user, allowing Node to give more clear insight to your modules by -reducing complexity. - -Given: - -```sh -/opt/local/myapp -``` - -Transform to: - -```sh -/opt/local/non-local-deps/myapp -/opt/local/non-local-deps/node_modules -> $PREFIX/lib/node (etc.) +```js +export default function (exports, require, module, __filename, __dirname) { + // user code inserted here +} ``` -And nest as many times as needed. +_If_ exporting under the hood is required, the resulting (hidden) promise +would likely be resolved synchronously on `require()`, given we are in +complete control and can guarantee its safety. -#### 3.3.2. Errors from new path behavior. - -In the case that an `import` is unable to find a module, Node should -make a **best effort** to see if `require` would have found the module and -print out where it was found, if `NODE_PATH` was used, if `HOME` was used, etc. - -#### 3.3.3. Shipping both ESM and NCJS - -When a `package.json` main is encountered, file extension searches are used to -provide a means to ship both ESM and NCJS variants of packages. If we have two -entry points `index.mjs` and `index.js` setting `"main":"./index"` in -`package.json` will make Node pick up either, depending on what is supported. - -##### 3.3.3.1. Excluding main - -Since `main` in `package.json` is entirely optional even inside of npm -packages, some people may prefer to exclude main entirely in the case of using -`./index` as that is still in the Node module search algorithm. - -### 3.4. ESM Evaluation +### 3.4. NM2 Evaluation #### 3.4.1. Environment Variables -ESM will not be bootstrapped with magic variables and will await upcoming specifications in order to provide such behaviors in a standard way. As such, the following variables are changed: +NM2 will be bootstrapped with standard NCJS "magic" variables. | Variable | Exists | Value | | ---- | ---- | --- | -| this | y | [`undefined`](https://tc39.github.io/ecma262/#sec-module-environment-records-getthisbinding) | -| arguments | n | | -| require | n | | -| module | n | | -| exports | n | | -| __filename | n | | -| __dirname | n | | - -Like normal scoping rules, if a variable does not exist in a scope, the outer scope is used to find the variable. Since ESM are always strict, errors may be thrown upon trying to use variables that do not exist globally when using ESM. - -##### 3.4.1.1. Workaround - -Although heavily advised against, you can have a NM module sibling for your ESM that can export these things: - -```js -// expose.js -module.exports = {__dirname}; -``` - -```js -// use.mjs -import expose from './expose.js'; -const {__dirname} = expose; -``` - -#### 3.4.2. Timing +| this | y | ? | +| arguments | y | same as regular scripts | +| require | y | same as regular scripts | +| module | y | same as regular scripts | +| exports | y | same as regular scripts | +| __filename | y | same as regular scripts | +| __dirname | y | same as regular scripts | -When loading ESM or using any form of `import`, the stack will unwind prior to performing any resolution or evaluation. +Like normal scoping rules, if a variable does not exist in a scope, the outer scope is used to find the variable. Since NM2 are always strict, errors may be thrown upon trying to use variables that do not exist globally when using NM2. -##### 3.4.2.1. Example +### 4. Caveats -```javascript -// entry.js -require('one.mjs'); -console.log('two'); -``` - -```javascript -// one.mjs -console.log('one'); -``` - -```sh -> node entry.js -two -one -``` - -### 3.5. Cross Module System Communication - -ESM and NCJS differ in many ways, as such they need a well defined means of converting between the two module types and systems. - -### 3.5.1. NCJS to ESM +This approach saves existing operability and tooling functionality in exchange +for a couple caveats, namely: -After *any* NCJS finishes evaluation, it will be placed into the same cache as ESM. -The value of what is placed in the cache will reflect a single default export pointing to the value of `module.exports` at the time evaluation ended. +1. `import` & `export` are unimplemented / unavailable to users +2. Top-level scope is unavailable. +3. Top-level async operations are **unsafe**. + - Runs into complex preemption and pseudo threading problems. + - As such, top-level `await` is unavailable. -Essentially after any NCJS completes evaluation: +### 5. As a Transitional stage -1. if there was an error, place the error in the ESM cache and return -2. let `export` be the value of `module.exports` -3. if there was an error, place the error in the ESM cache and return -4. create an ESM with `{default:export}` as its namespace -5. place the ESM in the ESM cache - -### 3.5.2. ESM to NCJS - -ESM are *never* placed in the NCJS cache (`require.cache`). - -If `require` resolves to an ESM source text, the following steps are taken. - -1. let `ret` be a newly created Promise -2. queue a load job on the event loop - 1. if loading completes normally resolve `ret` to the ESM Module Namespace - 2. if an error occurs reject `ret` -3. put a "fetching" entry in the ESM cache -4. return `ret` - -##### 5.4.1.1. Examples - -Given: - -```javascript -// NCJS.js -module.exports = { - default:'my-default', - thing:'stuff' -}; -``` +It is possible that this _could_ be a transitional approach _if_ ESM improves +to having good enough support for tooling and interop that it is favorable +to move to it fully. -You will grab `module.exports` when performing an ES import. - -```javascript -// es.mjs - -// grabs the namespace -import * as baz from './NCJS.js'; -// baz = { -// default => module.exports; -// } - -// grabs "default", aka module.exports directly -import foo from './NCJS.js'; -// foo = module.exports; - -// grabs "default", aka module.exports directly -import {default as bar} from './NCJS.js'; -// bar = module.exports; -``` - ------- - -Given: - -```javascript -// NCJS.js -module.exports = null; -``` - -You will grab `module.exports` when performing an ES import. - -```javascript -// es.mjs -import foo from './NCJS.js'; -// foo = null; - -import * as bar from './NCJS.js'; -// bar = {default=null}; -``` - ------- - -Given: - -```javascript -// NCJS.js -module.exports = function two() { - return 2; -}; -``` - -You will grab `module.exports` when performing an ES import. - -```javascript -// es.mjs -import foo from './NCJS.js'; -foo(); // 2 - -import * as bar from './NCJS.js'; -bar.name; // undefined -bar.default(); // 2 -bar(); // throws, bar is not a function -``` - ------- - -Given: - -```javascript -// NCJS.js -module.exports = Promise.resolve(3); -``` - -You will grab `module.exports` when performing an ES import. - -```javascript -// es.mjs -import foo from './NCJS.js'; -foo.then(console.log); // outputs 3 - -import * as bar from './NCJS.js'; -bar.default.then(console.log); // outputs 3 -bar.then(console.log); // throws, bar does not have a .then property -``` - -### 5.5. NCJS consuming ESM - -#### 5.5.1. default exports - -ESM only export named values. A "default" export is an export that uses -the property named `default`. - -##### 5.5.1.1. Examples - -Given: - -```javascript -// es.mjs -let foo = {bar:'my-default'}; -// note: -// this is a value -// it is not a binding like `export {foo}` -export default foo; -foo = null; -``` - -```javascript -// NCJS.js -const esm = require('./es'); -// esm ~= Promise => { -// default => result_from_evaluating_foo; -// } -esm.then( - es_namespace => { - console.log(es_namespace.default); - // {bar='my-default'} - } -) -``` - ------- - -Given: - -```javascript -// es.mjs -export let foo = {bar:'my-default'}; -export {foo as bar}; -export function f() {}; -export class c {}; -``` - -```javascript -// NCJS.js -const esm = require('./es'); -// esm ~= Promise => { -// foo=foo; -// bar=foo; -// f=f; -// c=c; -// } -``` - -### 5.6. Known Gotchas - -All of these gotchas relate to opt-in semantics and the fact that NCJS has a -dynamic loader while ESM has a static loader. - -No existing code will be affected. - -#### 5.6.1. ESM exports are read only - -The objects create by an ESM are [ModuleNamespace Objects][5]. - -These have `[[Set]]` be a no-op and are read only views of the exports of an ESM. Attempting to reassign any named export will not work, but assigning to -the properties of the exports follows normal rules. - -### 5.7. NCJS modules allow mutation of imported modules - -NCJS modules have allowed mutation on imported modules. When ES modules are -integrating against NCJS systems like Grunt, it may be necessary to mutate a -`module.exports`. - -Remember that `module.exports` from NCJS is directly available under `default` -for `import`. This means that if you use: - -```javascript -import * as namespace from 'grunt'; -``` - -According to ES `*` grabs the namespace directly whose properties will be -read-only. - -However, doing: - -```javascript -import grunt_default from 'grunt'; -``` - -Grabs the `default` which is exactly what `module.exports` is, and all the -properties will be mutable. - -#### 5.7.1. ES will not honor reassigning `module.exports` after evaluation - -Since we need a consistent time to snapshot the `module.exports` of a NCJS -module. We will execute it immediately after evaluation. Code such as: - -```javascript -// bad-NCJS.js -module.exports = 123; -setTimeout(_ => module.exports = null); -``` +Two options may be available in that case to detect an actual ESM: -Will not see `module.exports` change to `null`. All ES module `import`s of the -module will always see `{default=123}`. +1. Another file extension (such as `.esm`). +2. Detection of top-level `import` / `export` / `await` statements. + - Similar or identical to the Unambiguous JavaScript Grammar approach