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

Commit 7d40f06

Browse files
guybedfordMylesBorins
authored andcommitted
esm: Revert "esm: Remove --loader."
This reverts commit 1b0695b.
1 parent d4e2866 commit 7d40f06

40 files changed

+468
-1
lines changed

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ module.exports = {
4141
'test/es-module/test-esm-type-flag.js',
4242
'test/es-module/test-esm-type-flag-alias.js',
4343
'*.mjs',
44+
'test/es-module/test-esm-example-loader.js',
4445
],
4546
parserOptions: { sourceType: 'module' },
4647
},

doc/api/cli.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,13 @@ default) is not firewall-protected.**
268268

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

271+
### `--loader=file`
272+
<!-- YAML
273+
added: v9.0.0
274+
-->
275+
276+
Specify the `file` of the custom [experimental ECMAScript Module][] loader.
277+
271278
### `--max-http-header-size=size`
272279
<!-- YAML
273280
added: v11.6.0
@@ -716,6 +723,7 @@ Node.js options that are allowed are:
716723
- `--inspect`
717724
- `--inspect-brk`
718725
- `--inspect-port`
726+
- `--loader`
719727
- `--max-http-header-size`
720728
- `--napi-modules`
721729
- `--no-deprecation`
@@ -903,6 +911,7 @@ greater than `4` (its current default value). For more information, see the
903911
[debugger]: debugger.html
904912
[debugging security implications]: https://nodejs.org/en/docs/guides/debugging-getting-started/#security-implications
905913
[emit_warning]: process.html#process_process_emitwarning_warning_type_code_ctor
914+
[experimental ECMAScript Module]: esm.html#esm_loader_hooks
906915
[libuv threadpool documentation]: http://docs.libuv.org/en/latest/threadpool.html
907916
[remote code execution]: https://www.owasp.org/index.php/Code_Injection
908917
[secureProtocol]: tls.html#tls_tls_createsecurecontext_options

doc/api/esm.md

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,128 @@ READ_PACKAGE_JSON(_packageURL_)
289289
> 1. Throw an _Invalid Package Configuration_ error.
290290
> 1. Return the parsed JSON source of the file at _pjsonURL_.
291291
292+
## Experimental Loader hooks
293+
294+
**Note: This API is currently being redesigned and will still change.**.
295+
296+
<!-- type=misc -->
297+
298+
To customize the default module resolution, loader hooks can optionally be
299+
provided via a `--loader ./loader-name.mjs` argument to Node.js.
300+
301+
When hooks are used they only apply to ES module loading and not to any
302+
CommonJS modules loaded.
303+
304+
### Resolve hook
305+
306+
The resolve hook returns the resolved file URL and module format for a
307+
given module specifier and parent file URL:
308+
309+
```js
310+
const baseURL = new URL('file://');
311+
baseURL.pathname = `${process.cwd()}/`;
312+
313+
export async function resolve(specifier,
314+
parentModuleURL = baseURL,
315+
defaultResolver) {
316+
return {
317+
url: new URL(specifier, parentModuleURL).href,
318+
format: 'esm'
319+
};
320+
}
321+
```
322+
323+
The `parentModuleURL` is provided as `undefined` when performing main Node.js
324+
load itself.
325+
326+
The default Node.js ES module resolution function is provided as a third
327+
argument to the resolver for easy compatibility workflows.
328+
329+
In addition to returning the resolved file URL value, the resolve hook also
330+
returns a `format` property specifying the module format of the resolved
331+
module. This can be one of the following:
332+
333+
| `format` | Description |
334+
| --- | --- |
335+
| `'module'` | Load a standard JavaScript module |
336+
| `'commonjs'` | Load a Node.js CommonJS module |
337+
| `'builtin'` | Load a Node.js builtin module |
338+
| `'dynamic'` | Use a [dynamic instantiate hook][] |
339+
340+
For example, a dummy loader to load JavaScript restricted to browser resolution
341+
rules with only JS file extension and Node.js builtin modules support could
342+
be written:
343+
344+
```js
345+
import path from 'path';
346+
import process from 'process';
347+
import Module from 'module';
348+
349+
const builtins = Module.builtinModules;
350+
const JS_EXTENSIONS = new Set(['.js', '.mjs']);
351+
352+
const baseURL = new URL('file://');
353+
baseURL.pathname = `${process.cwd()}/`;
354+
355+
export function resolve(specifier, parentModuleURL = baseURL, defaultResolve) {
356+
if (builtins.includes(specifier)) {
357+
return {
358+
url: specifier,
359+
format: 'builtin'
360+
};
361+
}
362+
if (/^\.{0,2}[/]/.test(specifier) !== true && !specifier.startsWith('file:')) {
363+
// For node_modules support:
364+
// return defaultResolve(specifier, parentModuleURL);
365+
throw new Error(
366+
`imports must begin with '/', './', or '../'; '${specifier}' does not`);
367+
}
368+
const resolved = new URL(specifier, parentModuleURL);
369+
const ext = path.extname(resolved.pathname);
370+
if (!JS_EXTENSIONS.has(ext)) {
371+
throw new Error(
372+
`Cannot load file with non-JavaScript file extension ${ext}.`);
373+
}
374+
return {
375+
url: resolved.href,
376+
format: 'esm'
377+
};
378+
}
379+
```
380+
381+
With this loader, running:
382+
383+
```console
384+
NODE_OPTIONS='--experimental-modules --loader ./custom-loader.mjs' node x.js
385+
```
386+
387+
would load the module `x.js` as an ES module with relative resolution support
388+
(with `node_modules` loading skipped in this example).
389+
390+
### Dynamic instantiate hook
391+
392+
To create a custom dynamic module that doesn't correspond to one of the
393+
existing `format` interpretations, the `dynamicInstantiate` hook can be used.
394+
This hook is called only for modules that return `format: 'dynamic'` from
395+
the `resolve` hook.
396+
397+
```js
398+
export async function dynamicInstantiate(url) {
399+
return {
400+
exports: ['customExportName'],
401+
execute: (exports) => {
402+
// Get and set functions provided for pre-allocated export names
403+
exports.customExportName.set('value');
404+
}
405+
};
406+
}
407+
```
408+
409+
With the list of module exports provided upfront, the `execute` function will
410+
then be called at the exact point of module evaluation order for that module
411+
in the import tree.
412+
292413
[Node.js EP for ES Modules]: https://github.com/nodejs/node-eps/blob/master/002-es-modules.md
414+
[dynamic instantiate hook]: #esm_dynamic_instantiate_hook
293415
[`module.createRequireFromPath()`]: modules.html#modules_module_createrequirefrompath_filename
294416
[ESM Minimal Kernel]: https://github.com/nodejs/modules/blob/master/doc/plan-for-new-modules-implementation.md

