/** * ESM hooks for running integration tests without `tsx`. * * The sandbox blocks esbuild's pipe-based service-worker spawn (EPERM), so * this loader performs the TypeScript transform in-process with the locally * installed `typescript` package (ts.transpileModule — pure JS, no child * process), resolves the tsconfig `@/*` path alias, probes extensions for * extensionless relative imports, and falls back to raw package-subpath * files (`next/headers` etc.) that lack an exports map. * * Usage: node --import ./tests/integration/tsconfig-paths-loader.mjs tests/integration/.test.ts */ import { pathToFileURL, fileURLToPath } from "node:url"; import { existsSync, readFileSync, statSync } from "node:fs"; import { dirname, join, resolve as pathResolve } from "node:path"; import ts from "typescript"; const ROOT = pathResolve(process.cwd()); const EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]; function isFile(p) { try { return statSync(p).isFile(); } catch { return false; } } /** Resolve a specifier target, probing extensions and index files. */ function probe(base) { if (isFile(base)) return base; for (const ext of EXTENSIONS) { if (isFile(base + ext)) return base + ext; } for (const ext of EXTENSIONS) { if (isFile(join(base, "index" + ext))) return join(base, "index" + ext); } return null; } /** Fallback for bare package subpaths (e.g. `next/headers`) that lack an exports map. */ function probePackage(spec) { const base = pathResolve(ROOT, "node_modules", spec); for (const ext of [".js", ".mjs", ".cjs", ".json"]) { if (isFile(base + ext)) return base + ext; } if (isFile(base)) return base; return null; } export async function resolve(specifier, context, nextResolve) { if (specifier.startsWith("@/")) { const target = probe(pathResolve(ROOT, "src", specifier.slice(2))); if (target) return { url: pathToFileURL(target).href, shortCircuit: true }; } else if ( (specifier.startsWith("./") || specifier.startsWith("../")) && context.parentURL ) { const parent = fileURLToPath(context.parentURL); const target = probe(pathResolve(dirname(parent), specifier)); if (target) return { url: pathToFileURL(target).href, shortCircuit: true }; } else if (!specifier.startsWith("node:")) { try { return await nextResolve(specifier, context); } catch (error) { const target = probePackage(specifier); if (target) return { url: pathToFileURL(target).href, shortCircuit: true }; throw error; } } return nextResolve(specifier, context); } export async function load(url, context, nextLoad) { if (url.startsWith("file:")) { const filePath = fileURLToPath(url); if (/\.(ts|tsx|mts|cts)$/.test(filePath)) { const source = readFileSync(filePath, "utf8"); const out = ts.transpileModule(source, { fileName: filePath, reportDiagnostics: false, compilerOptions: { module: ts.ModuleKind.ESNext, moduleResolution: ts.ModuleResolutionKind.Bundler, target: ts.ScriptTarget.ES2022, isolatedModules: true, esModuleInterop: true, allowJs: true, jsx: ts.JsxEmit.ReactJSX, verbatimModuleSyntax: false, }, }); return { format: "module", source: out.outputText, shortCircuit: true }; } } return nextLoad(url, context); }