11#!/usr/bin/env node
2- import { promises as fs } from 'node:fs' ;
3- import { createRequire } from 'node:module' ;
2+ import { promises as fs , readFileSync } from 'node:fs' ;
3+ import ModuleConstructor , { createRequire } from 'node:module' ;
44import * as path from 'node:path' ;
55import { fileURLToPath , pathToFileURL } from 'node:url' ;
66import {
@@ -11,6 +11,18 @@ import {
1111 type SyncResult ,
1212} from './index' ;
1313
14+ const TYPE_SCRIPT_EXTENSIONS = new Set ( [ '.ts' , '.tsx' , '.cts' , '.mts' ] ) ;
15+
16+ type TypeScriptModule = typeof import ( 'typescript' ) ;
17+
18+ type TypeScriptSupportState = {
19+ mode : 'require' | 'import' ;
20+ source: string ;
21+ } ;
22+
23+ let cachedTypeScriptSupport : TypeScriptSupportState | undefined ;
24+ let manualTypeScriptHookInstalled = false ;
25+
1426type CliRunOptions = {
1527 cwd ?: string ;
1628 stdout ? : Pick < NodeJS . WritableStream , 'write' > ;
@@ -259,7 +271,7 @@ async function loadConfigModule({
259271
260272 let imported : Record < string , unknown > ;
261273 try {
262- imported = await import ( url ) ;
274+ imported = await importResolvedModule ( { resolvedPath , url, packageDir } ) ;
263275 } catch ( error ) {
264276 const message = error instanceof Error ? error . message : String ( error ) ;
265277 throw new Error (
@@ -306,6 +318,246 @@ function resolveModuleSpecifier(
306318 }
307319}
308320
321+ async function importResolvedModule ( {
322+ resolvedPath,
323+ url,
324+ packageDir,
325+ } : {
326+ resolvedPath : string ;
327+ url : string ;
328+ packageDir : string ;
329+ } ) : Promise < Record < string , unknown >> {
330+ if ( ! isTypeScriptModule ( resolvedPath ) ) {
331+ return ( await import ( url ) ) as Record < string , unknown > ;
332+ }
333+
334+ const requireFromPkg = createRequire ( path . join ( packageDir , 'package.json' ) ) ;
335+ const support = await ensureTypeScriptSupport ( resolvedPath , requireFromPkg ) ;
336+
337+ if ( support . mode === 'require' ) {
338+ try {
339+ return requireFromPkg ( resolvedPath ) as Record < string , unknown> ;
340+ } catch ( error ) {
341+ if ( isErrRequireEsm ( error ) ) {
342+ return ( await import ( url ) ) as Record < string , unknown > ;
343+ }
344+ throw error ;
345+ }
346+ }
347+
348+ return ( await import ( url ) ) as Record < string , unknown > ;
349+ }
350+
351+ function isTypeScriptModule ( filePath : string ) : boolean {
352+ if ( filePath . endsWith ( '.d.ts' ) ) {
353+ return false ;
354+ }
355+
356+ const extension = path . extname ( filePath ) . toLowerCase ( ) ;
357+ return TYPE_SCRIPT_EXTENSIONS . has ( extension ) ;
358+ }
359+
360+ type LoaderAttemptResult =
361+ | { status : 'loaded' ; state: TypeScriptSupportState }
362+ | { status : 'missing' }
363+ | { status : 'failed' ; message: string } ;
364+
365+ async function ensureTypeScriptSupport (
366+ modulePath : string ,
367+ requireFromPkg : NodeJS . Require ,
368+ ) : Promise < TypeScriptSupportState > {
369+ if ( cachedTypeScriptSupport ) {
370+ return cachedTypeScriptSupport ;
371+ }
372+
373+ const missing : string [ ] = [ ] ;
374+ const errors : string [ ] = [ ] ;
375+
376+ const candidates : Array < { label : string ; run: ( ) => Promise < LoaderAttemptResult > } > = [
377+ {
378+ label : 'ts-node/register/transpile-only' ,
379+ run : ( ) => registerCommonJsLoader ( requireFromPkg , 'ts-node/register/transpile-only' ) ,
380+ } ,
381+ {
382+ label : 'ts-node/register' ,
383+ run : ( ) => registerCommonJsLoader ( requireFromPkg , 'ts-node/register' ) ,
384+ } ,
385+ {
386+ label : 'typescript' ,
387+ run : ( ) => installManualTypeScriptHook ( requireFromPkg ) ,
388+ } ,
389+ ] ;
390+
391+ for ( const candidate of candidates ) {
392+ const result = await candidate . run ( ) ;
393+
394+ if ( result . status === 'loaded' ) {
395+ cachedTypeScriptSupport = result . state ;
396+ return result . state ;
397+ }
398+
399+ if ( result . status === 'missing' ) {
400+ missing . push ( candidate . label ) ;
401+ continue ;
402+ }
403+
404+ errors . push ( `${ candidate . label } : ${ result . message } ` ) ;
405+ }
406+
407+ const messageLines = [
408+ `Unable to load TypeScript configuration module at ${ modulePath } .` ,
409+ 'Install one of "ts-node" or "typescript" in your project to enable TypeScript configs.' ,
410+ ] ;
411+
412+ if ( missing . length > 0 ) {
413+ messageLines . push ( `Missing dependencies: ${ missing . join ( ', ' ) } ` ) ;
414+ }
415+
416+ if ( errors . length > 0 ) {
417+ messageLines . push ( 'Errors encountered while initialising TypeScript support:' ) ;
418+ for ( const error of errors ) {
419+ messageLines . push ( ` - ${ error } ` ) ;
420+ }
421+ }
422+
423+ throw new Error ( messageLines . join ( '\n' ) ) ;
424+ }
425+
426+ async function registerCommonJsLoader (
427+ requireFromPkg : NodeJS . Require ,
428+ specifier : string ,
429+ ) : Promise < LoaderAttemptResult > {
430+ try {
431+ requireFromPkg ( specifier ) ;
432+ return { status: 'loaded' , state : { mode : 'require' , source : specifier } } ;
433+ } catch ( error ) {
434+ if ( isModuleNotFoundError ( error , specifier ) ) {
435+ return { status : 'missing' } ;
436+ }
437+
438+ const message = error instanceof Error ? error . message : String ( error ) ;
439+ return { status : 'failed' , message } ;
440+ }
441+ }
442+
443+ async function installManualTypeScriptHook (
444+ requireFromPkg : NodeJS . Require ,
445+ ) : Promise < LoaderAttemptResult > {
446+ if ( manualTypeScriptHookInstalled ) {
447+ return { status : 'loaded' , state : { mode : 'require' , source : 'typescript' } } ;
448+ }
449+
450+ let typescript : TypeScriptModule ;
451+ try {
452+ typescript = requireFromPkg ( 'typescript' ) as TypeScriptModule ;
453+ } catch ( error ) {
454+ if ( isModuleNotFoundError ( error , 'typescript' ) ) {
455+ return { status : 'missing' } ;
456+ }
457+
458+ const message = error instanceof Error ? error . message : String ( error ) ;
459+ return { status : 'failed' , message } ;
460+ }
461+
462+ const extensions = (
463+ ModuleConstructor as unknown as {
464+ _extensions : Record < string , ( module : NodeJS . Module , filename : string ) => void > ;
465+ }
466+ ) . _extensions ;
467+
468+ for ( const extension of TYPE_SCRIPT_EXTENSIONS ) {
469+ if ( ! extensions [ extension ] ) {
470+ extensions [ extension ] = createTypeScriptExtensionHandler ( typescript , extension ) ;
471+ }
472+ }
473+
474+ manualTypeScriptHookInstalled = true ;
475+ return { status : 'loaded' , state : { mode : 'require' , source : 'typescript' } } ;
476+ }
477+
478+ function createTypeScriptExtensionHandler (
479+ typescript : TypeScriptModule ,
480+ extension : string ,
481+ ) : ( module : NodeJS . Module , filename : string ) => void {
482+ return ( module , filename ) => {
483+ const source = readFileSync ( filename , 'utf8' ) ;
484+ const result = typescript . transpileModule ( source , {
485+ compilerOptions : createTypeScriptTranspileOptions ( typescript , extension ) ,
486+ fileName : filename ,
487+ reportDiagnostics : true ,
488+ } ) ;
489+
490+ if ( result . diagnostics && result . diagnostics . length > 0 ) {
491+ const formatted = formatTypeScriptDiagnostics ( typescript , result . diagnostics , filename ) ;
492+ throw new Error ( `Failed to compile ${ filename } :\n${ formatted } ` ) ;
493+ }
494+
495+ const compiled = module as unknown as { _compile ( code : string , filename : string ) : void } ;
496+ compiled . _compile ( result . outputText , filename ) ;
497+ } ;
498+ }
499+
500+ function createTypeScriptTranspileOptions (
501+ typescript : TypeScriptModule ,
502+ extension : string ,
503+ ) : import ( 'typescript' ) . CompilerOptions {
504+ const options : import ( 'typescript' ) . CompilerOptions = {
505+ module : typescript . ModuleKind . CommonJS ,
506+ target : typescript . ScriptTarget . ES2020 ,
507+ esModuleInterop : true ,
508+ sourceMap : false ,
509+ allowSyntheticDefaultImports : true ,
510+ resolveJsonModule : true ,
511+ } ;
512+
513+ if ( extension === '.tsx' ) {
514+ options . jsx = typescript . JsxEmit . React ;
515+ }
516+
517+ return options ;
518+ }
519+
520+ function formatTypeScriptDiagnostics (
521+ typescript : TypeScriptModule ,
522+ diagnostics : readonly import ( 'typescript' ) . Diagnostic [ ] ,
523+ filename : string ,
524+ ) : string {
525+ const host : import ( 'typescript' ) . FormatDiagnosticsHost = {
526+ getCanonicalFileName : ( fileName ) => fileName ,
527+ getCurrentDirectory : ( ) => path . dirname ( filename ) ,
528+ getNewLine : ( ) => '\n' ,
529+ } ;
530+
531+ return typescript . formatDiagnostics ( diagnostics , host ) ;
532+ }
533+
534+ function isModuleNotFoundError ( error : unknown , specifier : string ) : boolean {
535+ if ( ! error || typeof error !== 'object' ) {
536+ return false ;
537+ }
538+
539+ const code = ( error as NodeJS . ErrnoException ) . code ;
540+ if ( code !== 'MODULE_NOT_FOUND' ) {
541+ return false ;
542+ }
543+
544+ const message = ( error as NodeJS . ErrnoException ) . message ;
545+ if ( typeof message !== 'string' ) {
546+ return false ;
547+ }
548+
549+ return message . includes ( `'${ specifier } '` ) ;
550+ }
551+
552+ function isErrRequireEsm ( error : unknown ) : boolean {
553+ return Boolean (
554+ error &&
555+ typeof error === 'object' &&
556+ 'code' in ( error as Record < string , unknown > ) &&
557+ ( error as NodeJS . ErrnoException ) . code === 'ERR_REQUIRE_ESM' ,
558+ ) ;
559+ }
560+
309561function selectConfigExport ( imported : Record < string , unknown > , resolvedPath : string ) : unknown {
310562 if ( 'default' in imported && imported . default !== undefined ) {
311563 return imported . default ;
0 commit comments