import { existsSync , readFileSync , writeFileSync } from 'fs' ; import { Module , builtinModules } from 'module' ; import { dirname , join , basename , extname } from 'path' ; import { tmpdir } from 'os' ; import vm from 'vm' ; import { fileURLToPath } from 'url' ; import mkdirp from 'mkdirp' ; import destr from 'destr' ; import createRequire from 'create-require' ; import semver from 'semver' ; import { addHook } from 'pirates' ; import objectHash from 'object-hash' ; import { interopDefault , isDir , isWritable , md5 , detectESMSyntax , detectLegacySyntax } from './utils' ; import { TransformOptions , JITIOptions } from './types' ; const _EnvDebug = destr ( process . env . JITI_DEBUG ) ; const _EnvCache = destr ( process . env . JITI_CACHE ) ; const _EnvRequireCache = destr ( process . env . JITI_REQUIRE_CACHE ) ; const defaults = { debug : _EnvDebug , cache : _EnvCache !== undefined ? ! ! _EnvCache : true , requireCache : _EnvRequireCache !== undefined ? ! ! _EnvRequireCache : true , interopDefault : false , cacheVersion : '6' , legacy : semver . lt ( process . version || '0.0.0' , '14.0.0' ) , extensions : [ '.js' , '.mjs' , '.cjs' , '.ts' ] } ; type Require = typeof require ; export interface JITI extends Require { transform : ( opts : TransformOptions ) => string ; register : ( ) => ( ( ) => void ) ; } export default function createJITI ( _filename = process . cwd ( ) , opts = { } , parentModule ? ) { opts = { ... defaults , ... opts } ; if ( opts . legacy ) { opts . cacheVersion += '-legacy' ; } if ( opts . transformOptions ) { opts . cacheVersion += '-' + objectHash ( opts . transformOptions ) ; } function debug ( ... args ) { if ( opts . debug ) { console . log ( '[jiti]' , ... args ) ; } } if ( isDir ( _filename ) ) { _filename = join ( _filename , 'index.js' ) ; } if ( opts . cache === true ) { opts . cache = join ( tmpdir ( ) , 'node-jiti' ) ; } if ( opts . cache ) { try { mkdirp . sync ( opts . cache as string ) ; if ( ! isWritable ( opts . cache ) ) { throw new Error ( 'directory is not writable' ) ; } } catch ( err ) { debug ( 'Error creating cache directory at ' , opts . cache , err ) ; opts . cache = false ; } } const nativeRequire = createRequire ( _filename ) ; const tryResolve = ( id , options ? ) => { try { return nativeRequire . resolve ( id , options ) ; } catch ( e ) { } } ; const _additionalExts = [ ... opts . extensions ! ] . filter ( ext => ext !== '.js' ) ; const _resolve = ( id , options ? ) => { if ( opts . extensions ! . includes ( extname ( id ) ) ) { return nativeRequire . resolve ( id , options ) ; } let resolved , err ; try { return nativeRequire . resolve ( id , options ) ; } catch ( _err ) { err = _err ; } for ( const ext of _additionalExts ) { resolved = tryResolve ( id + ext , options ) || tryResolve ( id + '/index' + ext , options ) ; if ( resolved ) { return resolved ; } } throw err ; } ; _resolve . paths = nativeRequire . resolve . paths ; function getCache ( filename , source , get ) { if ( ! opts . cache || ! filename ) { return get ( ) ; } const sourceHash = ` /* v${ opts . cacheVersion }-${ md5 ( source , 16 ) } */` ; const filebase = basename ( dirname ( filename ) ) + '-' + basename ( filename ) ; const cacheFile = join ( opts . cache as string , filebase + '.' + md5 ( filename ) + '.js' ) ; if ( existsSync ( cacheFile ) ) { const cacheSource = readFileSync ( cacheFile , 'utf-8' ) ; if ( cacheSource . endsWith ( sourceHash ) ) { debug ( '[cache hit]' , filename , '~>' , cacheFile ) ; return cacheSource ; } } debug ( '[cache miss]' , filename ) ; const result = get ( ) ; if ( ! result . includes ( '__JITI_ERROR__' ) ) { writeFileSync ( cacheFile , result + sourceHash , 'utf-8' ) ; } return result ; } function transform ( topts ) { let code = getCache ( topts . filename , topts . source , ( ) => { const res = opts . transform ! ( { legacy : opts . legacy , ... opts . transformOptions , ... topts } ) ; if ( res . error && opts . debug ) { debug ( res . error ) ; } return res . code ; } ) ; if ( code . startsWith ( '#!' ) ) { code = '// ' + code ; } return code ; } function _interopDefault ( mod ) { return opts . interopDefault ? interopDefault ( mod ) : mod ; } function jiti ( id ) { if ( id . startsWith ( 'node:' ) ) { id = id . substr ( 5 ) ; } else if ( id . startsWith ( 'file:' ) ) { id = fileURLToPath ( id ) ; } if ( builtinModules . includes ( id ) || id === '.pnp.js' ) { return nativeRequire ( id ) ; } const filename = _resolve ( id ) ; const ext = extname ( filename ) ; if ( ext && ! opts . extensions ! . includes ( ext ) ) { debug ( '[unknown]' , filename ) ; return nativeRequire ( id ) ; } if ( opts . requireCache && nativeRequire . cache [ filename ] ) { return _interopDefault ( nativeRequire . cache [ filename ] ?. exports ) ; } let source = readFileSync ( filename , 'utf-8' ) ; const isTypescript = ext === '.ts' ; const isNativeModule = ext === '.mjs' ; const isCommonJS = ext === '.cjs' ; const needsTranspile = ! isCommonJS && ( isTypescript || isNativeModule || detectESMSyntax ( source ) || ( opts . legacy && detectLegacySyntax ( source ) ) ) ; if ( needsTranspile ) { debug ( '[transpile]' , filename ) ; source = transform ( { filename , source , ts : isTypescript } ) ; } else { try { debug ( '[native]' , filename ) ; return _interopDefault ( nativeRequire ( id ) ) ; } catch ( err ) { debug ( 'Native require error:' , err ) ; debug ( '[fallback]' , filename ) ; source = transform ( { filename , source , ts : isTypescript } ) ; } } const mod = new Module ( filename ) ; mod . filename = filename ; if ( parentModule ) { mod . parent = parentModule ; if ( Array . isArray ( parentModule . children ) && ! parentModule . children . includes ( mod ) ) { parentModule . children . push ( mod ) ; } } mod . require = createJITI ( filename , opts , mod ) ; mod . path = dirname ( filename ) ; mod . paths = Module . _nodeModulePaths ( mod . path ) ; if ( opts . requireCache ) { nativeRequire . cache [ filename ] = mod ; } let compiled ; try { compiled = vm . runInThisContext ( Module . wrap ( source ) , { filename , lineOffset : 0 , displayErrors : false } ) ; } catch ( err ) { if ( opts . requireCache ) { delete nativeRequire . cache [ filename ] ; } opts . onError ! ( err ) ; } try { compiled ( mod . exports , mod . require , mod , mod . filename , dirname ( mod . filename ) ) ; } catch ( err ) { if ( opts . requireCache ) { delete nativeRequire . cache [ filename ] ; } opts . onError ! ( err ) ; } if ( mod . exports && mod . exports . __JITI_ERROR__ ) { const { filename , line , column , code , message } = mod . exports . __JITI_ERROR__ ; const loc = `${ filename }:${ line }:${ column }` ; const err = new Error ( `${ code }: ${ message } \n ${ loc }` ) ; Error . captureStackTrace ( err , jiti ) ; opts . onError ! ( err ) ; } mod . loaded = true ; const _exports = _interopDefault ( mod . exports ) ; return _exports ; } function register ( ) { return addHook ( ( source , filename ) => jiti . transform ( { source , filename , ts : ! ! filename . match ( /.ts$/ ) } ) , { exts : opts . extensions } ) ; } jiti . resolve = _resolve ; jiti . cache = opts . requireCache ? nativeRequire . cache : { } ; jiti . extensions = nativeRequire . extensions ; jiti . main = nativeRequire . main ; jiti . transform = transform ; jiti . register = register ; return jiti ; }