diff --git a/lib/types.d.ts b/lib/types.d.ts index 4efb2b56..1a1bd12c 100644 --- a/lib/types.d.ts +++ b/lib/types.d.ts @@ -35,12 +35,22 @@ export interface Jiti extends NodeRequire { /** * Transform source code */ - transform: (opts: TransformOptions) => string; + transform: ( + opts: TransformOptions, + sourceTransformer?: (source: string) => string, + ) => Promise; /** * Evaluate transformed code as a module */ - evalModule: (source: string, options?: EvalModuleOptions) => unknown; + evalModule: ( + source: string, + options?: EvalModuleOptions, + sourceTransformer?: ( + source: string, + filename: string, + ) => Promise | string, + ) => unknown; } /** diff --git a/src/cache.ts b/src/cache.ts index a8204775..cb14d556 100644 --- a/src/cache.ts +++ b/src/cache.ts @@ -7,11 +7,11 @@ import { debug, isWritable, md5 } from "./utils"; const CACHE_VERSION = "9"; -export function getCache( +export async function getCache( ctx: Context, topts: TransformOptions, - get: () => string, -): string { + get: () => Promise | string, +): Promise { if (!ctx.opts.fsCache || !topts.filename) { return get(); } @@ -41,7 +41,7 @@ export function getCache( } debug(ctx, "[cache]", "[miss]", topts.filename); - const result = get(); + const result = await Promise.resolve(get()); if (!result.includes("__JITI_ERROR__")) { writeFileSync(cacheFilePath, result + sourceHash, "utf8"); diff --git a/src/eval.ts b/src/eval.ts index 9cd80dd4..2b848d4f 100644 --- a/src/eval.ts +++ b/src/eval.ts @@ -15,11 +15,15 @@ import { jitiRequire, nativeImportOrRequire } from "./require"; import createJiti from "./jiti"; import { transform } from "./transform"; -export function evalModule( +export async function evalModule( ctx: Context, source: string, evalOptions: EvalModuleOptions = {}, -): any { + sourceTransformer?: ( + source: string, + filename: string, + ) => Promise | string, +): Promise { // Resolve options const id = evalOptions.id || @@ -45,13 +49,17 @@ export function evalModule( (isTypescript || isESM || ctx.isTransformRe.test(filename) || hasESMSyntax(source))); const start = performance.now(); if (needsTranspile) { - source = transform(ctx, { - filename, - source, - ts: isTypescript, - async: evalOptions.async ?? false, - jsx: ctx.opts.jsx, - }); + source = await transform( + ctx, + { + filename, + source, + ts: isTypescript, + async: evalOptions.async ?? false, + jsx: ctx.opts.jsx, + }, + sourceTransformer, + ); const time = Math.round((performance.now() - start) * 1000) / 1000; debug( ctx, @@ -85,13 +93,17 @@ export function evalModule( } catch (error: any) { debug(ctx, "Native require error:", error); debug(ctx, "[fallback]", filename); - source = transform(ctx, { - filename, - source, - ts: isTypescript, - async: evalOptions.async ?? false, - jsx: ctx.opts.jsx, - }); + source = await transform( + ctx, + { + filename, + source, + ts: isTypescript, + async: evalOptions.async ?? false, + jsx: ctx.opts.jsx, + }, + sourceTransformer, + ); } } } @@ -118,6 +130,7 @@ export function evalModule( nativeImport: ctx.nativeImport, onError: ctx.onError, createRequire: ctx.createRequire, + callbackStore: ctx.callbackStore, // Pass the callback store }, true /* isNested */, ); diff --git a/src/jiti.ts b/src/jiti.ts index b4d1103d..7238faec 100644 --- a/src/jiti.ts +++ b/src/jiti.ts @@ -5,6 +5,7 @@ import type { Context, EvalModuleOptions, JitiResolveOptions, + SourceTransformer, } from "./types"; import { platform } from "node:os"; import { pathToFileURL } from "mlly"; @@ -32,6 +33,7 @@ export default function createJiti( | "nativeImport" | "onError" | "createRequire" + | "callbackStore" >, isNested = false, ): Jiti { @@ -96,6 +98,7 @@ export default function createJiti( parentCache: parentContext.parentCache, nativeImport: parentContext.nativeImport, createRequire: parentContext.createRequire, + callbackStore: parentContext.callbackStore, }; // Debug @@ -135,11 +138,15 @@ export default function createJiti( paths: nativeRequire.resolve.paths, }, ), - transform(opts: TransformOptions) { - return transform(ctx, opts); + transform(opts: TransformOptions, sourceTransformer?: SourceTransformer) { + return transform(ctx, opts, sourceTransformer); }, - evalModule(source: string, options?: EvalModuleOptions) { - return evalModule(ctx, source, options); + evalModule( + source: string, + options?: EvalModuleOptions, + sourceTransformer?: SourceTransformer, + ) { + return evalModule(ctx, source, options, sourceTransformer); }, async import( id: string, diff --git a/src/transform.ts b/src/transform.ts index 78509319..ef1b08bc 100644 --- a/src/transform.ts +++ b/src/transform.ts @@ -1,30 +1,56 @@ -import type { Context, TransformOptions } from "./types"; +import type { Context, TransformOptions, SourceTransformer } from "./types"; import { getCache } from "./cache"; import { debug } from "./utils"; -export function transform(ctx: Context, topts: TransformOptions): string { - let code = getCache(ctx, topts, () => { +export async function transform( + ctx: Context, + topts: TransformOptions, + sourceTransformer?: SourceTransformer, +): Promise { + if (!topts.filename) { + throw new Error("transform: filename is required"); + } + + const filename = topts.filename; + + ctx.callbackStore ??= new Map(); + if (sourceTransformer) { + ctx.callbackStore.set("sourceTransformer", sourceTransformer); + } + + return await getCache(ctx, topts, async () => { + let source = topts.source; + const transformer = ctx.callbackStore?.get("sourceTransformer"); + + if (transformer) { + try { + source = await Promise.resolve(transformer(source, filename)); + } catch (error_) { + debug(ctx, "Source transformer error:", error_); + } + } + const res = ctx.opts.transform!({ + ...topts, ...ctx.opts.transformOptions, babel: { ...(ctx.opts.sourceMaps ? { - sourceFileName: topts.filename, + sourceFileName: filename, sourceMaps: "inline", } : {}), ...ctx.opts.transformOptions?.babel, }, interopDefault: ctx.opts.interopDefault, - ...topts, + source, }); + if (res.error && ctx.opts.debug) { debug(ctx, res.error); } - return res.code; + + const code = res.code; + return code.startsWith("#!") ? "// " + code : code; }); - if (code.startsWith("#!")) { - code = "// " + code; - } - return code; } diff --git a/src/types.ts b/src/types.ts index be7faec7..d139809a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -25,4 +25,16 @@ export interface Context { additionalExts: string[]; nativeRequire: NodeRequire; createRequire: (typeof import("node:module"))["createRequire"]; + callbackStore?: Map; +} + +export type SourceTransformer = ( + source: string, + filename: string, +) => Promise | string; + +export interface CacheOptions { + key: string; + invalidate?: boolean; + transform?: () => Promise | string; } diff --git a/test/fixtures.test.ts b/test/fixtures.test.ts index 07652f61..330c9065 100644 --- a/test/fixtures.test.ts +++ b/test/fixtures.test.ts @@ -49,6 +49,8 @@ describe("fixtures", async () => { .replace(" ^", " ^") // eslint-disable-next-line no-control-regex .replace(/\u001B\[[\d;]*m/gu, "") + .replace(/^filename.*$/gm, "") // Remove filename lines + .replace(/\n+/g, "\n") // Remove extra newlines .trim() ); }