lib/internal/process/esm_loader.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ if (type && type !== 'commonjs' && type !== 'module')
1414
exports.typeFlag = type;
1515

1616
const { Loader } = require('internal/modules/esm/loader');
17+
const { pathToFileURL } = require('internal/url');
1718
const {
1819
wrapToModuleMap,
1920
} = require('internal/vm/source_text_module');
@@ -44,8 +45,15 @@ exports.loaderPromise = new Promise((resolve) => loaderResolve = resolve);
4445
exports.ESMLoader = undefined;
4546

4647
exports.initializeLoader = function(cwd, userLoader) {
47-
const ESMLoader = new Loader();
48+
let ESMLoader = new Loader();
4849
const loaderPromise = (async () => {
50+
if (userLoader) {
51+
const hooks = await ESMLoader.import(
52+
userLoader, pathToFileURL(`${cwd}/`).href);
53+
ESMLoader = new Loader();
54+
ESMLoader.hook(hooks);
55+
exports.ESMLoader = ESMLoader;
56+
}
4957
return ESMLoader;
5058
})();
5159
loaderResolve(loaderPromise);

src/node_options.cc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ void PerIsolateOptions::CheckOptions(std::vector<std::string>* errors) {
9595
}
9696

9797
void EnvironmentOptions::CheckOptions(std::vector<std::string>* errors) {
98+
if (!userland_loader.empty() && !experimental_modules) {
99+
errors->push_back("--loader requires --experimental-modules be enabled");
100+
}
101+
98102
if (syntax_check_only && has_eval_string) {
99103
errors->push_back("either --check or --eval can be used, not both");
100104
}
@@ -236,6 +240,11 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
236240
"(default: llhttp).",
237241
&EnvironmentOptions::http_parser,
238242
kAllowedInEnvironment);
243+
AddOption("--loader",
244+
"(with --experimental-modules) use the specified file as a "
245+
"custom loader",
246+
&EnvironmentOptions::userland_loader,
247+
kAllowedInEnvironment);
239248
AddOption("--no-deprecation",
240249
"silence deprecation warnings",
241250
&EnvironmentOptions::no_deprecation,

src/node_options.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ class EnvironmentOptions : public Options {
105105
bool trace_deprecation = false;
106106
bool trace_sync_io = false;
107107
bool trace_warnings = false;
108+
std::string userland_loader;
108109

109110
bool syntax_check_only = false;
110111
bool has_eval_string = false;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/example-loader.mjs
2+
/* eslint-disable node-core/required-modules */
3+
import assert from 'assert';
4+
import ok from '../fixtures/es-modules/test-esm-ok.mjs';
5+
6+
assert(ok);
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/loader-with-dep.mjs
2+
/* eslint-disable node-core/required-modules */
3+
import '../fixtures/es-modules/test-esm-ok.mjs';
4+
5+
// We just test that this module doesn't fail loading
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/loader-invalid-format.mjs
2+
/* eslint-disable node-core/required-modules */
3+
import { expectsError, mustCall } from '../common/index.mjs';
4+
import assert from 'assert';
5+
6+
import('../fixtures/es-modules/test-esm-ok.mjs')
7+
.then(assert.fail, expectsError({
8+
code: 'ERR_INVALID_RETURN_PROPERTY_VALUE',
9+
message: 'Expected string to be returned for the "format" from the ' +
10+
'"loader resolve" function but got type undefined.'
11+
}))
12+
.then(mustCall());
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/loader-invalid-url.mjs
2+
/* eslint-disable node-core/required-modules */
3+
4+
import { expectsError, mustCall } from '../common/index.mjs';
5+
import assert from 'assert';
6+
7+
import('../fixtures/es-modules/test-esm-ok.mjs')
8+
.then(assert.fail, expectsError({
9+
code: 'ERR_INVALID_RETURN_PROPERTY',
10+
message: 'Expected a valid url to be returned for the "url" from the ' +
11+
'"loader resolve" function but got ' +
12+
'../fixtures/es-modules/test-esm-ok.mjs.'
13+
}))
14+
.then(mustCall());

0 commit comments

Comments
 (0)