This is an experiment. Nobody should run this in production.
Design constraints, in order of importance:
- Independent of node core's implementation.
- Spec compliant.
- Maximum browser compat.
- Easy to use in existing applications.
- Compatible with ecosystem code already written using ESM.
- Minimal C++ to allow for fast iteration.
- Resolve relative URLs.
- Load cyclic modules.
-
import.meta.url
in modules. - Dynamic
import()
. - Handle multiple loaders / contexts.
- Don't SEGFAULT (and add DCHECKs).
const Loader = require('hackable-loader');
const registerUnprefixedNodeCoreModules = require('hackable-loader/resolve/node-core');
Loader
// Overwrite dynamic import to use this loader.
.enableDynamicImport()
// Overwrite import.meta to use this loader.
.enableImportMeta();
// Add support for resolving 'fs' etc.
registerUnprefixedNodeCoreModules(Loader.current);
// Load an entry point.
import('./module.mjs')
.then(ns => console.log(ns));
Configure import()
of the active v8::Isolate
to use this loader.
Configure import.meta
of the active v8::Isolate
to use this loader.
Add resolution of node's built-in modules like 'fs'
.
Otherwise they have to be imported using the node:
URL scheme:
// Without registerUnprefixedNodeCoreModules:
import { readFile } from 'node:fs';
// With:
import { readFile } from 'fs';
Since this affects the resolution algorithm, this has to be applied on a per-loader basis.
The Module
class is the JavaScript representation of a v8::Module
.
Compile the given source
as a module.
This is a potential future API.
This scheme is used to address resources that aren't on disk but are compiled into the node binary: The built-in modules like "fs".
Example: node:fs
Marks a resource that should be loaded as a node-style CommonJS module. The "content" of this resource is ignored by the loader itself, instead it will execute using the existing CommonJS module system.
If the contentType
has sideEffects=false
in its parameters,
we assume that we can run the module ahead of time to get its exports.
Module loading is split into three phases:
- Module resolution
- Resource fetching
- Module init
Given a specifier: string
and referrerURL: string
,
provide a url: string
or a set of potential urls: string[]
of a resource:
const resolve: (specifier: string, referrerURL: string) => string | string[];
If the resolution fails (e.g. because of an invalid URL), the function should throw.
Given a resource url: string
,
fetch the resource content and associated meta data.
type Resource = {
bytes?: Buffer,
contentType: string,
contentTypeParameters?: string,
};
const fetch: (url: string) => Resource;
If fetching fails (e.g. because the resource cannot be found), the function should throw.
Given a resource: Resource
and a target: Module
module handle,
initialize the target
.
Most implementations will check the resource.contentType
to select the appropriate behavior.
const init: (target: Module, resource: Resource, Module) => void;
If initialization fails (e.g. because the resource content fails to compile), the function should throw.
- One
Loader
instance per context / global. - By default the
Loader
for a context starts out empty. - Each
Loader
keeps a map of URL string toModule
. - When a URL is requested that hasn't been loaded already,
that process is handed over to a
LoadModuleJob
. TheLoadModuleJob
keeps track of everything that needs to wrap up before the requested module can be returned and provides aPromise
for its completion. - During
LoadModuleJob
execution, theresolve
,fetch
andinit
hooks of theLoader
will be called.
An interface that allows to manipulate the dynamic exports of a module. It is used to expose non-ESM modules inside of ESM modules.
reflect:node:fs
: The module that is used to set up the exports of node's built-infs
module.