Skip to content

Commit

Permalink
Initial step to transformAsync
Browse files Browse the repository at this point in the history
  • Loading branch information
ychi committed Apr 29, 2020
1 parent c5f2fd7 commit 49dbc32
Show file tree
Hide file tree
Showing 2 changed files with 282 additions and 3 deletions.
230 changes: 230 additions & 0 deletions packages/jest-transform/src/ScriptTransformer.ts
Expand Up @@ -120,6 +120,40 @@ export default class ScriptTransformer {
}
}

private async _getCacheKeyAsync(
fileData: string,
filename: Config.Path,
instrument: boolean,
supportsDynamicImport: boolean,
supportsStaticESM: boolean,
): Promise<string> {
const configString = this._cache.configString;
const transformer = await this._getTransformerAsync(filename);

if (transformer && typeof transformer.getCacheKeyAsync === 'function') {
return createHash('md5')
.update(
await transformer.getCacheKeyAsync(fileData, filename, configString, {
config: this._config,
instrument,
rootDir: this._config.rootDir,
supportsDynamicImport,
supportsStaticESM,
}),
)
.update(CACHE_VERSION)
.digest('hex');
} else {
return createHash('md5')
.update(fileData)
.update(configString)
.update(instrument ? 'instrument' : '')
.update(filename)
.update(CACHE_VERSION)
.digest('hex');
}
}

