Files
scan-receipts/tests/integration/tsconfig-paths-hooks.mjs
Timo 84b9987c49 Add full application: receipt scanning, auth, billing, and account deletion
Brings the working codebase (Next.js app, auth system, Stripe billing,
Docker/deploy config, tests, docs) into version control on top of the
placeholder initial commit, and adds account self-deletion (Danger Zone
in Settings, password + typed-email confirmation, cascading DB cleanup,
Stripe cancellation) per GDPR right-to-erasure.

Excludes local build caches, node_modules, and internal agent scratch
files; .gitignore hardened to keep those out going forward.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 20:59:04 +02:00

99 lines
3.3 KiB
JavaScript

/**
* 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/<suite>.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);
}