Node.js ES Module loader hooks for module resolution and transformation with TypeScript path mapping and package type detection.
Custom ES Module resolver that handles TypeScript files, path mapping, and complex module resolution scenarios.
/**
* Node.js ESM resolve hook for TypeScript module resolution
* @param specifier - Module specifier to resolve (e.g., './module', '@scope/package')
* @param context - Resolution context from Node.js loader
* @param nextResolve - Next resolve function in the chain
* @returns Promise resolving to module URL and format information
*/
function resolve(
specifier: string,
context: {
conditions: string[];
parentURL?: string;
importAttributes?: Record<string, string>;
},
nextResolve: (specifier: string) => Promise<ResolveFnOutput>
): Promise<ResolveFnOutput>;
interface ResolveFnOutput {
url: string;
format?: "builtin" | "commonjs" | "json" | "module" | "wasm";
shortCircuit?: boolean;
}Usage Examples:
// ESM Registration
import { register } from 'node:module';
import { pathToFileURL } from 'node:url';
register('@swc-node/register/esm', pathToFileURL('./').toString());
// Alternative registration using esm-register
import '@swc-node/register/esm-register';
// Now you can import TypeScript files directly
import { myFunction } from './utils.ts';
import { Component } from './components/Button.tsx';Custom ES Module loader that transforms TypeScript/JavaScript files using SWC during the load phase.
/**
* Node.js ESM load hook for TypeScript transformation
* @param url - Module URL to load
* @param context - Load context from Node.js loader
* @param nextLoad - Next load function in the chain
* @returns Promise resolving to transformed source code
*/
function load(
url: string,
context: {
format?: string;
importAttributes?: Record<string, string>;
},
nextLoad: (url: string, context: object) => Promise<LoadFnOutput>
): Promise<LoadFnOutput>;
interface LoadFnOutput {
source: string | ArrayBuffer;
format?: "builtin" | "commonjs" | "json" | "module" | "wasm";
shortCircuit?: boolean;
}Usage Examples:
// The loader automatically transforms files during import
// app.mts
import { readFile } from 'node:fs/promises';
import { processData } from './processor.ts'; // Automatically transformed
const data = await readFile('./data.json', 'utf-8');
const result = processData(JSON.parse(data));
console.log(result);
// processor.ts - This file is transformed by the loader
interface DataItem {
id: number;
name: string;
}
export function processData(items: DataItem[]): DataItem[] {
return items.filter(item => item.id > 0).sort((a, b) => a.name.localeCompare(b.name));
}Determines whether a package or file should be treated as CommonJS or ES Module based on package.json configuration.
/**
* Determines package type (module or commonjs) by traversing up to find package.json
* @param url - File URL to check
* @returns Promise resolving to package type or undefined if not determinable
*/
function getPackageType(url: string): Promise<"module" | "commonjs" | undefined>;Usage Examples:
// The getPackageType function is used internally by the ESM loader
// It determines package type based on the nearest package.json file
// This happens automatically during module resolution
// When importing a .js file, the loader checks package.json:
// { "type": "module" } -> treated as ESM
// { "type": "commonjs" } or no type -> treated as CommonJS
import { someFunction } from './utils.js'; // Package type determined automaticallyThe ESM resolver provides advanced resolution capabilities:
Supports tsconfig.json path mapping for module resolution:
// tsconfig.json
{
"compilerOptions": {
"baseUrl": "./src",
"paths": {
"@/*": ["./src/*"],
"@utils/*": ["./src/utils/*"],
"@components/*": ["./src/components/*"]
}
}
}// These imports are resolved using path mapping
import { helper } from '@/helpers/utility'; // -> ./src/helpers/utility.ts
import { Button } from '@components/Button'; // -> ./src/components/Button.tsx
import { formatDate } from '@utils/date'; // -> ./src/utils/date.tsAutomatically resolves TypeScript extensions and provides fallbacks:
// Extension mapping and resolution
const extensionAlias = {
'.js': ['.ts', '.tsx', '.js'], // .js imports can resolve to .ts/.tsx
'.mjs': ['.mts', '.mjs'], // .mjs imports can resolve to .mts
'.cjs': ['.cts', '.cjs'] // .cjs imports can resolve to .cts
};
const supportedExtensions = [
'.js', '.mjs', '.cjs',
'.ts', '.tsx', '.mts', '.cts',
'.json', '.wasm', '.node'
];Properly handles Node.js built-in modules:
// These imports are handled as built-ins
import { readFile } from 'node:fs/promises'; // -> 'builtin' format
import { URL } from 'node:url'; // -> 'builtin' format
import path from 'path'; // -> 'node:path' with 'builtin' formatSimplified registration for ES Module environments:
// esm-register.mts automatically registers the loader
import { register } from 'node:module';
import { pathToFileURL } from 'node:url';
register('@swc-node/register/esm', pathToFileURL('./').toString());Usage Examples:
# Using the esm-register helper
node --loader @swc-node/register/esm-register app.mts
# Direct loader registration
node --loader @swc-node/register/esm app.mts
# With environment variables
SWC_NODE_PROJECT=./tsconfig.esm.json node --loader @swc-node/register/esm app.mtsThe resolver uses oxc-resolver with TypeScript-aware configuration:
interface ResolverOptions {
tsconfig: {
configFile: string;
references: "auto";
};
conditionNames: string[];
enforceExtension: "auto";
extensions: string[];
extensionAlias: Record<string, string[]>;
moduleType: boolean;
}The ESM loader provides comprehensive error handling:
// Resolution fallback chain:
// 1. TypeScript-aware resolution with path mapping
// 2. Standard Node.js resolution
// 3. CommonJS resolution (for compatibility)
try {
// Primary TypeScript resolution
const resolved = await resolver.async(basePath, specifier);
return { url: resolved.path, format: getFormat(resolved) };
} catch (tsError) {
try {
// Fallback to Node.js resolver
const nodeResolution = await nextResolve(specifier);
return nodeResolution;
} catch (nodeError) {
try {
// Final fallback to CommonJS resolution
const cjsPath = createRequire(process.cwd()).resolve(specifier);
return { url: pathToFileURL(cjsPath).toString(), format: 'commonjs' };
} catch (cjsError) {
throw nodeError; // Throw the Node.js error as most relevant
}
}
}The loader handles special URL formats and import attributes:
// Data URLs are passed through unchanged
import data from 'data:application/json,{"test":true}';
// Import attributes are respected (Node.js 18.20+)
import config from './config.json' with { type: 'json' };
// Node.js built-ins with node: prefix
import { readFile } from 'node:fs/promises';