private _getFileCachePath(
filename: Config.Path,
content: string,
Expand Down Expand Up @@ -153,6 +187,39 @@ export default class ScriptTransformer {
return cachePath;
}

private async _getFileCachePathAsync(
filename: Config.Path,
content: string,
instrument: boolean,
supportsDynamicImport: boolean,
supportsStaticESM: boolean,
): Promise<Config.Path> {
const baseCacheDir = HasteMap.getCacheFilePath(
this._config.cacheDirectory,
'jest-transform-cache-' + this._config.name,
VERSION,
);
const cacheKey = await this._getCacheKeyAsync(
content,
filename,
instrument,
supportsDynamicImport,
supportsStaticESM,
);
// Create sub folders based on the cacheKey to avoid creating one
// directory with many files.
const cacheDir = path.join(baseCacheDir, cacheKey[0] + cacheKey[1]);
const cacheFilenamePrefix = path
.basename(filename, path.extname(filename))
.replace(/\W/g, '');
const cachePath = slash(
path.join(cacheDir, cacheFilenamePrefix + '_' + cacheKey),
);
createDirectory(cacheDir);

return cachePath;
}

private _getTransformPath(filename: Config.Path) {
const transformRegExp = this._cache.transformRegExp;
if (!transformRegExp) {
Expand All @@ -171,6 +238,40 @@ export default class ScriptTransformer {
return undefined;
}

private async _getTransformerAsync(filename: Config.Path) {
let transform: Transformer | null = null;
if (!this._config.transform || !this._config.transform.length) {
return null;
}

const transformPath = this._getTransformPath(filename);
if (transformPath) {
const transformer = this._transformCache.get(transformPath);
if (transformer != null) {
return transformer;
}

transform = await import(transformPath);

if (!transform) {
throw new TypeError('Jest: a transform must export something.');
}
const transformerConfig = this._transformConfigCache.get(transformPath);
if (typeof transform.createTransformer === 'function') {
transform = transform.createTransformer(transformerConfig);
}
if (
typeof transform.process !== 'function' &&
typeof transform.processAsync !== 'function') {
throw new TypeError(
'Jest: a transform must export a `process` or `processAsync` function.',
);
}
this._transformCache.set(transformPath, transform);
}
return transform;
}

private _getTransformer(filename: Config.Path) {
let transform: Transformer | null = null;
if (!this._config.transform || !this._config.transform.length) {
Expand Down Expand Up @@ -390,6 +491,135 @@ export default class ScriptTransformer {
};
}

// TODO: replace third argument with TransformOptions in Jest 26
async transformSourceAsync(
filepath: Config.Path,
content: string,
instrument: boolean,
supportsDynamicImport = false,
supportsStaticESM = false,
): Promise<TransformResult> {
const filename = this._getRealPath(filepath);
const transform = await this._getTransformerAsync(filename);
const cacheFilePath = await this._getFileCachePathAsync(
filename,
content,
instrument,
supportsDynamicImport,
supportsStaticESM,
);
let sourceMapPath: Config.Path | null = cacheFilePath + '.map';
// Ignore cache if `config.cache` is set (--no-cache)
let code = this._config.cache ? readCodeCacheFile(cacheFilePath) : null;

const shouldCallTransform = transform && this.shouldTransform(filename);

// That means that the transform has a custom instrumentation
// logic and will handle it based on `config.collectCoverage` option
const transformWillInstrument =
shouldCallTransform && transform && transform.canInstrument;

if (code) {
// This is broken: we return the code, and a path for the source map
// directly from the cache. But, nothing ensures the source map actually
// matches that source code. They could have gotten out-of-sync in case
// two separate processes write concurrently to the same cache files.
return {
code,
originalCode: content,
sourceMapPath,
};
}

let transformed: TransformedSource = {
code: content,
map: null,
};

if (transform && shouldCallTransform) {
const processed = transform.process(content, filename, this._config, {
instrument,
supportsDynamicImport,
supportsStaticESM,
});

if (typeof processed === 'string') {
transformed.code = processed;
} else if (processed != null && typeof processed.code === 'string') {
transformed = processed;
} else {
throw new TypeError(
"Jest: a transform's `process` function must return a string, " +
'or an object with `code` key containing this string.',
);
}
}

if (!transformed.map) {
try {
//Could be a potential freeze here.
//See: https://github.com/facebook/jest/pull/5177#discussion_r158883570
const inlineSourceMap = sourcemapFromSource(transformed.code);
if (inlineSourceMap) {
transformed.map = inlineSourceMap.toObject();
}
} catch (e) {
const transformPath = this._getTransformPath(filename);
console.warn(
`jest-transform: The source map produced for the file ${filename} ` +
`by ${transformPath} was invalid. Proceeding without source ` +
'mapping for that file.',
);
}
}

// Apply instrumentation to the code if necessary, keeping the instrumented code and new map
let map = transformed.map;
if (!transformWillInstrument && instrument) {
/**
* We can map the original source code to the instrumented code ONLY if
* - the process of transforming the code produced a source map e.g. ts-jest
* - we did not transform the source code
*
* Otherwise we cannot make any statements about how the instrumented code corresponds to the original code,
* and we should NOT emit any source maps
*
*/
const shouldEmitSourceMaps =
(transform != null && map != null) || transform == null;

const instrumented = this._instrumentFile(
filename,
transformed,
supportsDynamicImport,
supportsStaticESM,
shouldEmitSourceMaps,
);

code =
typeof instrumented === 'string' ? instrumented : instrumented.code;
map = typeof instrumented === 'string' ? null : instrumented.map;
} else {
code = transformed.code;
}

if (map) {
const sourceMapContent =
typeof map === 'string' ? map : JSON.stringify(map);
writeCacheFile(sourceMapPath, sourceMapContent);
} else {
sourceMapPath = null;
}

writeCodeCacheFile(cacheFilePath, code);

return {
code,
originalCode: content,
sourceMapPath,
};
}

private _transformAndBuildScript(
filename: Config.Path,
options: Options,
Expand Down
55 changes: 52 additions & 3 deletions packages/jest-transform/src/types.ts
Expand Up @@ -55,21 +55,70 @@ export interface CacheKeyOptions extends TransformOptions {
rootDir: string;
}

export interface Transformer {
interface SyncTransFormer {
canInstrument?: boolean;
createTransformer?: (options?: any) => Transformer;
createTransformer?: (options?: any) => SyncTransFormer;

getCacheKey?: (
fileData: string,
fileDate: string,
filePath: Config.Path,
configStr: string,
options: CacheKeyOptions,
) => string;

getCacheKeyAsync?: (
fileDate: string,
filePath: Config.Path,
configStr: string,
options: CacheKeyOptions,
) => Promise<string>;

process: (
sourceText: string,
sourcePath: Config.Path,
config: Config.ProjectConfig,
options?: TransformOptions,
) => TransformedSource;

processAsync?: (
sourceText: string,
sourcePath: Config.Path,
config: Config.ProjectConfig,
options?: TransformOptions,
) => Promise<TransformedSource>;
}

interface AsyncTransformer {
canInstrument?: boolean;
createTransformer?: (options?: any) => AsyncTransformer;

getCacheKey?: (
fileDate: string,
filePath: Config.Path,
configStr: string,
options: CacheKeyOptions,
) => string;

getCacheKeyAsync?: (
fileDate: string,
filePath: Config.Path,
configStr: string,
options: CacheKeyOptions,
) => Promise<string>;

process: (
sourceText: string,
sourcePath: Config.Path,
config: Config.ProjectConfig,
options?: TransformOptions,
) => TransformedSource;

processAsync?: (
sourceText: string,
sourcePath: Config.Path,
config: Config.ProjectConfig,
options?: TransformOptions,
) => Promise<TransformedSource>;
}

export type Transformer = SyncTransFormer | AsyncTransformer;

0 comments on commit 49dbc32

Please sign in to comment.