Skip to content
This repository has been archived by the owner on Apr 16, 2020. It is now read-only.

Commit

Permalink
esm: Revert "esm: Remove --loader."
Browse files Browse the repository at this point in the history
This reverts commit 1b0695b.
  • Loading branch information
guybedford authored and MylesBorins committed Mar 10, 2019
1 parent a9887b8 commit c7e5fe0
Show file tree
Hide file tree
Showing 41 changed files with 461 additions and 1 deletion.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ module.exports = {
'test/es-module/test-esm-type-flag.js',
'test/es-module/test-esm-type-flag-alias.js',
'*.mjs',
'test/es-module/test-esm-example-loader.js',
],
parserOptions: { sourceType: 'module' },
},
Expand Down
9 changes: 9 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,13 @@ default) is not firewall-protected.**

See the [debugging security implications][] section for more information.

### `--loader=file`
<!-- YAML
added: v9.0.0
-->

Specify the `file` of the custom [experimental ECMAScript Module][] loader.

### `--max-http-header-size=size`
<!-- YAML
added: v11.6.0
Expand Down Expand Up @@ -716,6 +723,7 @@ Node.js options that are allowed are:
- `--inspect`
- `--inspect-brk`
- `--inspect-port`
- `--loader`
- `--max-http-header-size`
- `--napi-modules`
- `--no-deprecation`
Expand Down Expand Up @@ -903,6 +911,7 @@ greater than `4` (its current default value). For more information, see the
[debugger]: debugger.html
[debugging security implications]: https://nodejs.org/en/docs/guides/debugging-getting-started/#security-implications
[emit_warning]: process.html#process_process_emitwarning_warning_type_code_ctor
[experimental ECMAScript Module]: esm.html#esm_experimental_loader_hooks
[libuv threadpool documentation]: http://docs.libuv.org/en/latest/threadpool.html
[remote code execution]: https://www.owasp.org/index.php/Code_Injection
[secureProtocol]: tls.html#tls_tls_createsecurecontext_options
122 changes: 122 additions & 0 deletions doc/api/esm.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,128 @@ READ_PACKAGE_JSON(_packageURL_)
> 1. Throw an _Invalid Package Configuration_ error.
> 1. Return the parsed JSON source of the file at _pjsonURL_.
## Experimental Loader hooks
**Note: This API is currently being redesigned and will still change.**.
<!-- type=misc -->
To customize the default module resolution, loader hooks can optionally be
provided via a `--loader ./loader-name.mjs` argument to Node.js.
When hooks are used they only apply to ES module loading and not to any
CommonJS modules loaded.
### Resolve hook
The resolve hook returns the resolved file URL and module format for a
given module specifier and parent file URL:
```js
const baseURL = new URL('file://');
baseURL.pathname = `${process.cwd()}/`;

export async function resolve(specifier,
parentModuleURL = baseURL,
defaultResolver) {
return {
url: new URL(specifier, parentModuleURL).href,
format: 'esm'
};
}
```
The `parentModuleURL` is provided as `undefined` when performing main Node.js
load itself.
The default Node.js ES module resolution function is provided as a third
argument to the resolver for easy compatibility workflows.
In addition to returning the resolved file URL value, the resolve hook also
returns a `format` property specifying the module format of the resolved
module. This can be one of the following:
| `format` | Description |
| --- | --- |
| `'module'` | Load a standard JavaScript module |
| `'commonjs'` | Load a Node.js CommonJS module |
| `'builtin'` | Load a Node.js builtin module |
| `'dynamic'` | Use a [dynamic instantiate hook][] |
For example, a dummy loader to load JavaScript restricted to browser resolution
rules with only JS file extension and Node.js builtin modules support could
be written:
```js
import path from 'path';
import process from 'process';
import Module from 'module';

const builtins = Module.builtinModules;
const JS_EXTENSIONS = new Set(['.js', '.mjs']);

const baseURL = new URL('file://');
baseURL.pathname = `${process.cwd()}/`;

export function resolve(specifier, parentModuleURL = baseURL, defaultResolve) {
if (builtins.includes(specifier)) {
return {
url: specifier,
format: 'builtin'
};
}
if (/^\.{0,2}[/]/.test(specifier) !== true && !specifier.startsWith('file:')) {
// For node_modules support:
// return defaultResolve(specifier, parentModuleURL);
throw new Error(
`imports must begin with '/', './', or '../'; '${specifier}' does not`);
}
const resolved = new URL(specifier, parentModuleURL);
const ext = path.extname(resolved.pathname);
if (!JS_EXTENSIONS.has(ext)) {
throw new Error(
`Cannot load file with non-JavaScript file extension ${ext}.`);
}
return {
url: resolved.href,
format: 'esm'
};
}
```
With this loader, running:
```console
NODE_OPTIONS='--experimental-modules --loader ./custom-loader.mjs' node x.js
```
would load the module `x.js` as an ES module with relative resolution support
(with `node_modules` loading skipped in this example).
### Dynamic instantiate hook
To create a custom dynamic module that doesn't correspond to one of the
existing `format` interpretations, the `dynamicInstantiate` hook can be used.
This hook is called only for modules that return `format: 'dynamic'` from
the `resolve` hook.
```js
export async function dynamicInstantiate(url) {
return {
exports: ['customExportName'],
execute: (exports) => {
// Get and set functions provided for pre-allocated export names
exports.customExportName.set('value');
}
};
}
```
With the list of module exports provided upfront, the `execute` function will
then be called at the exact point of module evaluation order for that module
in the import tree.
[Node.js EP for ES Modules]: https://github.com/nodejs/node-eps/blob/master/002-es-modules.md
[dynamic instantiate hook]: #esm_dynamic_instantiate_hook
[`module.createRequireFromPath()`]: modules.html#modules_module_createrequirefrompath_filename
[ESM Minimal Kernel]: https://github.com/nodejs/modules/blob/master/doc/plan-for-new-modules-implementation.md
12 changes: 11 additions & 1 deletion lib/internal/process/esm_loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ const {
ERR_INVALID_TYPE_FLAG,
ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING,
} = require('internal/errors').codes;
const { emitExperimentalWarning } = require('internal/util');

