Files
react-native/packager/react-packager/src/node-haste/Module.js
T
Christoph Pojer 6554ad5983 Kill fastfs
Summary:
This kills fastfs in favor of Jest's hasteFS. It gets rid of a ton of code, including the mocking code in ResolutionRequest which we don't need any more. Next step after this is to rewrite HasteMap, ModuleCache, Module/Package. We are getting closer to a nicer and faster world! :)

Here is what I did:
* Use Jest's HasteFS instead of fastfs. A fresh instance is received every time something changes on the FS.
* HasteFS is not shared with everything any more. Only one reference is kept in DependencyGraph and there are a few smaller functions that are passed around (getClosestPackage and dirExists). Note: `dirExists` now does fs access instead of an offline check. This sucks but stat calls aren't slow and aren't going to be a bottleneck in ResolutionRequest, I promise! When it is time to tackle a ResolutionRequest rewrite with jest-resolve, this will go away. "It gets worse before it gets better" :) The ModuleGraph equivalent does *not* do fs access and retains the previous way of doing things because we shouldn't do online fs access there.
* Add flow annotations to ResolutionRequest. This required a few tiny hacks for now because of ModuleGraph's duck typing. I'll get rid of this soon.
* Updated ModuleGraph to work with the new code, also created a mock HasteFS instance there.
* I fixed a few tiny mock issues for `fs` to make the tests work; I had to add one tiny little internal update to `dgraph._hasteFS._files` because the file watching in the tests isn't real. It is instrumented through some function calls, therefore the hasteFS instance doesn't get automatically updated. One way to solve this is to add `JestHasteMap.emit('change', …)` for testing but I didn't want to cut a Jest release just for that. #movefast

(Note: I will likely land this in 1.5 weeks from now after my vacation and I have yet to fully test all the product flows. Please give me feedback so I can make sure this is solid!)

Reviewed By: davidaurelio

Differential Revision: D4204082

fbshipit-source-id: d6dc9fcb77ac224df4554a59f0fce241c01b0512
2016-11-30 04:28:32 -08:00

373 lines
10 KiB
JavaScript

