![]() |
旅行中的长颈鹿 · KNIME案例(526)Python图矩阵 ...· 9 月前 · |
![]() |
谈吐大方的山楂 · Optimizer Hints | ...· 9 月前 · |
![]() |
苦闷的猕猴桃 · python tkinter ...· 1 年前 · |
![]() |
严肃的苦咖啡 · python实现栈,实现push(),pop ...· 1 年前 · |
![]() |
打盹的大熊猫 · 【Python】使用pdfkit将str或者 ...· 1 年前 · |
resolve(specifier, context, nextResolve)
load(url, context, nextLoad)
globalPreload()
resolve(specifier, context, nextResolve)
load(url, context, nextLoad)
globalPreload()
Loader hooks are executed off the main thread.
v18.6.0, v16.17.0Add support for chaining loaders.
v17.1.0, v16.14.0Add support for import assertions.
v17.0.0, v16.12.0
Consolidate loader hooks, removed
getFormat
,
getSource
,
transformSource
, and
getGlobalPreloadCode
hooks added
load
and
globalPreload
hooks allowed returning
format
from either
resolve
or
load
hooks.
Unflag Top-Level Await.
v15.3.0, v14.17.0, v12.22.0Stabilize modules implementation.
v14.13.0, v12.20.0Support for detection of CommonJS named exports.
v14.0.0, v13.14.0, v12.20.0Remove experimental modules warning.
v13.2.0, v12.17.0Loading ECMAScript modules no longer requires a command-line flag.
v12.0.0
Add support for ES modules using
.js
file extension via
package.json
"type"
field.
Added in: v8.5.0
ECMAScript modules are
the official standard format
to package JavaScript
code for reuse. Modules are defined using a variety of
import
and
export
statements.
The following example of an ES module exports a function:
// addTwo.mjs
function addTwo(num) {
return num + 2;
export { addTwo };
The following example of an ES module imports the function from
addTwo.mjs
:
// app.mjs
import { addTwo } from './addTwo.mjs';
// Prints: 6
console.log(addTwo(4));
Node.js fully supports ECMAScript modules as they are currently specified and provides interoperability between them and its original module format, CommonJS .
Node.js has two module systems: CommonJS modules and ECMAScript modules.
Authors can tell Node.js to use the ECMAScript modules loader
via the
.mjs
file extension, the
package.json
"type"
field, or the
--input-type
flag. Outside of those cases, Node.js will use the CommonJS
module loader. See
Determining module system
for more details.
import
Specifiers
#
The
specifier
of an
import
statement is the string after the
from
keyword,
e.g.
'node:path'
in
import { sep } from 'node:path'
. Specifiers are also
used in
export from
statements, and as the argument to an
import()
expression.
There are three types of specifiers:
Relative specifiers
like
'./startup.js'
or
'../config.mjs'
. They refer
to a path relative to the location of the importing file.
The file extension
is always necessary for these.
Bare specifiers
like
'some-package'
or
'some-package/shuffle'
. They can
refer to the main entry point of a package by the package name, or a
specific feature module within a package prefixed by the package name as per
the examples respectively.
Including the file extension is only necessary
for packages without an
"exports"
field.
Absolute specifiers
like
'file:///opt/nodejs/config.js'
. They refer
directly and explicitly to a full path.
Bare specifier resolutions are handled by the Node.js module resolution algorithm . All other specifier resolutions are always only resolved with the standard relative URL resolution semantics.
Like in CommonJS, module files within packages can be accessed by appending a
path to the package name unless the package's
package.json
contains an
"exports"
field, in which case files within packages can only be accessed
via the paths defined in
"exports"
.
For details on these package resolution rules that apply to bare specifiers in the Node.js module resolution, see the packages documentation .
A file extension must be provided when using the
import
keyword to resolve
relative or absolute specifiers. Directory indexes (e.g.
'./startup/index.js'
)
must also be fully specified.
This behavior matches how
import
behaves in browser environments, assuming a
typically configured server.
ES modules are resolved and cached as URLs. This means that special characters
must be
percent-encoded
, such as
#
with
%23
and
?
with
%3F
.
file:
,
node:
, and
data:
URL schemes are supported. A specifier like
'https://example.com/app.js'
is not supported natively in Node.js unless using
a
custom HTTPS loader
.
file:
URLs
#
Modules are loaded multiple times if the
import
specifier used to resolve
them has a different query or fragment.
import './foo.mjs?query=1'; // loads ./foo.mjs with query of "?query=1"
import './foo.mjs?query=2'; // loads ./foo.mjs with query of "?query=2"
The volume root may be referenced via
/
,
//
, or
file:///
. Given the
differences between
URL
and path resolution (such as percent encoding
details), it is recommended to use
url.pathToFileURL
when importing a path.
data:
imports
#
data:
URLs
are supported for importing with the following MIME types:
text/javascript
for ES modules
application/json
for JSON
application/wasm
for Wasm
import 'data:text/javascript,console.log("hello!");';
import _ from 'data:application/json,"world!"' assert { type: 'json' };
data:
URLs only resolve
bare specifiers
for builtin modules
and
absolute specifiers
. Resolving
relative specifiers
does not work because
data:
is not a
special scheme
. For example, attempting to load
./foo
from
data:text/javascript,import "./foo";
fails to resolve because there
is no concept of relative resolution for
data:
URLs.
node:
imports
#
Added
node:
import support to
require(...)
.
Added in: v14.13.1, v12.20.0
node:
URLs are supported as an alternative means to load Node.js builtin
modules. This URL scheme allows for builtin modules to be referenced by valid
absolute URL strings.
import fs from 'node:fs/promises';
The Import Assertions proposal adds an inline syntax for module import statements to pass on more information alongside the module specifier.
import fooData from './foo.json' assert { type: 'json' };
const { default: barData } =
await import('./bar.json', { assert: { type: 'json' } });
Node.js supports the following
type
values, for which the assertion is
mandatory:
Assertion
type
|
Needed for |
---|---|
'json'
|
JSON modules |
Core modules
provide named exports of their public API. A
default export is also provided which is the value of the CommonJS exports.
The default export can be used for, among other things, modifying the named
exports. Named exports of builtin modules are updated only by calling
module.syncBuiltinESMExports()
.
import EventEmitter from 'node:events';
const e = new EventEmitter();
import { readFile } from 'node:fs';
readFile('./foo.txt', (err, source) => {
if (err) {
console.error(err);
} else {
console.log(source);
});
import fs, { readFileSync } from 'node:fs';
import { syncBuiltinESMExports } from 'node:module';
import { Buffer } from 'node:buffer';
fs.readFileSync = () => Buffer.from('Hello, ESM');
syncBuiltinESMExports();
fs.readFileSync === readFileSync;
import()
expressions
#
Dynamic
import()
is supported in both CommonJS and ES modules. In CommonJS
modules it can be used to load ES modules.
import.meta
#
The
import.meta
meta property is an
Object
that contains the following
properties.
import.meta.url
#
file:
URL of the module.
This is defined exactly the same as it is in browsers providing the URL of the current module file.
This enables useful patterns such as relative file loading:
import { readFileSync } from 'node:fs';
const buffer = readFileSync(new URL('./data.proto', import.meta.url));
import.meta.resolve(specifier[, parent])
#
This feature is only available with the
--experimental-import-meta-resolve
command flag enabled.
specifier
<string>
The module specifier to resolve relative to
parent
.
parent
<string>
|
<URL>
The absolute parent module URL to resolve from. If none
is specified, the value of
import.meta.url
is used as the default.
Provides a module-relative resolution function scoped to each module, returning the URL string. In alignment with browser behavior, this now returns synchronously.
Caveat
This can result in synchronous file-system operations, which
can impact performance similarly to
require.resolve
.
const dependencyAsset = import.meta.resolve('component-lib/asset.css');
import.meta.resolve
also accepts a second argument which is the parent module
from which to resolve:
import.meta.resolve('./dep', import.meta.url);
import
statements
#
An
import
statement can reference an ES module or a CommonJS module.
import
statements are permitted only in ES modules, but dynamic
import()
expressions are supported in CommonJS for loading ES modules.
When importing
CommonJS modules
, the
module.exports
object is provided as the default export. Named exports may be
available, provided by static analysis as a convenience for better ecosystem
compatibility.
require
#
The CommonJS module
require
always treats the files it references as CommonJS.
Using
require
to load an ES module is not supported because ES modules have
asynchronous execution. Instead, use
import()
to load an ES module
from a CommonJS module.
CommonJS modules consist of a
module.exports
object which can be of any type.
When importing a CommonJS module, it can be reliably imported using the ES module default import or its corresponding sugar syntax:
import { default as cjs } from 'cjs';
// The following import statement is "syntax sugar" (equivalent but sweeter)
// for `{ default as cjsSugar }` in the above import statement:
import cjsSugar from 'cjs';
console.log(cjs);
console.log(cjs === cjsSugar);
// Prints:
// <module.exports>
// true
The ECMAScript Module Namespace representation of a CommonJS module is always
a namespace with a
default
export key pointing to the CommonJS
module.exports
value.
This Module Namespace Exotic Object can be directly observed either when using
import * as m from 'cjs'
or a dynamic import:
import * as m from 'cjs';
console.log(m);
console.log(m === await import('cjs'));
// Prints:
// [Module] { default: <module.exports> }
// true
For better compatibility with existing usage in the JS ecosystem, Node.js in addition attempts to determine the CommonJS named exports of every imported CommonJS module to provide them as separate ES module exports using a static analysis process.
For example, consider a CommonJS module written:
// cjs.cjs
exports.name = 'exported';
The preceding module supports named imports in ES modules:
import { name } from './cjs.cjs';
console.log(name);
// Prints: 'exported'
import cjs from './cjs.cjs';
console.log(cjs);
// Prints: { name: 'exported' }
import * as m from './cjs.cjs';
console.log(m);
// Prints: [Module] { default: { name: 'exported' }, name: 'exported' }
As can be seen from the last example of the Module Namespace Exotic Object being
logged, the
name
export is copied off of the
module.exports
object and set
directly on the ES module namespace when the module is imported.
Live binding updates or new exports added to
module.exports
are not detected
for these named exports.
The detection of named exports is based on common syntax patterns but does not always correctly detect named exports. In these cases, using the default import form described above can be a better option.
Named exports detection covers many common export patterns, reexport patterns and build tool and transpiler outputs. See cjs-module-lexer for the exact semantics implemented.
require
,
exports
, or
module.exports
#
In most cases, the ES module
import
can be used to load CommonJS modules.
If needed, a
require
function can be constructed within an ES module using
module.createRequire()
.
__filename
or
__dirname
#
These CommonJS variables are not available in ES modules.
__filename
and
__dirname
use cases can be replicated via
import.meta.url
.
Addons are not currently supported with ES module imports.
They can instead be loaded with
module.createRequire()
or
process.dlopen
.
require.resolve
#
Relative resolution can be handled via
new URL('./local', import.meta.url)
.
For a complete
require.resolve
replacement, there is a flagged experimental
import.meta.resolve
API.
Alternatively
module.createRequire()
can be used.
NODE_PATH
#
NODE_PATH
is not part of resolving
import
specifiers. Please use symlinks
if this behavior is desired.
require.extensions
#
require.extensions
is not used by
import
. The expectation is that loader
hooks can provide this workflow in the future.
require.cache
#
require.cache
is not used by
import
as the ES module loader has its own
separate cache.
JSON files can be referenced by
import
:
import packageConfig from './package.json' assert { type: 'json' };
The
assert { type: 'json' }
syntax is mandatory; see
Import Assertions
.
The imported JSON only exposes a
default
export. There is no support for named
exports. A cache entry is created in the CommonJS cache to avoid duplication.
The same object is returned in CommonJS if the JSON module has already been
imported from the same path.
Importing WebAssembly modules is supported under the
--experimental-wasm-modules
flag, allowing any
.wasm
files to be
imported as normal modules while also supporting their module imports.
This integration is in line with the ES Module Integration Proposal for WebAssembly .
For example, an
index.mjs
containing:
import * as M from './module.wasm';
console.log(M);
executed under:
node --experimental-wasm-modules index.mjs
would provide the exports interface for the instantiation of
module.wasm
.
await
#
The
await
keyword may be used in the top level body of an ECMAScript module.
Assuming an
a.mjs
with
export const five = await Promise.resolve(5);
And a
b.mjs
with
import { five } from './a.mjs';
console.log(five); // Logs `5`
node b.mjs # works
If a top level
await
expression never resolves, the
node
process will exit
with a
13
status code
.
import { spawn } from 'node:child_process';
import { execPath } from 'node:process';
spawn(execPath, [
'--input-type=module',
'--eval',
// Never-resolving Promise:
'await new Promise(() => {})',
]).once('exit', (code) => {
console.log(code); // Logs `13`
});
Importing network based modules using
https:
and
http:
is supported under
the
--experimental-network-imports
flag. This allows web browser-like imports
to work in Node.js with a few differences due to application stability and
security concerns that are different when running in a privileged environment
instead of a browser sandbox.
Automatic protocol negotiation for HTTP/2 and HTTP/3 is not yet supported.
http:
is vulnerable to man-in-the-middle attacks and is not allowed to be
used for addresses outside of the IPv4 address
127.0.0.0/8
(
127.0.0.1
to
127.255.255.255
) and the IPv6 address
::1
. Support for
http:
is intended
to be used for local development.
Authorization
,
Cookie
, and
Proxy-Authorization
headers are not sent to the
server. Avoid including user info in parts of imported URLs. A security model
for safely using these on the server is being worked on.
CORS is designed to allow a server to limit the consumers of an API to a specific set of hosts. This is not supported as it does not make sense for a server-based implementation.
These modules cannot access other modules that are not over
http:
or
https:
.
To still access local modules while avoiding the security concern, pass in
references to the local dependencies:
// file.mjs
import worker_threads from 'node:worker_threads';
import { configure, resize } from 'https://example.com/imagelib.mjs';
configure({ worker_threads });
// https://example.com/imagelib.mjs
let worker_threads;
export function configure(opts) {
worker_threads = opts.worker_threads;
export function resize(img, size) {
// Perform resizing in worker_thread to avoid main thread blocking
}
For now, the
--experimental-network-imports
flag is required to enable loading
resources over
http:
or
https:
. In the future, a different mechanism will be
used to enforce this. Opt-in is required to prevent transitive dependencies
inadvertently using potentially mutable state that could affect reliability
of Node.js applications.
Add support for chaining loaders.
v16.12.0
Removed
getFormat
,
getSource
,
transformSource
, and
globalPreload
; added
load
hook and
getGlobalPreload
hook.
Added in: v8.8.0
This API is currently being redesigned and will still change.
To customize the default module resolution, loader hooks can optionally be
provided via a
--experimental-loader ./loader-name.mjs
argument to Node.js.
When hooks are used they apply to each subsequent loader, the entry point, and
all
import
calls. They won't apply to
require
calls; those still follow
CommonJS
rules.
Loaders follow the pattern of
--require
:
node \
--experimental-loader unpkg \
--experimental-loader http-to-https \
--experimental-loader cache-buster
These are called in the following sequence:
cache-buster
calls
http-to-https
which calls
unpkg
.
Hooks are part of a chain, even if that chain consists of only one custom
(user-provided) hook and the default hook, which is always present. Hook
functions nest: each one must always return a plain object, and chaining happens
as a result of each function calling
next<hookName>()
, which is a reference
to the subsequent loader's hook.
A hook that returns a value lacking a required property triggers an exception.
A hook that returns without calling
next<hookName>()
and
without returning
shortCircuit: true
also triggers an exception. These errors are to help
prevent unintentional breaks in the chain.
Hooks are run in a separate thread, isolated from the main. That means it is a
different
realm
. The hooks thread may be
terminated by the main thread at any time, so do not depend on asynchronous
operations (like
console.log
) to complete.
resolve(specifier, context, nextResolve)
#
Add support for chaining resolve hooks. Each hook must either call
nextResolve()
or include a
shortCircuit
property set to
true
in its return.
Add support for import assertions.
The loaders API is being redesigned. This hook may disappear or its signature may change. Do not rely on the API described below.
specifier
<string>
context
<Object>
conditions
<string[]>
Export conditions of the relevant
package.json
importAssertions
<Object>
An object whose key-value pairs represent the
assertions for the module to import
parentURL
<string>
|
<undefined>
The module importing this one, or undefined
if this is the Node.js entry point
nextResolve
<Function>
The subsequent
resolve
hook in the chain, or the
Node.js default
resolve
hook after the last user-supplied
resolve
hook
specifier
<string>
context
<Object>
format
<string>
|
<null>
|
<undefined>
A hint to the load hook (it might be
ignored)
'builtin' | 'commonjs' | 'json' | 'module' | 'wasm'
importAssertions
<Object>
|
<undefined>
The import assertions to use when
caching the module (optional; if excluded the input will be used)
shortCircuit
<undefined>
|
<boolean>
A signal that this hook intends to
terminate the chain of
resolve
hooks.
Default:
false
url
<string>
The absolute URL to which this input resolves
Caveat
Despite support for returning promises and async functions, calls
to
resolve
may block the main thread which can impact performance.
The
resolve
hook chain is responsible for telling Node.js where to find and
how to cache a given
import
statement or expression. It can optionally return
its format (such as
'module'
) as a hint to the
load
hook. If a format is
specified, the
load
hook is ultimately responsible for providing the final
format
value (and it is free to ignore the hint provided by
resolve
); if
resolve
provides a
format
, a custom
load
hook is required even if only to
pass the value to the Node.js default
load
hook.
Import type assertions are part of the cache key for saving loaded modules into
the internal module cache. The
resolve
hook is responsible for
returning an
importAssertions
object if the module should be cached with
different assertions than were present in the source code.
The
conditions
property in
context
is an array of conditions for
package exports conditions
that apply to this resolution
request. They can be used for looking up conditional mappings elsewhere or to
modify the list when calling the default resolution logic.
The current
package exports conditions
are always in
the
context.conditions
array passed into the hook. To guarantee
default
Node.js module specifier resolution behavior
when calling
defaultResolve
, the
context.conditions
array passed to it
must
include
all
elements of the
context.conditions
array originally passed into the
resolve
hook.
export function resolve(specifier, context, nextResolve) {
const { parentURL = null } = context;
if (Math.random() > 0.5) { // Some condition.
// For some or all specifiers, do some custom logic for resolving.
// Always return an object of the form {url: <string>}.
return {
shortCircuit: true,
url: parentURL ?
new URL(specifier, parentURL).href :
new URL(specifier).href,
if (Math.random() < 0.5) { // Another condition.
// When calling `defaultResolve`, the arguments can be modified. In this
// case it's adding another value for matching conditional exports.
return nextResolve(specifier, {
...context,
conditions: [...context.conditions, 'another-condition'],
// Defer to the next hook in the chain, which would be the
// Node.js default resolve if this is the last user-specified loader.
return nextResolve(specifier);
}
load(url, context, nextLoad)
#
Add support for chaining load hooks. Each hook must either call
nextLoad()
or include a
shortCircuit
property set to
true
in its return.
The loaders API is being redesigned. This hook may disappear or its signature may change. Do not rely on the API described below.
In a previous version of this API, this was split across 3 separate, now
deprecated, hooks (
getFormat
,
getSource
, and
transformSource
).
url
<string>
The URL returned by the
resolve
chain
context
<Object>
conditions
<string[]>
Export conditions of the relevant
package.json
format
<string>
|
<null>
|
<undefined>
The format optionally supplied by the
resolve
hook chain
importAssertions
<Object>
nextLoad
<Function>
The subsequent
load
hook in the chain, or the
Node.js default
load
hook after the last user-supplied
load
hook
specifier
<string>
context
<Object>
format
<string>
shortCircuit
<undefined>
|
<boolean>
A signal that this hook intends to
terminate the chain of
resolve
hooks.
Default:
false
source
<string>
|
<ArrayBuffer>
|
<TypedArray>
The source for Node.js to evaluate
The
load
hook provides a way to define a custom method of determining how
a URL should be interpreted, retrieved, and parsed. It is also in charge of
validating the import assertion.
The final value of
format
must be one of the following:
format
|
Description |
Acceptable types for
source
returned by
load
|
---|---|---|
'builtin'
|
Load a Node.js builtin module | Not applicable |
'commonjs'
|
Load a Node.js CommonJS module | Not applicable |
'json'
|
Load a JSON file |
{
string
,
ArrayBuffer
,
TypedArray
}
|
'module'
|
Load an ES module |
{
string
,
ArrayBuffer
,
TypedArray
}
|
'wasm'
|
Load a WebAssembly module |
{
ArrayBuffer
,
TypedArray
}
|
The value of
source
is ignored for type
'builtin'
because currently it is
not possible to replace the value of a Node.js builtin (core) module. The value
of
source
is ignored for type
'commonjs'
because the CommonJS module loader
does not provide a mechanism for the ES module loader to override the
CommonJS module return value
. This limitation might be
overcome in the future.
Caveat
: The ESM
load
hook and namespaced exports from CommonJS modules
are incompatible. Attempting to use them together will result in an empty
object from the import. This may be addressed in the future.
These types all correspond to classes defined in ECMAScript.
ArrayBuffer
object is a
SharedArrayBuffer
.
TypedArray
object is a
Uint8Array
.
If the source value of a text-based format (i.e.,
'json'
,
'module'
)
is not a string, it is converted to a string using
util.TextDecoder
.
The
load
hook provides a way to define a custom method for retrieving the
source code of an ES module specifier. This would allow a loader to potentially
avoid reading files from disk. It could also be used to map an unrecognized
format to a supported one, for example
yaml
to
module
.
export async function load(url, context, nextLoad) {
const { format } = context;
if (Math.random() > 0.5) { // Some condition
For some or all URLs, do some custom logic for retrieving the source.
Always return an object of the form {
format: <string>,
source: <string|buffer>,
return {
format,
shortCircuit: true,
source: '...',
// Defer to the next hook in the chain.
return nextLoad(url);
}
In a more advanced scenario, this can also be used to transform an unsupported source to a supported one (see Examples below).
globalPreload()
#
Add support for chaining globalPreload hooks.
The loaders API is being redesigned. This hook may disappear or its signature may change. Do not rely on the API described below.
In a previous version of this API, this hook was named
getGlobalPreloadCode
.
context
<Object>
Information to assist the preload code
port
<MessagePort>
Sometimes it might be necessary to run some code inside of the same global scope that the application runs in. This hook allows the return of a string that is run as a sloppy-mode script on startup.
Similar to how CommonJS wrappers work, the code runs in an implicit function
scope. The only argument is a
require
-like function that can be used to load
builtins like "fs":
getBuiltin(request: string)
.
If the code needs more advanced
require
features, it has to construct
its own
require
using
module.createRequire()
.
export function globalPreload(context) {
return `\
globalThis.someInjectedProperty = 42;
console.log('I just set some globals!');
const { createRequire } = getBuiltin('module');
const { cwd } = getBuiltin('process');
const require = createRequire(cwd() + '/<preload>');
// [...]
}
In order to allow communication between the application and the loader, another
argument is provided to the preload code:
port
. This is available as a
parameter to the loader hook and inside of the source text returned by the hook.
Some care must be taken in order to properly call
port.ref()
and
port.unref()
to prevent a process from being in a state where it won't
close normally.
The various loader hooks can be used together to accomplish wide-ranging customizations of the Node.js code loading and evaluation behaviors.
In current Node.js, specifiers starting with
https://
are experimental (see
HTTPS and HTTP imports
).
The loader below registers hooks to enable rudimentary support for such specifiers. While this may seem like a significant improvement to Node.js core functionality, there are substantial downsides to actually using this loader: performance is much slower than loading files from disk, there is no caching, and there is no security.
// https-loader.mjs
import { get } from 'node:https';
export function resolve(specifier, context, nextResolve) {
const { parentURL = null } = context;
// Normally Node.js would error on specifiers starting with 'https://', so
// this hook intercepts them and converts them into absolute URLs to be
// passed along to the later hooks below.
if (specifier.startsWith('https://')) {
return {
shortCircuit: true,
url: specifier,
} else if (parentURL && parentURL.startsWith('https://')) {
return {
shortCircuit: true,
url: new URL(specifier, parentURL).href,
// Let Node.js handle all other specifiers.
return nextResolve(specifier);
export function load(url, context, nextLoad) {
// For JavaScript to be loaded over the network, we need to fetch and
// return it.
if (url.startsWith('https://'
)) {
return new Promise((resolve, reject) => {
get(url, (res) => {
let data = '';
res.setEncoding('utf8');
res.on('data', (chunk) => data += chunk);
res.on('end', () => resolve({
// This example assumes all network-provided JavaScript is ES module
// code.
format: 'module',
shortCircuit: true,
source: data,
}).on('error', (err) => reject(err));
// Let Node.js handle all other URLs.
return nextLoad(url);
}
// main.mjs
import { VERSION } from 'https://coffeescript.org/browser-compiler-modern/coffeescript.js';
console.log(VERSION);
With the preceding loader, running
node --experimental-loader ./https-loader.mjs ./main.mjs
prints the current version of CoffeeScript per the module at the URL in
main.mjs
.
Sources that are in formats Node.js doesn't understand can be converted into
JavaScript using the
load
hook
. Before that hook gets called,
however, a
resolve
hook
needs to tell Node.js not to
throw an error on unknown file types.
This is less performant than transpiling source files before running Node.js; a transpiler loader should only be used for development and testing purposes.
// coffeescript-loader.mjs
import { readFile } from 'node:fs/promises';
import { dirname, extname, resolve as resolvePath } from 'node:path';
import { cwd } from 'node:process';
import { fileURLToPath, pathToFileURL } from 'node:url';
import CoffeeScript from 'coffeescript';
const baseURL = pathToFileURL(`${cwd()}/`).href;
// CoffeeScript files end in .coffee, .litcoffee, or .coffee.md.
const extensionsRegex = /\.coffee$|\.litcoffee$|\.coffee\.md$/;
export function resolve(specifier, context, nextResolve) {
if (extensionsRegex.test(specifier)) {
const { parentURL = baseURL } = context;
// Node.js normally errors on unknown file extensions, so return a URL for
// specifiers ending in the CoffeeScript file extensions.
return {
shortCircuit: true,
url: new URL(specifier, parentURL).href,
// Let Node.js handle all other specifiers.
return nextResolve(specifier);
export async function load(url, context, nextLoad) {
if (extensionsRegex.test(url)) {
// Now that we patched resolve to let CoffeeScript URLs through, we need to
// tell Node.js what format such URLs should be interpreted as. Because
// CoffeeScript transpiles into JavaScript, it should be one of the two
// JavaScript formats: 'commonjs' or 'module'.
// CoffeeScript files can be either CommonJS or ES modules, so we want any
// CoffeeScript file to be treated by Node.js the same as a .js file at the
// same location. To determine how Node.js would interpret an arbitrary .js
// file, search up the file system for the nearest parent package.json file
// and read its "type" field.
const format = await getPackageType(url);
// When a hook returns a format of 'commonjs', `source` is ignored.
// To handle CommonJS files, a handler needs to be registered with
// `require.extensions` in order to process the files with the CommonJS
// loader. Avoiding the need for a separate CommonJS handler is a future
// enhancement planned for ES module loaders.
if (format === 'commonjs') {
return {
format,
shortCircuit: true,
const { source: rawSource } = await nextLoad(url, { ...context, format });
// This hook converts CoffeeScript source code into JavaScript source code
// for all imported CoffeeScript files.
const transformedSource = coffeeCompile(rawSource.toString(), url);
return {
format,
shortCircuit: true,
source: transformedSource,
// Let Node.js handle all other URLs.
return nextLoad(url);
async function getPackageType(url) {
// `url` is only a file path during the first iteration when passed the
// resolved url from the load() hook
// an actual file path from load() will contain a file extension as it's
// required by the spec
// this simple truthy check for whether `url` contains a file extension will
// work for most projects but does not cover some edge-cases (such as
// extensionless files or a url ending in a trailing space)
const isFilePath = !!extname(url);
// If it is a file path, get the directory it's in
const dir = isFilePath ?
dirname(fileURLToPath(url)) :
// Compose a file path to a package.json in the same directory,
// which may or may not exist
const packagePath = resolvePath(dir, 'package.json');
// Try to read the possibly nonexistent package.json
const type = await readFile(packagePath, { encoding: 'utf8' })
.then((filestring) => JSON.parse(filestring).type)
.catch((err) => {
if (err?.code !== 'ENOENT') console.error(err);
// Ff package.json existed and contained a `type` field with a value, voila
if (type) return type;
// Otherwise, (if not at the root) continue checking the next directory up
// If at the root, stop and return false
return dir.length > 1 && getPackageType(resolvePath(dir, '..'));
}
# main.coffee
import { scream } from './scream.coffee'
console.log scream 'hello, world'
import { version } from 'node:process'
console.log "Brought to you by Node.js version #{version}"
# scream.coffee
export scream = (str) -> str.toUpperCase()
With the preceding loader, running
node --experimental-loader ./coffeescript-loader.mjs main.coffee
causes
main.coffee
to be turned into JavaScript after its source code is
loaded from disk but before Node.js executes it; and so on for any
.coffee
,
.litcoffee
or
.coffee.md
files referenced via
import
statements of any
loaded file.
The resolver has the following properties:
The algorithm to load an ES module specifier is given through the ESM_RESOLVE method below. It returns the resolved URL for a module specifier relative to a parentURL.
The algorithm to determine the module format of a resolved URL is provided by ESM_FORMAT , which returns the unique module format for any file. The "module" format is returned for an ECMAScript Module, while the "commonjs" format is used to indicate loading through the legacy CommonJS loader. Additional formats such as "addon" can be extended in future updates.
In the following algorithms, all subroutine errors are propagated as errors of these top-level routines unless stated otherwise.
defaultConditions
is the conditional environment name array,
["node", "import"]
.
The resolver can throw the following errors:
ESM_RESOLVE ( specifier , parentURL )
PACKAGE_RESOLVE ( packageSpecifier , parentURL )
PACKAGE_EXPORTS_RESOLVE ( packageURL , subpath , exports , conditions )
PACKAGE_IMPORTS_RESOLVE ( specifier , parentURL , conditions )
PACKAGE_IMPORTS_EXPORTS_RESOLVE ( matchKey , matchObj , packageURL , isImports , conditions )
PACKAGE_TARGET_RESOLVE ( packageURL , target , patternMatch , isImports , conditions )
ESM_FILE_FORMAT ( url )
The Loaders API provides a mechanism for customizing the ESM specifier resolution algorithm. An example loader that provides CommonJS-style resolution for ESM specifiers is commonjs-extension-resolution-loader .