mirror of https://github.com/status-im/metro.git
Simplify Module.js
Reviewed By: rafeca Differential Revision: D7628732 fbshipit-source-id: 50f5612d94727190c372148eba1a01f245f21d73
This commit is contained in:
parent
16e843ef98
commit
01827a0fab
|
@ -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(
|
||||||
|
|
|
@ -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",
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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 '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue