Skip to content

Commit 49dbc32

Browse files
committed
Initial step to transformAsync
1 parent c5f2fd7 commit 49dbc32

2 files changed

Lines changed: 282 additions & 3 deletions

File tree

packages/jest-transform/src/ScriptTransformer.ts

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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,

packages/jest-transform/src/types.ts

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,21 +55,70 @@ export interface CacheKeyOptions extends TransformOptions {
5555
rootDir: string;
5656
}
5757

58-
export interface Transformer {
58+
interface SyncTransFormer {
5959
canInstrument?: boolean;
60-
createTransformer?: (options?: any) => Transformer;
60+
createTransformer?: (options?: any) => SyncTransFormer;
6161

6262
getCacheKey?: (
63-
fileData: string,
63+
fileDate: string,
6464
filePath: Config.Path,
6565
configStr: string,
6666
options: CacheKeyOptions,
6767
) => string;
6868

69+
getCacheKeyAsync?: (
70+
fileDate: string,
71+
filePath: Config.Path,
72+
configStr: string,
73+
options: CacheKeyOptions,
74+
) => Promise<string>;
75+
6976
process: (
7077
sourceText: string,
7178
sourcePath: Config.Path,
7279
config: Config.ProjectConfig,
7380
options?: TransformOptions,
7481
) => TransformedSource;
82+
83+
processAsync?: (
84+
sourceText: string,
85+
sourcePath: Config.Path,
86+
config: Config.ProjectConfig,
87+
options?: TransformOptions,
88+
) => Promise<TransformedSource>;
7589
}
90+
91+
interface AsyncTransformer {
92+
canInstrument?: boolean;
93+
createTransformer?: (options?: any) => AsyncTransformer;
94+
95+
getCacheKey?: (
96+
fileDate: string,
97+
filePath: Config.Path,
98+
configStr: string,
99+
options: CacheKeyOptions,
100+
) => string;
101+
102+
getCacheKeyAsync?: (
103+
fileDate: string,
104+
filePath: Config.Path,
105+
configStr: string,
106+
options: CacheKeyOptions,
107+
) => Promise<string>;
108+
109+
process: (
110+
sourceText: string,
111+
sourcePath: Config.Path,
112+
config: Config.ProjectConfig,
113+
options?: TransformOptions,
114+
) => TransformedSource;
115+
116+
processAsync?: (
117+
sourceText: string,
118+
sourcePath: Config.Path,
119+
config: Config.ProjectConfig,
120+
options?: TransformOptions,
121+
) => Promise<TransformedSource>;
122+
}
123+
124+
export type Transformer = SyncTransFormer | AsyncTransformer;

0 commit comments

Comments
 (0)