ESM Loader & Module Resolver
Dissect the inner mechanics of babel-esm-loader.js, Node's custom ESM loaders thread that transpiles JSX/TSX and registers React boundaries on-the-fly.
Key Files Involved:
β’ Custom ESM loader:./dinou/core/babel-esm-loader.js
β’ Loader entry:./dinou/core/register-loader.mjs
β’ Import/Cache helper:./dinou/core/import-module.js
π‘ Overview
Node.js by default is built to execute standard, compiled JavaScript. It does not know how to parse TypeScript (.ts/.tsx), JSX elements, CSS Modules, or dynamic media resources. Furthermore, Node.js does not natively recognize React's special "use client" and "use server" directives.
Dinou bridges this execution gap by starting the server with a custom Node.js ESM Loader (via the --import flag in register-loader.mjs). The loader runs in a dedicated worker thread, intercepting every ES Modules dynamic import() and static import chain to resolve, stub, and transpile code on the fly.
βοΈ Loader Registration (register-loader.mjs)
The entry point of the loader thread is register-loader.mjs. When the Master Express Server starts up, Node.js is executed with the command line option --import=./dinou/core/register-loader.mjs. This registers our custom ESM hooks globally in the V8 VM instance.
import { register } from "node:module";
import { pathToFileURL } from "node:url";
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
globalThis.__dinou_require__ = require;
const loaderPath = require.resolve("./babel-esm-loader.js");
register(pathToFileURL(loaderPath).href, pathToFileURL("./"));This script performs three actions:
- Dynamic Registration: It uses Node's native
module.registerAPI to registerbabel-esm-loader.jsas the active loader. - CommonJS Bridge creation: Node's ESM loader runs in a separate thread where native CommonJS
requireis unavailable. It instantiates a standard require resolver usingcreateRequireand binds it toglobalThis.__dinou_require__so that the loader can synchronously resolve CommonJS files. - Bootstrapping hooks: Once registered, all subsequent dynamic module imports will flow through our JIT compiler hooks.
π Physical File Structure
The babel-esm-loader.js file follows this logical pipeline structure during module resolution and compilation:
π 1. Dependencies & Resolution Mocking
At startup, the loader pulls in core Node.js file system APIs, Babel transpilation modules, and project helper functions (like getAbsPathWithExt).
Crucially, when running under ES Modules in a non-Webpack environment, Node.js needs to resolve the standard "react" and "react-dom" packages to their respective server-specific bundles (.react-server.js). The loader overrides Node's native module resolver hook Module._resolveFilename to dynamically map these imports:
const fs = require("fs");
const path = require("path");
const { transformAsync } = require("@babel/core");
const { fileURLToPath, pathToFileURL } = require("url");
const createScopedName = require("./createScopedName");
const { extensionsWithDot } = require("./asset-extensions.js");
const { getAbsPathWithExt } = require("./get-abs-path-with-ext.js");
const { normalizePathCase } = require("./path-utils.js");
const Module = require("module");
const originalResolveFilename = Module._resolveFilename;
const isWebpack = process.env.DINOU_BUILD_TOOL === "webpack";
let reactServerPath, reactDomServerPath, reactJsxRuntimePath, reactJsxDevRuntimePath;
if (!isWebpack) {
const reactPkgJson = require.resolve("react/package.json");
reactServerPath = path.join(path.dirname(reactPkgJson), "react.react-server.js");
reactJsxRuntimePath = path.join(path.dirname(reactPkgJson), "jsx-runtime.react-server.js");
reactJsxDevRuntimePath = path.join(path.dirname(reactPkgJson), "jsx-dev-runtime.react-server.js");
const reactDomPkgJson = require.resolve("react-dom/package.json");
reactDomServerPath = path.join(path.dirname(reactDomPkgJson), "react-dom.react-server.js");
}
Module._resolveFilename = function (request, parent, isMain, options) {
if (!isWebpack) {
if (request === "react") {
return reactServerPath;
} else if (request === "react-dom") {
return reactDomServerPath;
} else if (request === "react/jsx-runtime") {
return reactJsxRuntimePath;
} else if (request === "react/jsx-dev-runtime") {
return reactJsxDevRuntimePath;
}
}
return originalResolveFilename.call(this, request, parent, isMain, options);
};
require("./css-require-hook.js")();βοΈ 2. The Resolve Hook (exports.resolve)
When a file triggers an import, Node's loader triggers the resolve() hook. Dinou intercepts the request to support clean directory path aliases and implicit file extensions:
exports.resolve = async function resolve(specifier, context, defaultResolve) {
const absPathWithExt = getAbsPathWithExt(specifier, context);
if (absPathWithExt) {
const url = pathToFileURL(absPathWithExt).href;
return {
url,
shortCircuit: true,
};
}
return defaultResolve(specifier, context, defaultResolve);
};Under the Hood: getAbsPathWithExt() and Path Normalization
In the ES Modules specification, Node.js natively requires explicit relative paths with complete file extensions (e.g., import Header from "./Header.tsx" instead of "./Header"). To restore standard frontend importing syntax, getAbsPathWithExt() executes three operations:
- Alias Resolution (tsconfig.json): It reads the project's
tsconfig.json(orjsconfig.json) sΓncronamente at startup. It maps thecompilerOptions.pathskeys (e.g.,"@/*") to their absolute target base directories on disk. - Relative Path Resolution: If the specifier is relative (starts with
./or../), it converts the path to an absolute location relative to the parent file's folder (context.parentURL). - Implicit Extension & Index Scans (
tryExtensions): Once an absolute path is resolved, if it does not point directly to an active file, it scans sequentially for valid file suffixes:.js,.ts,.jsx, and.tsx. If the path points to a directory (e.g.,components/Header/), it sweeps for index entryfiles:index.js,index.ts,index.jsx, orindex.tsx.
π¦ 3. The Load Hook (exports.load)
Once a module's file URL is resolved, Node calls the load() hook to fetch and parse the source code. Dinou intercepts the loading process depending on the file type:
π¨ A & B. Asset & CSS Interception
If a component imports stylesheets or static media files, Node's loader would throw an evaluation error. Dinou intercepts these extensions and returns virtual mock stubs:
- Non-JS Media Assets (images, fonts, etc.): The loader checks against the list of asset extensions. It creates a scoped hashed name and returns a virtual module that simply exports the static public asset URL (e.g.
export default "/assets/logo.a1b2.png"). - CSS Stylesheets: Imports ending in
.cssare required synchronously (which triggers the backendcss-require-hookto parse class names into a mapped CSS modules dictionary) and exported as a JSON-serialized object module.
// Handles asset file extensions (e.g. .png, .jpg, .svg, etc.)
const assetExts = extensionsWithDot;
const ext = path.extname(url.split("?")[0]);
if (assetExts.includes(ext)) {
const filepath = fileURLToPath(url);
const localName = path.basename(filepath, ext);
const hashedName = createScopedName(localName, filepath);
const virtualExport = `export default "/assets/${hashedName}${ext}";`;
return {
format: "module",
source: virtualExport,
shortCircuit: true,
url,
};
}
// Handles stylesheets and CSS module mapping dictionaries
if (ext === ".css") {
const mod = require(fileURLToPath(url));
const source = `export default ${JSON.stringify(mod)};`;
return { format: "module", source, shortCircuit: true, url };
}βοΈ C.1. Client References ("use client")
When a Server Component renders, it builds a metadata description (RSC Flight payload) detailing where Client Components are nested. The server should never compile or evaluate the actual JS body of a Client Component, as browser-only globals (like window or document) or React hooks (like useEffect or useState) would crash the Node.js server.
The isReactServer && hasUseClient Conditional Guard:
If the loader is running within the React Server Components rendering graph (identified by checking if process.execArgv contains the react-server flag) and detects the "use client" directive in a file:
- Discarding the Source Code: The loader completely discards the original source file body to protect the server environment.
- Parsing Exports: It parses the file's exports sΓncronamente using the helper
parseExports(source). - Registering Proxies: It replaces the exports with a call to
registerClientReference()from React's server-dom packages (as shown below). This registers a metadata hook pointing to the local file URL and export key.
const cleanUrl = url.split("?")[0];
if (/\.(jsx|tsx|ts|js)$/.test(cleanUrl)) {
const filename = fileURLToPath(cleanUrl.startsWith("file://") ? cleanUrl : pathToFileURL(cleanUrl).href);
const rel = path.relative(normalizePathCase(process.cwd()), normalizePathCase(filename));
const source = fs.readFileSync(filename, "utf-8");
const urlToReturn = pathToFileURL(filename).href;
const useClientRegex = /^\s*(?:(?:\/\/[^\n]*\n\s*)|(?:\/\*[\s\S]*?\*\/\s*))*['"]use client['"]/;
const hasUseClient = useClientRegex.test(source);
const isReactServer = process.execArgv.some(arg => arg.includes("react-server"));
if (isReactServer && hasUseClient) {
const parseExports = require("./parse-exports.js");
const exports = parseExports(source);
let newSrc = "";
if (isWebpack) {
newSrc += 'import { registerClientReference } from "react-server-dom-webpack/server";\n';
} else {
const packageJsonPath = require.resolve("@roggc/react-server-dom-esm/package.json");
const serverNodePath = path.join(path.dirname(packageJsonPath), "server.node.js");
const serverNodeUrl = pathToFileURL(serverNodePath).href;
newSrc += `import pkg from ${JSON.stringify(serverNodeUrl)};\n`;
newSrc += 'const {registerClientReference} = pkg;\n';
}
for (const name of exports) {
if (name === 'default') {
newSrc += 'export default registerClientReference(function() {\n';
newSrc += ' throw new Error(' + JSON.stringify("Attempted to call the default export of " + urlToReturn + " from the server but it's on the client.") + ');\n';
newSrc += '}, ' + JSON.stringify(urlToReturn) + ', "default");\n';
} else {
newSrc += 'export const ' + name + ' = registerClientReference(function() {\n';
newSrc += ' throw new Error(' + JSON.stringify("Attempted to call " + name + "() from the server but " + name + " is on the client.") + ');\n';
newSrc += '}, ' + JSON.stringify(urlToReturn) + ', ' + JSON.stringify(name) + ');\n';
}
}
return {
format: "module",
source: newSrc,
shortCircuit: true,
url: urlToReturn,
};
}
}This ensures that the server process only outputs the metadata link (reference location) instead of running client-only code. If the server tries to invoke a client component default or named export directly, the proxy function throws a descriptive runtime exception.
π C.2. Server Functions ("use server")
If a file contains the "use server" directive, the functions exported from this module represent Server Functions that the client browser can trigger remotely via POST request callbacks.
The isReactServer && hasUseServer Conditional Guard:
When executing Server Components, if the loader catches a "use server" module:
- Transpile code: Unlike client components, it does not discard the function bodies. It compiles the code via Babel to generate pure JavaScript compatible with the execution environment.
- Map IDs: It maps every exported function key to an absolute reference using its relative file system URL and the export symbol name.
- Server Registry Binding: It appends calls to
registerServerReference()mapping the function pointer to its unique remote address (as shown below).
const { useServerRegex } = require("../constants.js");
const hasUseServer = useServerRegex.test(source);
if (isReactServer && hasUseServer) {
const parseExports = require("./parse-exports.js");
const exports = parseExports(source);
const { code } = await transformAsync(source, {
filename,
presets: [
["@babel/preset-react", { runtime: "automatic" }],
"@babel/preset-typescript",
],
sourceMaps: "inline",
ast: false,
});
let newSrc = code + "\n\n";
if (!isWebpack) {
const packageJsonPath = require.resolve("@roggc/react-server-dom-esm/package.json");
const serverNodePath = path.join(path.dirname(packageJsonPath), "server.node.js");
const serverNodeUrl = pathToFileURL(serverNodePath).href;
newSrc += `import pkgServer from ${JSON.stringify(serverNodeUrl)};\n`;
newSrc += 'const {registerServerReference} = pkgServer;\n';
}
const relativeFileUrl = "file:///" + rel.replace(/\\/g, "/");
for (const name of exports) {
if (name !== 'default') {
newSrc += `registerServerReference(${name}, dots);
`;
}
}
return {
format: "module",
source: newSrc,
shortCircuit: true,
url: urlToReturn,
};
}This links each function to a unique identifier. When the client invokes a Server Function, the browser transmits a POST request containing these target parameters. The Dinou router reads the request, maps it to the registered function, executes it in the Node environment, and streams the React response back to the client.
π C.3. Standard JS Files
For standard, plain JavaScript files that are imported, the loader determines whether to bypass compiler steps:
- CommonJS node_modules bypass: If it's a
.jsfile in thenode_modulesfolder that does not contain ES Module syntax (like staticimport/exportstatements), it bypasses custom loaders and delegates to Node'sdefaultLoad. - Workspace JS files: If it belongs to our application source, the file is loaded directly as a standard ES Module.
const esmSyntaxRegex = /^(?:import|export)\b/m;
const hasESMSyntax = esmSyntaxRegex.test(source);
if (ext === ".js" && !rel.startsWith("src" + path.sep) && !hasESMSyntax) {
// Pass to default loader if it's a non-esm commonjs file in node_modules
return defaultLoad(url, context, defaultLoad);
}
if (ext === ".js") {
// If it's a JS file in our source, load it directly as module
return {
format: "module",
source,
shortCircuit: true,
url,
};
}β‘ C.4. JSX & TypeScript Transpilation
For source files that do not trigger client reference or server function stubs (regular utility files, layouts, or static pages), the loader compiles the TypeScript syntax and JSX elements:
const { code } = await transformAsync(source, {
filename,
presets: [
["@babel/preset-react", { runtime: "automatic" }],
"@babel/preset-typescript",
],
sourceMaps: "inline",
ast: false,
});
return {
format: "module",
source: code,
shortCircuit: true,
url: urlToReturn,
};The output returns clean, standardized ECMAScript modules (with inline source maps for debugging) that the V8 runtime engine can execute natively.
π οΈ Common Tweak Recipes
You can change compilation rules or register alternative Babel presets (such as adding decorator support) inside the transformAsync() parameters inside the load() hook.
Add new loader branches inside the load() hook (such as loading raw text or markdown files as JS modules) to extend the importing capabilities of your framework.
π οΈ Common Tweak Recipes
You can change compilation rules or register alternative Babel presets (such as adding decorator support) inside the transformAsync() parameters inside the load() hook.
Add new loader branches inside the load() hook (such as loading raw text or markdown files as JS modules) to extend the importing capabilities of your framework.