/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';
const GlobalTransformCache = require('../lib/GlobalTransformCache');
const TransformCache = require('../lib/TransformCache');
const chalk = require('chalk');
const crypto = require('crypto');
const docblock = require('./DependencyGraph/docblock');
const fs = require('fs');
const invariant = require('invariant');
const isAbsolutePath = require('absolute-path');
const jsonStableStringify = require('json-stable-stringify');
const {join: joinPath, relative: relativePath, extname} = require('path');
import type {TransformedCode, Options as TransformOptions} from '../JSTransformer/worker/worker';
import type {ReadTransformProps} from '../lib/TransformCache';
import type Cache from './Cache';
import type DependencyGraphHelpers from './DependencyGraph/DependencyGraphHelpers';
import type ModuleCache from './ModuleCache';
type ReadResult = {
code?: string,
dependencies?: ?Array<string>,
dependencyOffsets?: ?Array<number>,
map?: ?{},
};
export type TransformCode = (
module: Module,
sourceCode: string,
transformOptions: TransformOptions,
) => Promise<TransformedCode>;
export type Options = {
resetCache?: boolean,
cacheTransformResults?: boolean,
};
export type ConstructorArgs = {
file: string,
moduleCache: ModuleCache,
cache: Cache,
transformCode: ?TransformCode,
transformCacheKey: ?string,
depGraphHelpers: DependencyGraphHelpers,
options: Options,
};
class Module {
path: string;
type: string;
_moduleCache: ModuleCache;
_cache: Cache;
_transformCode: ?TransformCode;
_transformCacheKey: ?string;
_depGraphHelpers: DependencyGraphHelpers;
_options: Options;
_docBlock: Promise<{id?: string, moduleDocBlock: {[key: string]: mixed}}>;
_readSourceCodePromise: Promise<string>;
_readPromises: Map<string, Promise<ReadResult>>;
static _useGlobalCache: boolean;
constructor({
file,
moduleCache,
cache,
transformCode,
transformCacheKey,
depGraphHelpers,
options,
}: ConstructorArgs) {
if (!isAbsolutePath(file)) {
throw new Error('Expected file to be absolute path but got ' + file);
}
this.path = file;
this.type = 'Module';
this._moduleCache = moduleCache;
this._cache = cache;
this._transformCode = transformCode;
this._transformCacheKey = transformCacheKey;
invariant(
transformCode == null || transformCacheKey != null,
'missing transform cache key',
);
this._depGraphHelpers = depGraphHelpers;
this._options = options || {};
this._readPromises = new Map();
}
isHaste(): Promise<boolean> {
return this._cache.get(
this.path,
'isHaste',
() => this._readDocBlock().then(({id}) => !!id)
);
}
getCode(transformOptions: TransformOptions) {
return this.read(transformOptions).then(({code}) => code);
}
getMap(transformOptions: TransformOptions) {
return this.read(transformOptions).then(({map}) => map);
}
getName(): Promise<string | number> {
return this._cache.get(
this.path,
'name',
() => this._readDocBlock().then(({id}) => {
if (id) {
return id;
}
const p = this.getPackage();
if (!p) {
// Name is full path
return this.path;
}
return p.getName()
.then(name => {
if (!name) {
return this.path;
}
return joinPath(name, relativePath(p.root, this.path)).replace(/\\/g, '/');
});
})
);
}
getPackage() {
return this._moduleCache.getPackageForModule(this);
}
getDependencies(transformOptions: TransformOptions) {
return this.read(transformOptions).then(({dependencies}) => dependencies);
}
/**
* 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() {
this._cache.invalidate(this.path);
this._readPromises.clear();
}
_parseDocBlock(docBlock) {
// Extract an id for the module if it's using @providesModule syntax
// and if it's NOT in node_modules (and not a whitelisted node_module).
// This handles the case where a project may have a dep that has @providesModule
// docblock comments, but doesn't want it to conflict with whitelisted @providesModule
// modules, such as react-haste, fbjs-haste, or react-native or with non-dependency,
// project-specific code that is using @providesModule.
const moduleDocBlock = docblock.parseAsObject(docBlock);
const provides = moduleDocBlock.providesModule || moduleDocBlock.provides;
const id = provides && !this._depGraphHelpers.isNodeModulesDir(this.path)
? /^\S+/.exec(provides)[0]
: undefined;
return {id, moduleDocBlock};
}
_readSourceCode() {
if (!this._readSourceCodePromise) {
this._readSourceCodePromise = new Promise(
resolve => resolve(fs.readFileSync(this.path, 'utf8'))
);
}
return this._readSourceCodePromise;
}
_readDocBlock() {
if (!this._docBlock) {
this._docBlock = this._readSourceCode()
.then(docBlock => this._parseDocBlock(docBlock));
}
return this._docBlock;
}
/**
* To what we read from the cache or worker, we need to add id and source.
*/
_finalizeReadResult(
source: string,
id?: string,
extern: boolean,
result: TransformedCode,
) {
if (this._options.cacheTransformResults === false) {
const {dependencies} = result;
return {dependencies};
}
return {...result, id, source};
}
_transformCodeForCallback(
cacheProps: ReadTransformProps,
callback: (error: ?Error, result: ?TransformedCode) => void,
) {
const {_transformCode} = this;
invariant(_transformCode != null, 'missing code transform funtion');
const {sourceCode, transformOptions} = cacheProps;
return _transformCode(this, sourceCode, transformOptions).then(
freshResult => process.nextTick(callback, undefined, freshResult),
error => process.nextTick(callback, error),
);
}
_getTransformedCode(
cacheProps: ReadTransformProps,
callback: (error: ?Error, result: ?TransformedCode) => void,
) {
const globalCache = GlobalTransformCache.get();
if (!Module._useGlobalCache || globalCache == null) {
this._transformCodeForCallback(cacheProps, callback);
return;
}
globalCache.fetch(cacheProps, (globalCacheError, globalCachedResult) => {
if (globalCacheError != null && Module._useGlobalCache) {
console.log(chalk.red(
'\nWarning: the global cache failed with error:',
));
console.log(chalk.red(globalCacheError.stack));
console.log(chalk.red(
'The global cache will be DISABLED for the ' +
'remainder of the transformation.',
));
Module._useGlobalCache = false;
}
if (globalCacheError != null || globalCachedResult == null) {
this._transformCodeForCallback(cacheProps, callback);
return;
}
callback(undefined, globalCachedResult);
});
}
_getAndCacheTransformedCode(
cacheProps: ReadTransformProps,
callback: (error: ?Error, result: ?TransformedCode) => void,
) {
this._getTransformedCode(cacheProps, (error, result) => {
if (error) {
callback(error);
return;
}
invariant(result != null, 'missing result');
TransformCache.writeSync({...cacheProps, result});
callback(undefined, result);
});
}
/**
* Read everything about a module: source code, transformed code,
* dependencies, etc. The overall process is to read the cache first, and if
* it's a miss, we let the worker write to the cache and read it again.
*/
read(transformOptions: TransformOptions): Promise<ReadResult> {
const key = stableObjectHash(transformOptions || {});
const promise = this._readPromises.get(key);
if (promise != null) {
return promise;
}
const freshPromise = Promise.all([
this._readSourceCode(),
this._readDocBlock(),
]).then(([sourceCode, {id, moduleDocBlock}]) => {
// Ignore requires in JSON files or generated code. An example of this
// is prebuilt files like the SourceMap library.
const extern = this.isJSON() || 'extern' in moduleDocBlock;
if (extern) {
transformOptions = {...transformOptions, extern};
}
const transformCacheKey = this._transformCacheKey;
invariant(transformCacheKey != null, 'missing transform cache key');
const cacheProps = {
filePath: this.path,
sourceCode,
transformCacheKey,
transformOptions,
cacheOptions: this._options,
};
const cachedResult = TransformCache.readSync(cacheProps);
if (cachedResult) {
return Promise.resolve(this._finalizeReadResult(sourceCode, id, extern, cachedResult));
}
return new Promise((resolve, reject) => {
this._getAndCacheTransformedCode(
cacheProps,
(transformError, freshResult) => {
if (transformError) {
reject(transformError);
return;
}
invariant(freshResult != null, 'inconsistent state');
resolve(this._finalizeReadResult(sourceCode, id, extern, freshResult));
},
);
});
});
this._readPromises.set(key, freshPromise);
return freshPromise;
}
hash() {
return `Module : ${this.path}`;
}
isJSON() {
return extname(this.path) === '.json';
}
isAsset() {
return false;
}
isPolyfill() {
return false;
}
toJSON() {
return {
hash: this.hash(),
isJSON: this.isJSON(),
isAsset: this.isAsset(),
type: this.type,
path: this.path,
};
}
}
Module._useGlobalCache = true;
// 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;