1- import { type Options , bundleRequire } from 'bundle-require ' ;
1+ import { type JitiOptions , createJiti as createJitiSource } from 'jiti ' ;
22import { stat } from 'node:fs/promises' ;
3+ import path from 'node:path' ;
4+ import type { CompilerOptions } from 'typescript' ;
5+ import { fileExists } from './file-system' ;
6+ import { loadTargetConfig } from './load-ts-config' ;
37import { settlePromise } from './promises.js' ;
48
5- export async function importModule < T = unknown > ( options : Options ) : Promise < T > {
9+ export async function importModule < T = unknown > (
10+ options : JitiOptions & { filepath : string ; tsconfig ?: string } ,
11+ ) : Promise < T > {
12+ const { filepath, tsconfig, ...jitiOptions } = options ;
13+
614 const resolvedStats = await settlePromise ( stat ( options . filepath ) ) ;
715 if ( resolvedStats . status === 'rejected' ) {
816 throw new Error ( `File '${ options . filepath } ' does not exist` ) ;
@@ -11,10 +19,154 @@ export async function importModule<T = unknown>(options: Options): Promise<T> {
1119 throw new Error ( `Expected '${ options . filepath } ' to be a file` ) ;
1220 }
1321
14- const { mod } = await bundleRequire < object > ( options ) ;
22+ const jitiInstance = await createTsJiti ( options . filepath , {
23+ ...jitiOptions ,
24+ tsconfigPath : options . tsconfig ,
25+ } ) ;
26+ return ( await jitiInstance . import ( filepath , { default : true } ) ) as T ;
27+ }
28+
29+ /**
30+ * Converts TypeScript paths configuration to jiti alias format
31+ * @param paths TypeScript paths object from compiler options
32+ * @param baseUrl Base URL for resolving relative paths
33+ * @returns Jiti alias object with absolute paths
34+ */
35+ export function mapTsPathsToJitiAlias (
36+ paths : Record < string , string [ ] > ,
37+ baseUrl : string ,
38+ ) : Record < string , string > {
39+ return Object . entries ( paths ) . reduce (
40+ ( aliases , [ pathPattern , pathMappings ] ) => {
41+ if ( ! Array . isArray ( pathMappings ) || pathMappings . length === 0 ) {
42+ return aliases ;
43+ }
44+ // Jiti does not support overloads (multiple mappings for the same path pattern)
45+ if ( pathMappings . length > 1 ) {
46+ throw new Error (
47+ `TypeScript path overloads are not supported by jiti. Path pattern '${ pathPattern } ' has ${ pathMappings . length } mappings: ${ pathMappings . join ( ', ' ) } . Jiti only supports a single alias mapping per pattern.` ,
48+ ) ;
49+ }
50+ const aliasKey = pathPattern . replace ( / \/ \* $ / , '' ) ;
51+ const aliasValue = ( pathMappings . at ( 0 ) as string ) . replace ( / \/ \* $ / , '' ) ;
52+ return {
53+ ...aliases ,
54+ [ aliasKey ] : path . isAbsolute ( aliasValue )
55+ ? aliasValue
56+ : path . resolve ( baseUrl , aliasValue ) ,
57+ } ;
58+ } ,
59+ { } satisfies Record < string , string > ,
60+ ) ;
61+ }
62+
63+ /**
64+ * Maps TypeScript JSX emit mode to Jiti JSX boolean option
65+ * @param tsJsxMode TypeScript JsxEmit enum value (0-5)
66+ * @returns true if JSX processing should be enabled, false otherwise
67+ */
68+ export const mapTsJsxToJitiJsx = ( tsJsxMode : number ) : boolean =>
69+ tsJsxMode !== 0 ;
70+
71+ /**
72+ * Possible TS to jiti options mapping
73+ * | Jiti Option | Jiti Type | TS Option | TS Type | Description |
74+ * |-------------------|-------------------------|-----------------------|--------------------------|-------------|
75+ * | alias | Record<string, string> | paths | Record<string, string[]> | Module path aliases for module resolution. |
76+ * | interopDefault | boolean | esModuleInterop | boolean | Enable default import interop. |
77+ * | sourceMaps | boolean | sourceMap | boolean | Enable sourcemap generation. |
78+ * | jsx | boolean | jsx | JsxEmit (0-5) | TS JsxEmit enum (0-5) => boolean JSX processing. |
79+ */
80+ export type MappableJitiOptions = Partial <
81+ Pick < JitiOptions , 'alias' | 'interopDefault' | 'sourceMaps' | 'jsx' >
82+ > ;
83+
84+ /**
85+ * Parse TypeScript compiler options to mappable jiti options
86+ * @param compilerOptions TypeScript compiler options
87+ * @param tsconfigDir Directory of the tsconfig file (for resolving relative baseUrl)
88+ * @returns Mappable jiti options
89+ */
90+ export function parseTsConfigToJitiConfig (
91+ compilerOptions : CompilerOptions ,
92+ tsconfigDir ?: string ,
93+ ) : MappableJitiOptions {
94+ const paths = compilerOptions . paths || { } ;
95+ const baseUrl = compilerOptions . baseUrl
96+ ? path . isAbsolute ( compilerOptions . baseUrl )
97+ ? compilerOptions . baseUrl
98+ : tsconfigDir
99+ ? path . resolve ( tsconfigDir , compilerOptions . baseUrl )
100+ : path . resolve ( process . cwd ( ) , compilerOptions . baseUrl )
101+ : tsconfigDir || process . cwd ( ) ;
15102
16- if ( typeof mod === 'object' && 'default' in mod ) {
17- return mod . default as T ;
103+ return {
104+ ...( Object . keys ( paths ) . length > 0
105+ ? {
106+ alias : mapTsPathsToJitiAlias ( paths , baseUrl ) ,
107+ }
108+ : { } ) ,
109+ ...( compilerOptions . esModuleInterop == null
110+ ? { }
111+ : { interopDefault : compilerOptions . esModuleInterop } ) ,
112+ ...( compilerOptions . sourceMap == null
113+ ? { }
114+ : { sourceMaps : compilerOptions . sourceMap } ) ,
115+ ...( compilerOptions . jsx == null
116+ ? { }
117+ : { jsx : mapTsJsxToJitiJsx ( compilerOptions . jsx ) } ) ,
118+ } ;
119+ }
120+
121+ /**
122+ * Create a jiti instance with options derived from tsconfig.
123+ * Used instead of direct jiti.createJiti to allow tsconfig integration.
124+ * @param filepath
125+ * @param options
126+ * @param jiti
127+ */
128+ export async function createTsJiti (
129+ filepath : string ,
130+ options : JitiOptions & { tsconfigPath ?: string } ,
131+ createJiti : ( typeof import ( 'jiti' ) ) [ 'createJiti' ] = createJitiSource ,
132+ ) {
133+ const { tsconfigPath, ...jitiOptions } = options ;
134+ const fallbackTsconfigPath = path . resolve ( './tsconfig.json' ) ;
135+ const tsDerivedJitiOptions : MappableJitiOptions = tsconfigPath
136+ ? await jitiOptionsFromTsConfig ( tsconfigPath )
137+ : ( await fileExists ( fallbackTsconfigPath ) )
138+ ? await jitiOptionsFromTsConfig ( tsconfigPath )
139+ : { } ;
140+ return createJiti ( filepath , { ...jitiOptions , ...tsDerivedJitiOptions } ) ;
141+ }
142+
143+ /**
144+ * Read tsconfig file and parse options to jiti options
145+ * @param tsconfigPath
146+ */
147+ export async function jitiOptionsFromTsConfig (
148+ tsconfigPath : string ,
149+ ) : Promise < MappableJitiOptions > {
150+ const compilerOptions = await deriveTsConfig ( tsconfigPath ) ;
151+ const tsconfigDir = path . dirname ( tsconfigPath ) ;
152+ return parseTsConfigToJitiConfig ( compilerOptions , tsconfigDir ) ;
153+ }
154+
155+ /**
156+ * Read tsconfig file by path and return the parsed options as JSON object
157+ * @param tsconfigPath
158+ */
159+ export async function deriveTsConfig (
160+ tsconfigPath : string ,
161+ ) : Promise < CompilerOptions > {
162+ // check if tsconfig file exists
163+ const exists = await fileExists ( tsconfigPath ) ;
164+ if ( ! exists ) {
165+ throw new Error (
166+ `Tsconfig file not found at path: ${ tsconfigPath . replace ( / \\ / g, '/' ) } ` ,
167+ ) ;
18168 }
19- return mod as T ;
169+
170+ const { options } = loadTargetConfig ( tsconfigPath ) ;
171+ return options ;
20172}
0 commit comments