const type = require('internal/options').getOptionValue('--type');
if (type && type !== 'commonjs' && type !== 'module')
throw new ERR_INVALID_TYPE_FLAG(type);
exports.typeFlag = type;

const { Loader } = require('internal/modules/esm/loader');
const { pathToFileURL } = require('internal/url');
const {
wrapToModuleMap,
} = require('internal/vm/source_text_module');
Expand Down Expand Up @@ -44,8 +46,16 @@ exports.loaderPromise = new Promise((resolve) => loaderResolve = resolve);
exports.ESMLoader = undefined;

exports.initializeLoader = function(cwd, userLoader) {
const ESMLoader = new Loader();
let ESMLoader = new Loader();
const loaderPromise = (async () => {
if (userLoader) {
emitExperimentalWarning('--loader');
const hooks = await ESMLoader.import(
userLoader, pathToFileURL(`${cwd}/`).href);
ESMLoader = new Loader();
ESMLoader.hook(hooks);
exports.ESMLoader = ESMLoader;
}
return ESMLoader;
})();
loaderResolve(loaderPromise);
Expand Down
9 changes: 9 additions & 0 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ void PerIsolateOptions::CheckOptions(std::vector<std::string>* errors) {
}

void EnvironmentOptions::CheckOptions(std::vector<std::string>* errors) {
if (!userland_loader.empty() && !experimental_modules) {
errors->push_back("--loader requires --experimental-modules be enabled");
}

if (syntax_check_only && has_eval_string) {
errors->push_back("either --check or --eval can be used, not both");
}
Expand Down Expand Up @@ -242,6 +246,11 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
"(default: llhttp).",
&EnvironmentOptions::http_parser,
kAllowedInEnvironment);
AddOption("--loader",
"(with --experimental-modules) use the specified file as a "
"custom loader",
&EnvironmentOptions::userland_loader,
kAllowedInEnvironment);
AddOption("--no-deprecation",
"silence deprecation warnings",
&EnvironmentOptions::no_deprecation,
Expand Down
1 change: 1 addition & 0 deletions src/node_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ class EnvironmentOptions : public Options {
bool trace_deprecation = false;
bool trace_sync_io = false;
bool trace_warnings = false;
std::string userland_loader;

bool syntax_check_only = false;
bool has_eval_string = false;
Expand Down
6 changes: 6 additions & 0 deletions test/es-module/test-esm-example-loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/example-loader.mjs
/* eslint-disable node-core/required-modules */
import assert from 'assert';
import ok from '../fixtures/es-modules/test-esm-ok.mjs';

assert(ok);
5 changes: 5 additions & 0 deletions test/es-module/test-esm-loader-dependency.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/loader-with-dep.mjs
/* eslint-disable node-core/required-modules */
import '../fixtures/es-modules/test-esm-ok.mjs';

// We just test that this module doesn't fail loading
12 changes: 12 additions & 0 deletions test/es-module/test-esm-loader-invalid-format.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/loader-invalid-format.mjs
/* eslint-disable node-core/required-modules */
import { expectsError, mustCall } from '../common/index.mjs';
import assert from 'assert';

import('../fixtures/es-modules/test-esm-ok.mjs')
.then(assert.fail, expectsError({
code: 'ERR_INVALID_RETURN_PROPERTY_VALUE',
message: 'Expected string to be returned for the "format" from the ' +
'"loader resolve" function but got type undefined.'
}))
.then(mustCall());
14 changes: 14 additions & 0 deletions test/es-module/test-esm-loader-invalid-url.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/loader-invalid-url.mjs
/* eslint-disable node-core/required-modules */

import { expectsError, mustCall } from '../common/index.mjs';
import assert from 'assert';

import('../fixtures/es-modules/test-esm-ok.mjs')
.then(assert.fail, expectsError({
code: 'ERR_INVALID_RETURN_PROPERTY',
message: 'Expected a valid url to be returned for the "url" from the ' +
'"loader resolve" function but got ' +
'../fixtures/es-modules/test-esm-ok.mjs.'
}))
.then(mustCall());
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/missing-dynamic-instantiate-hook.mjs
/* eslint-disable node-core/required-modules */

import { expectsError } from '../common/index.mjs';

import('test').catch(expectsError({
code: 'ERR_MISSING_DYNAMIC_INSTANTIATE_HOOK',
message: 'The ES Module loader may not return a format of \'dynamic\' ' +
'when no dynamicInstantiate function was provided'
}));
9 changes: 9 additions & 0 deletions test/es-module/test-esm-named-exports.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/builtin-named-exports-loader.mjs
/* eslint-disable node-core/required-modules */
import '../common/index.mjs';
import { readFile } from 'fs';
import assert from 'assert';
import ok from '../fixtures/es-modules/test-esm-ok.mjs';

assert(ok);
assert(readFile);
3 changes: 3 additions & 0 deletions test/es-module/test-esm-preserve-symlinks-not-found-plain.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/not-found-assert-loader.mjs
/* eslint-disable node-core/required-modules */
import './not-found.js';
3 changes: 3 additions & 0 deletions test/es-module/test-esm-preserve-symlinks-not-found.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/not-found-assert-loader.mjs
/* eslint-disable node-core/required-modules */
import './not-found.mjs';
8 changes: 8 additions & 0 deletions test/es-module/test-esm-resolve-hook.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/js-loader.mjs
/* eslint-disable node-core/required-modules */
import { namedExport } from '../fixtures/es-module-loaders/js-as-esm.js';
import assert from 'assert';
import ok from '../fixtures/es-modules/test-esm-ok.mjs';

assert(ok);
assert(namedExport);
11 changes: 11 additions & 0 deletions test/es-module/test-esm-shared-loader-dep.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/loader-shared-dep.mjs
/* eslint-disable node-core/required-modules */
import { createRequire } from '../common/index.mjs';

import assert from 'assert';
import '../fixtures/es-modules/test-esm-ok.mjs';

const require = createRequire(import.meta.url);
const dep = require('../fixtures/es-module-loaders/loader-dep.js');

assert.strictEqual(dep.format, 'module');
16 changes: 16 additions & 0 deletions test/es-module/test-esm-throw-undefined.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Flags: --experimental-modules
/* eslint-disable node-core/required-modules */

import '../common/index.mjs';
import assert from 'assert';

async function doTest() {
await assert.rejects(
async () => {
await import('../fixtures/es-module-loaders/throw-undefined.mjs');
},
(e) => e === undefined
);
}

doTest();
24 changes: 24 additions & 0 deletions test/fixtures/es-module-loaders/builtin-named-exports-loader.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import module from 'module';

export function dynamicInstantiate(url) {
const builtinInstance = module._load(url.substr(5));
const builtinExports = ['default', ...Object.keys(builtinInstance)];
return {
exports: builtinExports,
execute: exports => {
for (let name of builtinExports)
exports[name].set(builtinInstance[name]);
exports.default.set(builtinInstance);
}
};
}

export function resolve(specifier, base, defaultResolver) {
if (module.builtinModules.includes(specifier)) {
return {
url: `node:${specifier}`,
format: 'dynamic'
};
}
return defaultResolver(specifier, base);
}
34 changes: 34 additions & 0 deletions test/fixtures/es-module-loaders/example-loader.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import url from 'url';
import path from 'path';
import process from 'process';
import { builtinModules } from 'module';

const JS_EXTENSIONS = new Set(['.js', '.mjs']);

const baseURL = new url.URL('file://');
baseURL.pathname = process.cwd() + '/';

export function resolve(specifier, parentModuleURL = baseURL /*, defaultResolve */) {
if (builtinModules.includes(specifier)) {
return {
url: specifier,
format: 'builtin'
};
}
if (/^\.{0,2}[/]/.test(specifier) !== true && !specifier.startsWith('file:')) {
// For node_modules support:
// return defaultResolve(specifier, parentModuleURL);
throw new Error(
`imports must begin with '/', './', or '../'; '${specifier}' does not`);
}
const resolved = new url.URL(specifier, parentModuleURL);
const ext = path.extname(resolved.pathname);
if (!JS_EXTENSIONS.has(ext)) {
throw new Error(
`Cannot load file with non-JavaScript file extension ${ext}.`);
}
return {
url: resolved.href,
format: 'module'
};
}
Loading

0 comments on commit c7e5fe0

Please sign in to comment.