Simplify Module.js

Reviewed By: rafeca

Differential Revision: D7628732

fbshipit-source-id: 50f5612d94727190c372148eba1a01f245f21d73
This commit is contained in:
Miguel Jimenez Esun 2018-04-19 09:06:49 -07:00 committed by Facebook Github Bot
parent 16e843ef98
commit 01827a0fab
6 changed files with 244 additions and 826 deletions

View File

@ -291,12 +291,6 @@ class Bundler {
} }
} }
if (!cache && code == null) {
throw new Error(
'When not using experimental caches, code should always be provided',
);
}
// Second, if there was no result, compute it ourselves. // Second, if there was no result, compute it ourselves.
if (!data) { if (!data) {
data = await this._transformer.transform( data = await this._transformer.transform(

View File

@ -7,7 +7,7 @@ Array [
"a", "a",
"b", "b",
], ],
"id": "index", "id": "index.js",
"isAsset": false, "isAsset": false,
"isPolyfill": false, "isPolyfill": false,
"path": "/root/index.js", "path": "/root/index.js",
@ -15,7 +15,7 @@ Array [
}, },
Object { Object {
"dependencies": Array [], "dependencies": Array [],
"id": "a", "id": "a.js",
"isAsset": false, "isAsset": false,
"isPolyfill": false, "isPolyfill": false,
"path": "/root/a.js", "path": "/root/a.js",
@ -23,7 +23,7 @@ Array [
}, },
Object { Object {
"dependencies": Array [], "dependencies": Array [],
"id": "b", "id": "b.js",
"isAsset": false, "isAsset": false,
"isPolyfill": false, "isPolyfill": false,
"path": "/root/b.js", "path": "/root/b.js",

View File

@ -12,9 +12,6 @@
const Module = require('./Module'); const Module = require('./Module');
import type {TransformedCode} from '../JSTransformer/worker';
import type {ReadResult} from './Module';
class AssetModule extends Module { class AssetModule extends Module {
getPackage() { getPackage() {
return null; return null;
@ -28,12 +25,12 @@ class AssetModule extends Module {
return true; return true;
} }
_finalizeReadResult(source: string, result: TransformedCode): ReadResult { _readSourceCode() {
// We do not want to return the "source code" of assets, since it's going to // We do not want to return the "source code" of assets, since it's going to
// be binary data and can potentially be very large. This source property // be binary data and can potentially be very large. This source property
// is only used to generate the sourcemaps (since we include all the // is only used to generate the sourcemaps (since we include all the
// modules original sources in the sourcemaps). // modules original sources in the sourcemaps).
return {...result, source: ''}; return '';
} }
} }

View File

@ -10,26 +10,13 @@
'use strict'; 'use strict';
const crypto = require('crypto');
const docblock = require('jest-docblock');
const fs = require('fs'); const fs = require('fs');
const invariant = require('fbjs/lib/invariant');
const isAbsolutePath = require('absolute-path'); const isAbsolutePath = require('absolute-path');
const jsonStableStringify = require('json-stable-stringify');
const path = require('path');
import type { import type {
TransformedCode, TransformedCode,
Options as WorkerOptions, Options as WorkerOptions,
} from '../JSTransformer/worker'; } from '../JSTransformer/worker';
import type {GlobalTransformCache} from '../lib/GlobalTransformCache';
import type {
TransformCache,
GetTransformCacheKey,
ReadTransformProps,
} from '../lib/TransformCaching';
import type {Reporter} from '../lib/reporting';
import type DependencyGraphHelpers from './DependencyGraph/DependencyGraphHelpers';
import type ModuleCache from './ModuleCache'; import type ModuleCache from './ModuleCache';
import type {LocalPath} from './lib/toLocalPath'; import type {LocalPath} from './lib/toLocalPath';
import type {MetroSourceMapSegmentTuple} from 'metro-source-map'; import type {MetroSourceMapSegmentTuple} from 'metro-source-map';
@ -49,67 +36,23 @@ export type TransformCode = (
transformOptions: WorkerOptions, transformOptions: WorkerOptions,
) => Promise<TransformedCode>; ) => Promise<TransformedCode>;
export type HasteImpl = {
getHasteName(filePath: string): string | void,
// This exists temporarily to enforce consistency while we deprecate
// @providesModule.
enforceHasteNameMatches?: (
filePath: string,
expectedName: string | void,
) => void,
};
export type Options = {
globalTransformCache: ?GlobalTransformCache,
hasteImplModulePath?: string,
reporter: Reporter,
resetCache: boolean,
transformCache: TransformCache,
};
export type ConstructorArgs = { export type ConstructorArgs = {
depGraphHelpers: DependencyGraphHelpers,
experimentalCaches: boolean,
file: string, file: string,
getTransformCacheKey: GetTransformCacheKey,
localPath: LocalPath, localPath: LocalPath,
moduleCache: ModuleCache, moduleCache: ModuleCache,
options: Options,
transformCode: TransformCode, transformCode: TransformCode,
}; };
type DocBlock = {+[key: string]: string};
class Module { class Module {
localPath: LocalPath; localPath: LocalPath;
path: string; path: string;
type: string; type: string;
_experimentalCaches: boolean;
_moduleCache: ModuleCache; _moduleCache: ModuleCache;
_transformCode: TransformCode; _transformCode: TransformCode;
_getTransformCacheKey: GetTransformCacheKey;
_depGraphHelpers: DependencyGraphHelpers;
_options: Options;
_docBlock: ?DocBlock;
_hasteNameCache: ?{+hasteName: ?string};
_sourceCode: ?string; _sourceCode: ?string;
_readPromises: Map<string, Promise<ReadResult>>;
_readResultsByOptionsKey: Map<string, CachedReadResult>; constructor({file, localPath, moduleCache, transformCode}: ConstructorArgs) {
constructor({
depGraphHelpers,
experimentalCaches,
file,
getTransformCacheKey,
localPath,
moduleCache,
options,
transformCode,
}: ConstructorArgs) {
if (!isAbsolutePath(file)) { if (!isAbsolutePath(file)) {
throw new Error('Expected file to be absolute path but got ' + file); throw new Error('Expected file to be absolute path but got ' + file);
} }
@ -118,295 +61,59 @@ class Module {
this.path = file; this.path = file;
this.type = 'Module'; this.type = 'Module';
this._experimentalCaches = experimentalCaches;
this._moduleCache = moduleCache; this._moduleCache = moduleCache;
this._transformCode = transformCode; this._transformCode = transformCode;
this._getTransformCacheKey = getTransformCacheKey;
this._depGraphHelpers = depGraphHelpers;
this._options = options || {};
this._readPromises = new Map();
this._readResultsByOptionsKey = new Map();
} }
isHaste(): boolean { isHaste(): boolean {
return this._getHasteName() != null; return false;
} }
getName(): string { getName(): string {
// TODO: T26134860 Used for debugging purposes only; disabled with the new return this.localPath;
// caches.
if (this._experimentalCaches) {
return path.basename(this.path);
}
if (this.isHaste()) {
const name = this._getHasteName();
if (name != null) {
return name;
}
}
const p = this.getPackage();
if (!p) {
// Name is local path
return this.localPath;
}
const packageName = p.getName();
if (!packageName) {
return this.path;
}
return path
.join(packageName, path.relative(p.root, this.path))
.replace(/\\/g, '/');
} }
getPackage() { getPackage() {
return this._moduleCache.getPackageForModule(this); return this._moduleCache.getPackageForModule(this);
} }
/**
* We don't need to invalidate the TranformCache itself because it guarantees
* itself that if a source code changed we won't return the cached transformed
* code.
*/
invalidate() { invalidate() {
this._sourceCode = null; this._sourceCode = null;
// TODO: T26134860 Caches present in Module are not used with experimental
// caches, except for the one related to source code.
if (this._experimentalCaches) {
return;
}
this._readPromises.clear();
this._readResultsByOptionsKey.clear();
this._docBlock = null;
this._hasteNameCache = null;
} }
_readSourceCode(): string { _readSourceCode(): string {
if (this._sourceCode == null) { if (this._sourceCode == null) {
this._sourceCode = fs.readFileSync(this.path, 'utf8'); this._sourceCode = fs.readFileSync(this.path, 'utf8');
} }
return this._sourceCode; return this._sourceCode;
} }
_readDocBlock(): DocBlock { async read(transformOptions: WorkerOptions): Promise<ReadResult> {
if (this._docBlock == null) { const result: TransformedCode = await this._transformCode(
this._docBlock = docblock.parse(docblock.extract(this._readSourceCode())); this,
} null, // Source code is read on the worker
return this._docBlock; transformOptions,
);
const module = this;
return {
code: result.code,
dependencies: result.dependencies,
map: result.map,
get source() {
return module._readSourceCode();
},
};
} }
_getHasteName(): ?string { readCached(transformOptions: WorkerOptions): null {
if (this._hasteNameCache == null) {
this._hasteNameCache = {hasteName: this._readHasteName()};
}
return this._hasteNameCache.hasteName;
}
/**
* If a custom Haste implementation is provided, then we use it to determine
* the actual Haste name instead of "@providesModule".
* `enforceHasteNameMatches` has been added to that it is easier to
* transition from a system using "@providesModule" to a system using another
* custom system, by throwing if inconsistencies are detected. For example,
* we could verify that the file's basename (ex. "bar/foo.js") is the same as
* the "@providesModule" name (ex. "foo").
*/
_readHasteName(): ?string {
const hasteImplModulePath = this._options.hasteImplModulePath;
if (hasteImplModulePath == null) {
return this._readHasteNameFromDocBlock();
}
// eslint-disable-next-line no-useless-call
const HasteImpl = (require.call(null, hasteImplModulePath): HasteImpl);
if (HasteImpl.enforceHasteNameMatches != null) {
const name = this._readHasteNameFromDocBlock();
HasteImpl.enforceHasteNameMatches(this.path, name || undefined);
}
return HasteImpl.getHasteName(this.path);
}
/**
* We extract the Haste name from the `@providesModule` docbloc field. This is
* not allowed for modules living in `node_modules`, except if they are
* whitelisted.
*/
_readHasteNameFromDocBlock(): ?string {
const moduleDocBlock = this._readDocBlock();
const {providesModule} = moduleDocBlock;
if (providesModule && !this._depGraphHelpers.isNodeModulesDir(this.path)) {
return /^\S+/.exec(providesModule)[0];
}
return null; return null;
} }
/**
* To what we read from the cache or worker, we need to add id and source.
*/
_finalizeReadResult(source: string, result: TransformedCode): ReadResult {
return {...result, source};
}
async _transformCodeFor(
cacheProps: ReadTransformProps,
): Promise<TransformedCode> {
const {_transformCode} = this;
invariant(_transformCode != null, 'missing code transform funtion');
const {sourceCode, transformOptions} = cacheProps;
return await _transformCode(this, sourceCode, transformOptions);
}
async _transformAndStoreCodeGlobally(
cacheProps: ReadTransformProps,
globalCache: GlobalTransformCache,
): Promise<TransformedCode> {
const result = await this._transformCodeFor(cacheProps);
globalCache.store(globalCache.keyOf(cacheProps), result);
return result;
}
async _getTransformedCode(
cacheProps: ReadTransformProps,
): Promise<TransformedCode> {
const globalCache = this._options.globalTransformCache;
if (globalCache == null || !globalCache.shouldFetch(cacheProps)) {
return await this._transformCodeFor(cacheProps);
}
const globalCachedResult = await globalCache.fetch(
globalCache.keyOf(cacheProps),
);
if (globalCachedResult != null) {
return globalCachedResult;
}
return await this._transformAndStoreCodeGlobally(cacheProps, globalCache);
}
async _getAndCacheTransformedCode(
cacheProps: ReadTransformProps,
): Promise<TransformedCode> {
const result = await this._getTransformedCode(cacheProps);
this._options.transformCache.writeSync({...cacheProps, result});
return result;
}
/**
* Shorthand for reading both from cache or from fresh for all call sites that
* are asynchronous by default.
*/
async read(transformOptions: WorkerOptions): Promise<ReadResult> {
// TODO: T26134860 Cache layer lives inside the transformer now; just call
// the transform method.
if (this._experimentalCaches) {
const result: TransformedCode = await this._transformCode(
this,
null, // Source code is read on the worker
transformOptions,
);
const module = this;
return {
code: result.code,
dependencies: result.dependencies,
map: result.map,
get source() {
return module._readSourceCode();
},
};
}
const cached = this.readCached(transformOptions);
if (cached != null) {
return cached;
}
return this.readFresh(transformOptions);
}
/**
* Same as `readFresh`, but reads from the cache instead of transforming
* the file from source. This has the benefit of being synchronous. As a
* result it is possible to read many cached Module in a row, synchronously.
*/
readCached(transformOptions: WorkerOptions): CachedReadResult {
const key = stableObjectHash(transformOptions || {});
let result = this._readResultsByOptionsKey.get(key);
if (result != null) {
return result;
}
result = this._readFromTransformCache(transformOptions, key);
this._readResultsByOptionsKey.set(key, result);
return result;
}
/**
* Read again from the TransformCache, on disk. `readCached` should be favored
* so it's faster in case the results are already in memory.
*/
_readFromTransformCache(
transformOptions: WorkerOptions,
transformOptionsKey: string,
): CachedReadResult {
const cacheProps = this._getCacheProps(
transformOptions,
transformOptionsKey,
);
const cachedResult = this._options.transformCache.readSync(cacheProps);
if (cachedResult == null) {
return null;
}
return this._finalizeReadResult(cacheProps.sourceCode, cachedResult);
}
/**
* Gathers relevant data about a module: source code, transformed code,
* dependencies, etc. This function reads and transforms the source from
* scratch. We don't repeat the same work as `readCached` because we assume
* call sites have called it already.
*/
readFresh(transformOptions: WorkerOptions): Promise<ReadResult> { readFresh(transformOptions: WorkerOptions): Promise<ReadResult> {
const key = stableObjectHash(transformOptions || {}); return this.read(transformOptions);
const promise = this._readPromises.get(key);
if (promise != null) {
return promise;
}
const freshPromise = (async () => {
const cacheProps = this._getCacheProps(transformOptions, key);
const freshResult = await this._getAndCacheTransformedCode(cacheProps);
const finalResult = this._finalizeReadResult(
cacheProps.sourceCode,
freshResult,
);
this._readResultsByOptionsKey.set(key, finalResult);
return finalResult;
})();
this._readPromises.set(key, freshPromise);
return freshPromise;
}
_getCacheProps(transformOptions: WorkerOptions, transformOptionsKey: string) {
const sourceCode = this._readSourceCode();
const getTransformCacheKey = this._getTransformCacheKey;
return {
filePath: this.path,
localPath: this.localPath,
sourceCode,
getTransformCacheKey,
transformOptions,
transformOptionsKey,
cacheOptions: {
resetCache: this._options.resetCache,
reporter: this._options.reporter,
},
};
} }
hash() { hash() {
@ -422,19 +129,4 @@ class Module {
} }
} }
// use weak map to speed up hash creation of known objects
const knownHashes = new WeakMap();
function stableObjectHash(object) {
let digest = knownHashes.get(object);
if (!digest) {
digest = crypto
.createHash('md5')
.update(jsonStableStringify(object))
.digest('base64');
knownHashes.set(object, digest);
}
return digest;
}
module.exports = Module; module.exports = Module;

View File

@ -10,363 +10,94 @@
'use strict'; 'use strict';
jest jest.mock('fs').mock('../ModuleCache');
.mock('fs', () => new (require('metro-memory-fs'))())
.mock('graceful-fs')
.mock('../ModuleCache')
.mock('../DependencyGraph/DependencyGraphHelpers')
.mock('../../lib/TransformCaching');
const Module = require('../Module');
const ModuleCache = require('../ModuleCache');
const DependencyGraphHelpers = require('../DependencyGraph/DependencyGraphHelpers');
const TransformCaching = require('../../lib/TransformCaching');
const fs = require('fs'); const fs = require('fs');
const ModuleCache = require('../ModuleCache');
const packageJson = JSON.stringify({ const Module = require('../Module');
name: 'arbitrary',
version: '1.0.0',
description: "A require('foo') story",
});
function mockPackageFile() {
fs.reset();
fs.mkdirSync('/root');
fs.writeFileSync('/root/package.json', packageJson);
}
function mockIndexFile(indexJs) {
fs.reset();
fs.mkdirSync('/root');
fs.writeFileSync('/root/index.js', indexJs);
}
describe('Module', () => { describe('Module', () => {
const fileName = '/root/index.js'; let transformCode;
let moduleCache;
let module;
let cache; beforeEach(() => {
const transformCache = TransformCaching.mocked(); transformCode = jest.fn().mockReturnValue({
code: 'int main(void) { return -1; }',
const createCache = () => ({ dependencies: ['stdlib.h', 'conio.h'],
get: jest map: [],
.genMockFn()
.mockImplementation((filepath, field, cb) => cb(filepath)),
invalidate: jest.genMockFn(),
end: jest.genMockFn(),
});
let transformCacheKey;
const createModule = options =>
new Module({
options: {transformCache},
transformCode: (module, sourceCode, transformOptions) => {
return Promise.resolve({code: sourceCode});
},
...options,
cache,
file: (options && options.file) || fileName,
depGraphHelpers: new DependencyGraphHelpers(),
localPath: (options && options.localPath) || fileName,
moduleCache: new ModuleCache({cache}),
getTransformCacheKey: () => transformCacheKey,
}); });
const createJSONModule = options => moduleCache = new ModuleCache();
createModule({...options, file: '/root/package.json'});
beforeEach(function() { module = new Module({
Object.defineProperty(process, 'platform', { file: '/root/to/file.js',
configurable: true, localPath: 'file.js',
enumerable: true, moduleCache,
value: 'linux', transformCode,
});
cache = createCache();
transformCacheKey = 'abcdef';
transformCache.mock.reset();
});
describe('Experimental caches', () => {
it('Calls into the transformer directly when having experimental caches on', async () => {
const transformCode = jest.fn().mockReturnValue({
code: 'code',
dependencies: ['dep1', 'dep2'],
map: [],
});
const module = new Module({
cache,
experimentalCaches: true,
depGraphHelpers: new DependencyGraphHelpers(),
file: fileName,
getTransformCacheKey: () => transformCacheKey,
localPath: fileName,
moduleCache: new ModuleCache({cache}),
options: {transformCache},
transformCode,
});
mockIndexFile('originalCode');
jest.spyOn(fs, 'readFileSync');
// Read the first time, transform code is called.
const res1 = await module.read({foo: 3});
expect(res1.code).toBe('code');
expect(res1.dependencies).toEqual(['dep1', 'dep2']);
expect(transformCode).toHaveBeenCalledTimes(1);
// Read a second time, transformCode is called again!
const res2 = await module.read({foo: 3});
expect(res2.code).toBe('code');
expect(res2.dependencies).toEqual(['dep1', 'dep2']);
expect(transformCode).toHaveBeenCalledTimes(2);
// Code was never read, though, because experimental caches read on the
// worker, to speed up local cache!
expect(fs.readFileSync).not.toHaveBeenCalled();
}); });
}); });
describe('Module ID', () => { afterEach(() => {
const moduleId = 'arbitraryModule'; fs.readFileSync.mockReset();
const source = `/** });
* @providesModule ${moduleId}
*/
`;
let module; it('Returns the correct values for many properties and methods', () => {
beforeEach(() => { expect(module.localPath).toBe('file.js');
module = createModule(); expect(module.path).toBe('/root/to/file.js');
}); expect(module.type).toBe('Module');
describe('@providesModule annotations', () => { expect(module.hash()).toBeDefined();
beforeEach(() => { expect(module.getName()).toBe('file.js');
mockIndexFile(source); expect(module.isHaste()).toBe(false);
}); expect(module.isAsset()).toBe(false);
expect(module.isPolyfill()).toBe(false);
});
it('extracts the module name from the header', () => { it('reads the modules correctly', () => {
expect(module.getName()).toEqual(moduleId); const opts = {};
});
it('identifies the module as haste module', () => { // Caches are not in Module.js anymore.
expect(module.isHaste()).toBe(true); expect(module.readCached()).toBe(null);
});
it('does not transform the file in order to access the name', () => { // When reading fresh, we call directly into read.
const transformCode = jest module.readFresh(opts);
.genMockFn() expect(transformCode.mock.calls[0][0]).toBe(module);
.mockReturnValue(Promise.resolve()); expect(transformCode.mock.calls[0][1]).toBe(null);
expect(transformCode.mock.calls[0][2]).toBe(opts);
});
createModule({transformCode}).getName(); it('returns the result from the transform code straight away', async () => {
expect(transformCode).not.toBeCalled(); fs.readFileSync.mockReturnValue('original code');
});
it('does not transform the file in order to access the haste status', () => { expect(await module.read({})).toEqual({
const transformCode = jest code: 'int main(void) { return -1; }',
.genMockFn() dependencies: ['stdlib.h', 'conio.h'],
.mockReturnValue(Promise.resolve()); map: [],
createModule({transformCode}).isHaste(); source: 'original code',
expect(transformCode).not.toBeCalled();
});
});
describe('no annotation', () => {
beforeEach(() => {
mockIndexFile('arbitrary(code);');
});
it('uses the file name as module name', () => {
expect(module.getName()).toEqual(fileName);
});
it('does not identify the module as haste module', () =>
expect(module.isHaste()).toBe(false));
it('does not transform the file in order to access the name', () => {
const transformCode = jest
.genMockFn()
.mockReturnValue(Promise.resolve());
createModule({transformCode}).getName();
expect(transformCode).not.toBeCalled();
});
it('does not transform the file in order to access the haste status', () => {
const transformCode = jest
.genMockFn()
.mockReturnValue(Promise.resolve());
createModule({transformCode}).isHaste();
expect(transformCode).not.toBeCalled();
});
}); });
}); });
describe('Code', () => { it('checks that code is only read once until invalidated', async () => {
const fileContents = 'arbitrary(code)'; fs.readFileSync.mockReturnValue('original code');
beforeEach(function() {
mockIndexFile(fileContents);
});
it('exposes file contents as `code` property on the data exposed by `read()`', () => // Read once. No access to "source", so no reads.
createModule() await module.read({});
.read() expect(fs.readFileSync).toHaveBeenCalledTimes(0);
.then(({code}) => expect(code).toBe(fileContents)));
});
describe('Custom Code Transform', () => { // Read again, accessing "source".
let transformCode; expect((await module.read({})).source).toEqual('original code');
let transformResult; expect(fs.readFileSync).toHaveBeenCalledTimes(1);
const fileContents = 'arbitrary(code);';
const exampleCode = `
${'require'}('a');
${'System.import'}('b');
${'require'}('c');`;
beforeEach(function() { // Read again, accessing "source" again. Still 1 because code was cached.
transformResult = {code: ''}; expect((await module.read({})).source).toEqual('original code');
transformCode = jest expect(fs.readFileSync).toHaveBeenCalledTimes(1);
.genMockFn()
.mockImplementation((module, sourceCode, options) => {
transformCache.writeSync({
filePath: module.path,
sourceCode,
transformOptions: options,
getTransformCacheKey: () => transformCacheKey,
result: transformResult,
});
return Promise.resolve(transformResult);
});
mockIndexFile(fileContents);
});
it('passes the module and file contents to the transform function when reading', () => { // Invalidate.
const module = createModule({transformCode}); module.invalidate();
return module.read().then(() => {
expect(transformCode).toBeCalledWith(module, fileContents, undefined);
});
});
it('passes any additional options to the transform function when reading', () => { // Read again, this time it will read it.
const module = createModule({transformCode}); expect((await module.read({})).source).toEqual('original code');
const transformOptions = {arbitrary: Object()}; expect(fs.readFileSync).toHaveBeenCalledTimes(2);
return module
.read(transformOptions)
.then(() =>
expect(transformCode.mock.calls[0][2]).toBe(transformOptions),
);
});
it('passes the module and file contents to the transform for JSON files', () => {
mockPackageFile();
const module = createJSONModule({transformCode});
return module.read().then(() => {
expect(transformCode).toBeCalledWith(module, packageJson, undefined);
});
});
it('does not extend the passed options object for JSON files', () => {
mockPackageFile();
const module = createJSONModule({transformCode});
const options = {arbitrary: 'foo'};
return module.read(options).then(() => {
expect(transformCode).toBeCalledWith(module, packageJson, options);
});
});
it('uses dependencies that `transformCode` resolves to, instead of extracting them', async () => {
const mockedDependencies = ['foo', 'bar'];
transformResult = {
code: exampleCode,
dependencies: mockedDependencies,
};
const module = createModule({transformCode});
const data = await module.read();
expect(data.dependencies).toEqual(mockedDependencies);
});
it('forwards all additional properties of the result provided by `transformCode`', () => {
transformResult = {
code: exampleCode,
arbitrary: 'arbitrary',
dependencyOffsets: [12, 764],
map: {version: 3},
subObject: {foo: 'bar'},
};
const module = createModule({transformCode});
return module.read().then(result => {
expect(result).toEqual(jasmine.objectContaining(transformResult));
});
});
it('exposes the transformed code rather than the raw file contents', async () => {
transformResult = {code: exampleCode};
const module = createModule({transformCode});
const data = await module.read();
expect(data.code).toBe(exampleCode);
});
it('exposes the raw file contents as `source` property', () => {
const module = createModule({transformCode});
return module.read().then(data => expect(data.source).toBe(fileContents));
});
it('exposes a source map returned by the transform', async () => {
const map = {version: 3};
transformResult = {map, code: exampleCode};
const module = createModule({transformCode});
const data = await module.read();
expect(data.map).toBe(map);
});
it('caches the transform result for the same transform options', () => {
let module = createModule({transformCode});
return module.read().then(() => {
expect(transformCode).toHaveBeenCalledTimes(1);
// We want to check transform caching rather than shallow caching of
// Promises returned by read().
module = createModule({transformCode});
return module.read().then(() => {
expect(transformCode).toHaveBeenCalledTimes(1);
});
});
});
it('triggers a new transform for different transform options', () => {
const module = createModule({transformCode});
return module.read({foo: 1}).then(() => {
expect(transformCode).toHaveBeenCalledTimes(1);
return module.read({foo: 2}).then(() => {
expect(transformCode).toHaveBeenCalledTimes(2);
});
});
});
it('triggers a new transform for different source code', () => {
let module = createModule({transformCode});
return module.read().then(() => {
expect(transformCode).toHaveBeenCalledTimes(1);
cache = createCache();
mockIndexFile('test');
module = createModule({transformCode});
return module.read().then(() => {
expect(transformCode).toHaveBeenCalledTimes(2);
});
});
});
it('triggers a new transform for different transform cache key', () => {
let module = createModule({transformCode});
return module.read().then(() => {
expect(transformCode).toHaveBeenCalledTimes(1);
transformCacheKey = 'other';
module = createModule({transformCode});
return module.read().then(() => {
expect(transformCode).toHaveBeenCalledTimes(2);
});
});
});
}); });
}); });