or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

code-compilation.mdconfiguration-management.mdesm-loader-support.mdindex.mdruntime-registration.md
tile.json

esm-loader-support.mddocs/

ESM Loader Support

Node.js ES Module loader hooks for module resolution and transformation with TypeScript path mapping and package type detection.

Capabilities

ESM Module Resolver

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';

ESM Module Loader

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));
}

Package Type Detection

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 automatically

Module Resolution Features

The ESM resolver provides advanced resolution capabilities:

TypeScript Path Mapping

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.ts

Extension Resolution

Automatically 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'
];

Built-in Module Handling

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' format

ESM Registration Helper

Simplified 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.mts

Resolver Configuration

The 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;
}

Error Handling and Fallbacks

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
    }
  }
}

Data URL and Special Handling

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';