@@ -120,6 +120,40 @@ export default class ScriptTransformer {
120120 }
121121 }
122122
123+ private async _getCacheKeyAsync (
124+ fileData : string ,
125+ filename : Config . Path ,
126+ instrument : boolean ,
127+ supportsDynamicImport : boolean ,
128+ supportsStaticESM : boolean ,
129+ ) : Promise < string > {
130+ const configString = this . _cache . configString ;
131+ const transformer = await this . _getTransformerAsync ( filename ) ;
132+
133+ if ( transformer && typeof transformer . getCacheKeyAsync === 'function' ) {
134+ return createHash ( 'md5' )
135+ . update (
136+ await transformer . getCacheKeyAsync ( fileData , filename , configString , {
137+ config : this . _config ,
138+ instrument,
139+ rootDir : this . _config . rootDir ,
140+ supportsDynamicImport,
141+ supportsStaticESM,
142+ } ) ,
143+ )
144+ . update ( CACHE_VERSION )
145+ . digest ( 'hex' ) ;
146+ } else {
147+ return createHash ( 'md5' )
148+ . update ( fileData )
149+ . update ( configString )
150+ . update ( instrument ? 'instrument' : '' )
151+ . update ( filename )
152+ . update ( CACHE_VERSION )
153+ . digest ( 'hex' ) ;
154+ }
155+ }
156+
123157 private _getFileCachePath (
124158 filename : Config . Path ,
125159 content : string ,
@@ -153,6 +187,39 @@ export default class ScriptTransformer {
153187 return cachePath ;
154188 }
155189
190+ private async _getFileCachePathAsync (
191+ filename : Config . Path ,
192+ content : string ,
193+ instrument : boolean ,
194+ supportsDynamicImport : boolean ,
195+ supportsStaticESM : boolean ,
196+ ) : Promise < Config . Path > {
197+ const baseCacheDir = HasteMap . getCacheFilePath (
198+ this . _config . cacheDirectory ,
199+ 'jest-transform-cache-' + this . _config . name ,
200+ VERSION ,
201+ ) ;
202+ const cacheKey = await this . _getCacheKeyAsync (
203+ content ,
204+ filename ,
205+ instrument ,
206+ supportsDynamicImport ,
207+ supportsStaticESM ,
208+ ) ;
209+ // Create sub folders based on the cacheKey to avoid creating one
210+ // directory with many files.
211+ const cacheDir = path . join ( baseCacheDir , cacheKey [ 0 ] + cacheKey [ 1 ] ) ;
212+ const cacheFilenamePrefix = path
213+ . basename ( filename , path . extname ( filename ) )
214+ . replace ( / \W / g, '' ) ;
215+ const cachePath = slash (
216+ path . join ( cacheDir , cacheFilenamePrefix + '_' + cacheKey ) ,
217+ ) ;
218+ createDirectory ( cacheDir ) ;
219+
220+ return cachePath ;
221+ }
222+
156223 private _getTransformPath ( filename : Config . Path ) {
157224 const transformRegExp = this . _cache . transformRegExp ;
158225 if ( ! transformRegExp ) {
@@ -171,6 +238,40 @@ export default class ScriptTransformer {
171238 return undefined ;
172239 }
173240
241+ private async _getTransformerAsync ( filename : Config . Path ) {
242+ let transform : Transformer | null = null ;
243+ if ( ! this . _config . transform || ! this . _config . transform . length ) {
244+ return null ;
245+ }
246+
247+ const transformPath = this . _getTransformPath ( filename ) ;
248+ if ( transformPath ) {
249+ const transformer = this . _transformCache . get ( transformPath ) ;
250+ if ( transformer != null ) {
251+ return transformer ;
252+ }
253+
254+ transform = await import ( transformPath ) ;
255+
256+ if ( ! transform ) {
257+ throw new TypeError ( 'Jest: a transform must export something.' ) ;
258+ }
259+ const transformerConfig = this . _transformConfigCache . get ( transformPath ) ;
260+ if ( typeof transform . createTransformer === 'function' ) {
261+ transform = transform . createTransformer ( transformerConfig ) ;
262+ }
263+ if (
264+ typeof transform . process !== 'function' &&
265+ typeof transform . processAsync !== 'function' ) {
266+ throw new TypeError (
267+ 'Jest: a transform must export a `process` or `processAsync` function.' ,
268+ ) ;
269+ }
270+ this . _transformCache . set ( transformPath , transform ) ;
271+ }
272+ return transform ;
273+ }
274+
174275 private _getTransformer ( filename : Config . Path ) {
175276 let transform : Transformer | null = null ;
176277 if ( ! this . _config . transform || ! this . _config . transform . length ) {
@@ -390,6 +491,135 @@ export default class ScriptTransformer {
390491 } ;
391492 }
392493
494+ // TODO: replace third argument with TransformOptions in Jest 26
495+ async transformSourceAsync (
496+ filepath : Config . Path ,
497+ content : string ,
498+ instrument : boolean ,
499+ supportsDynamicImport = false ,
500+ supportsStaticESM = false ,
501+ ) : Promise < TransformResult > {
502+ const filename = this . _getRealPath ( filepath ) ;
503+ const transform = await this . _getTransformerAsync ( filename ) ;
504+ const cacheFilePath = await this . _getFileCachePathAsync (
505+ filename ,
506+ content ,
507+ instrument ,
508+ supportsDynamicImport ,
509+ supportsStaticESM ,
510+ ) ;
511+ let sourceMapPath : Config . Path | null = cacheFilePath + '.map' ;
512+ // Ignore cache if `config.cache` is set (--no-cache)
513+ let code = this . _config . cache ? readCodeCacheFile ( cacheFilePath ) : null ;
514+
515+ const shouldCallTransform = transform && this . shouldTransform ( filename ) ;
516+
517+ // That means that the transform has a custom instrumentation
518+ // logic and will handle it based on `config.collectCoverage` option
519+ const transformWillInstrument =
520+ shouldCallTransform && transform && transform . canInstrument ;
521+
522+ if ( code ) {
523+ // This is broken: we return the code, and a path for the source map
524+ // directly from the cache. But, nothing ensures the source map actually
525+ // matches that source code. They could have gotten out-of-sync in case
526+ // two separate processes write concurrently to the same cache files.
527+ return {
528+ code,
529+ originalCode : content ,
530+ sourceMapPath,
531+ } ;
532+ }
533+
534+ let transformed : TransformedSource = {
535+ code : content ,
536+ map : null ,
537+ } ;
538+
539+ if ( transform && shouldCallTransform ) {
540+ const processed = transform . process ( content , filename , this . _config , {
541+ instrument,
542+ supportsDynamicImport,
543+ supportsStaticESM,
544+ } ) ;
545+
546+ if ( typeof processed === 'string' ) {
547+ transformed . code = processed ;
548+ } else if ( processed != null && typeof processed . code === 'string' ) {
549+ transformed = processed ;
550+ } else {
551+ throw new TypeError (
552+ "Jest: a transform's `process` function must return a string, " +
553+ 'or an object with `code` key containing this string.' ,
554+ ) ;
555+ }
556+ }
557+
558+ if ( ! transformed . map ) {
559+ try {
560+ //Could be a potential freeze here.
561+ //See: https://github.com/facebook/jest/pull/5177#discussion_r158883570
562+ const inlineSourceMap = sourcemapFromSource ( transformed . code ) ;
563+ if ( inlineSourceMap ) {
564+ transformed . map = inlineSourceMap . toObject ( ) ;
565+ }
566+ } catch ( e ) {
567+ const transformPath = this . _getTransformPath ( filename ) ;
568+ console . warn (
569+ `jest-transform: The source map produced for the file ${ filename } ` +
570+ `by ${ transformPath } was invalid. Proceeding without source ` +
571+ 'mapping for that file.' ,
572+ ) ;
573+ }
574+ }
575+
576+ // Apply instrumentation to the code if necessary, keeping the instrumented code and new map
577+ let map = transformed . map ;
578+ if ( ! transformWillInstrument && instrument ) {
579+ /**
580+ * We can map the original source code to the instrumented code ONLY if
581+ * - the process of transforming the code produced a source map e.g. ts-jest
582+ * - we did not transform the source code
583+ *
584+ * Otherwise we cannot make any statements about how the instrumented code corresponds to the original code,
585+ * and we should NOT emit any source maps
586+ *
587+ */
588+ const shouldEmitSourceMaps =
589+ ( transform != null && map != null ) || transform == null ;
590+
591+ const instrumented = this . _instrumentFile (
592+ filename ,
593+ transformed ,
594+ supportsDynamicImport ,
595+ supportsStaticESM ,
596+ shouldEmitSourceMaps ,
597+ ) ;
598+
599+ code =
600+ typeof instrumented === 'string' ? instrumented : instrumented . code ;
601+ map = typeof instrumented === 'string' ? null : instrumented . map ;
602+ } else {
603+ code = transformed . code ;
604+ }
605+
606+ if ( map ) {
607+ const sourceMapContent =
608+ typeof map === 'string' ? map : JSON . stringify ( map ) ;
609+ writeCacheFile ( sourceMapPath , sourceMapContent ) ;
610+ } else {
611+ sourceMapPath = null ;
612+ }
613+
614+ writeCodeCacheFile ( cacheFilePath , code ) ;
615+
616+ return {
617+ code,
618+ originalCode : content ,
619+ sourceMapPath,
620+ } ;
621+ }
622+
393623 private _transformAndBuildScript (
394624 filename : Config . Path ,
395625 options : Options ,
0 commit comments