Sync new haste features from upstream

Reviewed By: amasad

Differential Revision: D2644383

fb-gh-sync-id: 5225e6afb8e5b835865473b2880a77235e6bd6eb
This commit is contained in:
Christoph Pojer 2015-11-16 22:48:28 -08:00 committed by facebook-github-bot-5
parent ccea74fc87
commit a8ded758d0
13 changed files with 405 additions and 89 deletions

View File

@ -15,7 +15,8 @@ const Promise = require('promise');
const GENERIC_PLATFORM = 'generic'; const GENERIC_PLATFORM = 'generic';
class HasteMap { class HasteMap {
constructor({ fastfs, moduleCache, helpers }) { constructor({ extensions, fastfs, moduleCache, helpers }) {
this._extensions = extensions;
this._fastfs = fastfs; this._fastfs = fastfs;
this._moduleCache = moduleCache; this._moduleCache = moduleCache;
this._helpers = helpers; this._helpers = helpers;
@ -24,7 +25,7 @@ class HasteMap {
build() { build() {
this._map = Object.create(null); this._map = Object.create(null);
let promises = this._fastfs.findFilesByExt('js', { let promises = this._fastfs.findFilesByExts(this._extensions, {
ignore: (file) => this._helpers.isNodeModulesDir(file), ignore: (file) => this._helpers.isNodeModulesDir(file),
}).map(file => this._processHasteModule(file)); }).map(file => this._processHasteModule(file));
@ -57,8 +58,7 @@ class HasteMap {
} }
} }
if (this._helpers.extname(absPath) === 'js' || if (this._extensions.indexOf(this._helpers.extname(absPath)) !== -1) {
this._helpers.extname(absPath) === 'json') {
if (path.basename(absPath) === 'package.json') { if (path.basename(absPath) === 'package.json') {
return this._processHastePackage(absPath); return this._processHastePackage(absPath);
} else { } else {

View File

@ -97,8 +97,10 @@ class ResolutionRequest {
); );
} }
getOrderedDependencies(response) { getOrderedDependencies(response, mocksPattern) {
return Promise.resolve().then(() => { return this._getAllMocks(mocksPattern).then(mocks => {
response.setMocks(mocks);
const entry = this._moduleCache.getModule(this._entryPath); const entry = this._moduleCache.getModule(this._entryPath);
const visited = Object.create(null); const visited = Object.create(null);
visited[entry.hash()] = true; visited[entry.hash()] = true;
@ -110,8 +112,20 @@ class ResolutionRequest {
depNames.map(name => this.resolveDependency(mod, name)) depNames.map(name => this.resolveDependency(mod, name))
).then((dependencies) => [depNames, dependencies]) ).then((dependencies) => [depNames, dependencies])
).then(([depNames, dependencies]) => { ).then(([depNames, dependencies]) => {
if (mocks) {
return mod.getName().then(name => {
if (mocks[name]) {
const mockModule =
this._moduleCache.getModule(mocks[name]);
depNames.push(name);
dependencies.push(mockModule);
}
return [depNames, dependencies];
});
}
return Promise.resolve([depNames, dependencies]);
}).then(([depNames, dependencies]) => {
let p = Promise.resolve(); let p = Promise.resolve();
const filteredPairs = []; const filteredPairs = [];
dependencies.forEach((modDep, i) => { dependencies.forEach((modDep, i) => {
@ -163,6 +177,20 @@ class ResolutionRequest {
)); ));
} }
_getAllMocks(pattern) {
// Take all mocks in all the roots into account. This is necessary
// because currently mocks are global: any module can be mocked by
// any mock in the system.
let mocks = null;
if (pattern) {
mocks = Object.create(null);
this._fastfs.matchFilesByPattern(pattern).forEach(file =>
mocks[path.basename(file, path.extname(file))] = file
);
}
return Promise.resolve(mocks);
}
_resolveHasteDependency(fromModule, toModuleName) { _resolveHasteDependency(fromModule, toModuleName) {
toModuleName = normalizePath(toModuleName); toModuleName = normalizePath(toModuleName);
@ -349,6 +377,7 @@ class ResolutionRequest {
_resetResolutionCache() { _resetResolutionCache() {
this._immediateResolutionCache = Object.create(null); this._immediateResolutionCache = Object.create(null);
} }
} }

View File

@ -13,6 +13,7 @@ class ResolutionResponse {
this.dependencies = []; this.dependencies = [];
this.asyncDependencies = []; this.asyncDependencies = [];
this.mainModuleId = null; this.mainModuleId = null;
this.mocks = null;
this._mappings = Object.create(null); this._mappings = Object.create(null);
this._finalized = false; this._finalized = false;
} }
@ -64,6 +65,10 @@ class ResolutionResponse {
} }
} }
setMocks(mocks) {
this.mocks = mocks;
}
getResolvedDependencyPairs(module) { getResolvedDependencyPairs(module) {
this._assertFinalized(); this._assertFinalized();
return this._mappings[module.hash()]; return this._mappings[module.hash()];

View File

@ -3676,4 +3676,184 @@ describe('DependencyGraph', function() {
}); });
}); });
}); });
describe('Extensions', () => {
pit('supports custom file extensions', () => {
var root = '/root';
fs.__setMockFilesystem({
'root': {
'index.jsx': [
'/**',
' * @providesModule index',
' */',
'require("a")',
].join('\n'),
'a.coffee': [
'/**',
' * @providesModule a',
' */',
].join('\n'),
'X.js': '',
},
});
var dgraph = new DependencyGraph({
...defaults,
roots: [root],
extensions: ['jsx', 'coffee'],
});
return dgraph.matchFilesByPattern('.*')
.then(files => {
expect(files).toEqual([
'/root/index.jsx', '/root/a.coffee',
]);
})
.then(() => getOrderedDependenciesAsJSON(dgraph, '/root/index.jsx'))
.then(deps => {
expect(deps).toEqual([
{
dependencies: ['a'],
id: 'index',
isAsset: false,
isAsset_DEPRECATED: false,
isJSON: false,
isPolyfill: false,
path: '/root/index.jsx',
resolution: undefined,
},
{
dependencies: [],
id: 'a',
isAsset: false,
isAsset_DEPRECATED: false,
isJSON: false,
isPolyfill: false,
path: '/root/a.coffee',
resolution: undefined,
},
]);
});
});
});
describe('Mocks', () => {
pit('resolves to null if mocksPattern is not specified', () => {
var root = '/root';
fs.__setMockFilesystem({
'root': {
'__mocks': {
'A.js': '',
},
'index.js': '',
},
});
var dgraph = new DependencyGraph({
...defaults,
roots: [root],
});
return dgraph.getDependencies('/root/index.js')
.then(response => response.finalize())
.then(response => {
expect(response.mocks).toBe(null);
});
});
pit('retrieves a list of all mocks in the system', () => {
var root = '/root';
fs.__setMockFilesystem({
'root': {
'__mocks__': {
'A.js': '',
'b.js': '',
},
'b.js': [
'/**',
' * @providesModule b',
' */',
].join('\n'),
},
});
var dgraph = new DependencyGraph({
...defaults,
roots: [root],
mocksPattern: /(?:[\\/]|^)__mocks__[\\/]([^\/]+)\.js$/,
});
return dgraph.getDependencies('/root/b.js')
.then(response => response.finalize())
.then(response => {
expect(response.mocks).toEqual({
A: '/root/__mocks__/A.js',
b: '/root/__mocks__/b.js',
});
});
});
pit('adds mocks as a dependency of their actual module', () => {
var root = '/root';
fs.__setMockFilesystem({
'root': {
'__mocks__': {
'A.js': [
'require("b");',
].join('\n'),
'b.js': '',
},
'A.js': [
'/**',
' * @providesModule A',
' */',
'require("foo");',
].join('\n'),
'foo.js': [
'/**',
' * @providesModule foo',
' */',
].join('\n'),
},
});
var dgraph = new DependencyGraph({
...defaults,
roots: [root],
mocksPattern: /(?:[\\/]|^)__mocks__[\\/]([^\/]+)\.js$/,
});
return getOrderedDependenciesAsJSON(dgraph, '/root/A.js')
.then(deps => {
expect(deps).toEqual([
{
path: '/root/A.js',
isJSON: false,
isAsset: false,
isAsset_DEPRECATED: false,
isPolyfill: false,
id: 'A',
dependencies: ['foo', 'A'],
},
{
path: '/root/foo.js',
isJSON: false,
isAsset: false,
isAsset_DEPRECATED: false,
isPolyfill: false,
id: 'foo',
dependencies: [],
},
{
path: '/root/__mocks__/A.js',
isJSON: false,
isAsset: false,
isAsset_DEPRECATED: false,
isPolyfill: false,
id: '/root/__mocks__/A.js',
dependencies: ['b'],
},
]);
});
});
});
}); });

View File

@ -38,6 +38,9 @@ class DependencyGraph {
providesModuleNodeModules, providesModuleNodeModules,
platforms, platforms,
cache, cache,
extensions,
mocksPattern,
extractRequires,
}) { }) {
this._opts = { this._opts = {
activity: activity || defaultActivity, activity: activity || defaultActivity,
@ -49,6 +52,9 @@ class DependencyGraph {
providesModuleNodeModules, providesModuleNodeModules,
platforms: platforms || [], platforms: platforms || [],
cache, cache,
extensions: extensions || ['js', 'json'],
mocksPattern,
extractRequires,
}; };
this._cache = this._opts.cache; this._cache = this._opts.cache;
this._helpers = new Helpers(this._opts); this._helpers = new Helpers(this._opts);
@ -70,7 +76,7 @@ class DependencyGraph {
const allRoots = this._opts.roots.concat(this._opts.assetRoots_DEPRECATED); const allRoots = this._opts.roots.concat(this._opts.assetRoots_DEPRECATED);
this._crawling = crawl(allRoots, { this._crawling = crawl(allRoots, {
ignore: this._opts.ignoreFilePath, ignore: this._opts.ignoreFilePath,
exts: ['js', 'json'].concat(this._opts.assetExts), exts: this._opts.extensions.concat(this._opts.assetExts),
fileWatcher: this._opts.fileWatcher, fileWatcher: this._opts.fileWatcher,
}); });
this._crawling.then((files) => activity.endEvent(crawlActivity)); this._crawling.then((files) => activity.endEvent(crawlActivity));
@ -88,10 +94,15 @@ class DependencyGraph {
this._fastfs.on('change', this._processFileChange.bind(this)); this._fastfs.on('change', this._processFileChange.bind(this));
this._moduleCache = new ModuleCache(this._fastfs, this._cache); this._moduleCache = new ModuleCache(
this._fastfs,
this._cache,
this._opts.extractRequires
);
this._hasteMap = new HasteMap({ this._hasteMap = new HasteMap({
fastfs: this._fastfs, fastfs: this._fastfs,
extensions: this._opts.extensions,
moduleCache: this._moduleCache, moduleCache: this._moduleCache,
assetExts: this._opts.exts, assetExts: this._opts.exts,
helpers: this._helpers, helpers: this._helpers,
@ -138,12 +149,16 @@ class DependencyGraph {
const response = new ResolutionResponse(); const response = new ResolutionResponse();
return Promise.all([ return Promise.all([
req.getOrderedDependencies(response), req.getOrderedDependencies(response, this._opts.mocksPattern),
req.getAsyncDependencies(response), req.getAsyncDependencies(response),
]).then(() => response); ]).then(() => response);
}); });
} }
matchFilesByPattern(pattern) {
return this.load().then(() => this._fastfs.matchFilesByPattern(pattern));
}
_getRequestPlatform(entryPath, platform) { _getRequestPlatform(entryPath, platform) {
if (platform == null) { if (platform == null) {
platform = getPlatformExtension(entryPath); platform = getPlatformExtension(entryPath);
@ -208,6 +223,7 @@ class DependencyGraph {
return this._loading; return this._loading;
}); });
} }
} }
function NotFoundError() { function NotFoundError() {

View File

@ -11,11 +11,11 @@
const docblock = require('./DependencyGraph/docblock'); const docblock = require('./DependencyGraph/docblock');
const isAbsolutePath = require('absolute-path'); const isAbsolutePath = require('absolute-path');
const path = require('path'); const path = require('path');
const replacePatterns = require('./replacePatterns'); const extractRequires = require('./lib/extractRequires');
class Module { class Module {
constructor(file, fastfs, moduleCache, cache) { constructor(file, fastfs, moduleCache, cache, extractor) {
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);
} }
@ -26,12 +26,11 @@ class Module {
this._fastfs = fastfs; this._fastfs = fastfs;
this._moduleCache = moduleCache; this._moduleCache = moduleCache;
this._cache = cache; this._cache = cache;
this._extractor = extractor;
} }
isHaste() { isHaste() {
return this._cache.get(this.path, 'haste', () => return this._read().then(data => !!data.id);
this._read().then(data => !!data.id)
);
} }
getName() { getName() {
@ -67,19 +66,17 @@ class Module {
} }
getDependencies() { getDependencies() {
return this._cache.get(this.path, 'dependencies', () => return this._read().then(data => data.dependencies);
this._read().then(data => data.dependencies)
);
}
invalidate() {
this._cache.invalidate(this.path);
} }
getAsyncDependencies() { getAsyncDependencies() {
return this._read().then(data => data.asyncDependencies); return this._read().then(data => data.asyncDependencies);
} }
invalidate() {
this._cache.invalidate(this.path);
}
_read() { _read() {
if (!this._reading) { if (!this._reading) {
this._reading = this._fastfs.readFile(this.path).then(content => { this._reading = this._fastfs.readFile(this.path).then(content => {
@ -96,7 +93,7 @@ class Module {
if ('extern' in moduleDocBlock) { if ('extern' in moduleDocBlock) {
data.dependencies = []; data.dependencies = [];
} else { } else {
var dependencies = extractRequires(content); var dependencies = (this._extractor || extractRequires)(content).deps;
data.dependencies = dependencies.sync; data.dependencies = dependencies.sync;
data.asyncDependencies = dependencies.async; data.asyncDependencies = dependencies.async;
} }
@ -140,48 +137,4 @@ class Module {
} }
} }
/**
* Extract all required modules from a `code` string.
*/
const blockCommentRe = /\/\*(.|\n)*?\*\//g;
const lineCommentRe = /\/\/.+(\n|$)/g;
function extractRequires(code) {
var deps = {
sync: [],
async: [],
};
code
.replace(blockCommentRe, '')
.replace(lineCommentRe, '')
// Parse sync dependencies. See comment below for further detils.
.replace(replacePatterns.IMPORT_RE, (match, pre, quot, dep, post) => {
deps.sync.push(dep);
return match;
})
.replace(replacePatterns.EXPORT_RE, (match, pre, quot, dep, post) => {
deps.sync.push(dep);
return match;
})
// Parse the sync dependencies this module has. When the module is
// required, all it's sync dependencies will be loaded into memory.
// Sync dependencies can be defined either using `require` or the ES6
// `import` or `export` syntaxes:
// var dep1 = require('dep1');
.replace(replacePatterns.REQUIRE_RE, (match, pre, quot, dep, post) => {
deps.sync.push(dep);
})
// Parse async dependencies this module has. As opposed to what happens
// with sync dependencies, when the module is required, it's async
// dependencies won't be loaded into memory. This is deferred till the
// code path gets to the import statement:
// System.import('dep1')
.replace(replacePatterns.SYSTEM_IMPORT_RE, (match, pre, quot, dep, post) => {
deps.async.push([dep]);
return match;
});
return deps;
}
module.exports = Module; module.exports = Module;

View File

@ -7,11 +7,12 @@ const path = require('path');
class ModuleCache { class ModuleCache {
constructor(fastfs, cache) { constructor(fastfs, cache, extractRequires) {
this._moduleCache = Object.create(null); this._moduleCache = Object.create(null);
this._packageCache = Object.create(null); this._packageCache = Object.create(null);
this._fastfs = fastfs; this._fastfs = fastfs;
this._cache = cache; this._cache = cache;
this._extractRequires = extractRequires;
fastfs.on('change', this._processFileChange.bind(this)); fastfs.on('change', this._processFileChange.bind(this));
} }
@ -23,6 +24,7 @@ class ModuleCache {
this._fastfs, this._fastfs,
this, this,
this._cache, this._cache,
this._extractRequires
); );
} }
return this._moduleCache[filePath]; return this._moduleCache[filePath];

View File

@ -11,18 +11,19 @@
jest jest
.dontMock('absolute-path') .dontMock('absolute-path')
.dontMock('../fastfs') .dontMock('../fastfs')
.dontMock('../replacePatterns') .dontMock('../lib/extractRequires')
.dontMock('../lib/replacePatterns')
.dontMock('../DependencyGraph/docblock') .dontMock('../DependencyGraph/docblock')
.dontMock('../Module'); .dontMock('../Module');
jest jest
.mock('fs'); .mock('fs');
var Fastfs = require('../fastfs'); const Fastfs = require('../fastfs');
var Module = require('../Module'); const Module = require('../Module');
var ModuleCache = require('../ModuleCache'); const ModuleCache = require('../ModuleCache');
var Promise = require('promise'); const Promise = require('promise');
var fs = require('fs'); const fs = require('fs');
describe('Module', () => { describe('Module', () => {
const fileWatcher = { const fileWatcher = {
@ -30,17 +31,31 @@ describe('Module', () => {
isWatchman: () => Promise.resolve(false), isWatchman: () => Promise.resolve(false),
}; };
const Cache = jest.genMockFn();
Cache.prototype.get = jest.genMockFn().mockImplementation(
(filepath, field, cb) => cb(filepath)
);
Cache.prototype.invalidate = jest.genMockFn();
Cache.prototype.end = jest.genMockFn();
describe('Async Dependencies', () => { describe('Async Dependencies', () => {
function expectAsyncDependenciesToEqual(expected) { function expectAsyncDependenciesToEqual(expected) {
var fastfs = new Fastfs( const fastfs = new Fastfs(
'test', 'test',
['/root'], ['/root'],
fileWatcher, fileWatcher,
{crawling: Promise.resolve(['/root/index.js']), ignore: []}, {crawling: Promise.resolve(['/root/index.js']), ignore: []},
); );
const cache = new Cache();
return fastfs.build().then(() => { return fastfs.build().then(() => {
var module = new Module('/root/index.js', fastfs, new ModuleCache(fastfs)); const module = new Module(
'/root/index.js',
fastfs,
new ModuleCache(fastfs, cache),
cache
);
return module.getAsyncDependencies().then(actual => return module.getAsyncDependencies().then(actual =>
expect(actual).toEqual(expected) expect(actual).toEqual(expected)
@ -94,4 +109,43 @@ describe('Module', () => {
return expectAsyncDependenciesToEqual([['dep1']]); return expectAsyncDependenciesToEqual([['dep1']]);
}); });
}); });
describe('Extrators', () => {
function createModuleWithExtractor(extractor) {
const fastfs = new Fastfs(
'test',
['/root'],
fileWatcher,
{crawling: Promise.resolve(['/root/index.js']), ignore: []},
);
const cache = new Cache();
return fastfs.build().then(() => {
return new Module(
'/root/index.js',
fastfs,
new ModuleCache(fastfs, cache),
cache,
extractor
);
});
}
pit('uses custom require extractors if specified', () => {
fs.__setMockFilesystem({
'root': {
'index.js': '',
},
});
return createModuleWithExtractor(
code => ({deps: {sync: ['foo', 'bar']}})
).then(module =>
module.getDependencies().then(actual =>
expect(actual).toEqual(['foo', 'bar'])
)
);
});
});
}); });

View File

@ -79,21 +79,19 @@ class Fastfs extends EventEmitter {
return [].concat(...this._roots.map(root => root.getFiles())); return [].concat(...this._roots.map(root => root.getFiles()));
} }
findFilesByExt(ext, { ignore }) { findFilesByExt(ext, { ignore } = {}) {
return this.findFilesByExts([ext], {ignore});
}
findFilesByExts(exts, { ignore } = {}) {
return this.getAllFiles() return this.getAllFiles()
.filter( .filter(file => (
file => file.ext() === ext && (!ignore || !ignore(file.path)) exts.indexOf(file.ext()) !== -1 && (!ignore || !ignore(file.path))
) ))
.map(file => file.path); .map(file => file.path);
} }
findFilesByExts(exts) { findFilesByName(name, { ignore } = {}) {
return this.getAllFiles()
.filter(file => exts.indexOf(file.ext()) !== -1)
.map(file => file.path);
}
findFilesByName(name, { ignore }) {
return this.getAllFiles() return this.getAllFiles()
.filter( .filter(
file => path.basename(file.path) === name && file => path.basename(file.path) === name &&
@ -102,6 +100,12 @@ class Fastfs extends EventEmitter {
.map(file => file.path); .map(file => file.path);
} }
matchFilesByPattern(pattern) {
return this.getAllFiles()
.filter(file => file.path.match(pattern))
.map(file => file.path);
}
readFile(filePath) { readFile(filePath) {
const file = this._getFile(filePath); const file = this._getFile(filePath);
if (!file) { if (!file) {

View File

@ -0,0 +1,57 @@
/**
* 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.
*/
'use strict';
const replacePatterns = require('./replacePatterns');
/**
* Extract all required modules from a `code` string.
*/
const blockCommentRe = /\/\*(.|\n)*?\*\//g;
const lineCommentRe = /\/\/.+(\n|$)/g;
function extractRequires(code) {
var deps = {
sync: [],
async: [],
};
code = code
.replace(blockCommentRe, '')
.replace(lineCommentRe, '')
// Parse the sync dependencies this module has. When the module is
// required, all it's sync dependencies will be loaded into memory.
// Sync dependencies can be defined either using `require` or the ES6
// `import` or `export` syntaxes:
// var dep1 = require('dep1');
.replace(replacePatterns.IMPORT_RE, (match, pre, quot, dep, post) => {
deps.sync.push(dep);
return match;
})
.replace(replacePatterns.EXPORT_RE, (match, pre, quot, dep, post) => {
deps.sync.push(dep);
return match;
})
.replace(replacePatterns.REQUIRE_RE, (match, pre, quot, dep, post) => {
deps.sync.push(dep);
return match;
})
// Parse async dependencies this module has. As opposed to what happens
// with sync dependencies, when the module is required, it's async
// dependencies won't be loaded into memory. This is deferred till the
// code path gets to the import statement:
// System.import('dep1')
.replace(replacePatterns.SYSTEM_IMPORT_RE, (match, pre, quot, dep, post) => {
deps.async.push([dep]);
return match;
});
return {code, deps};
}
module.exports = extractRequires;

View File

@ -0,0 +1,15 @@
/**
* 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.
*/
'use strict';
exports.IMPORT_RE = /(\bimport\s+(?:[^'"]+\s+from\s+)??)(['"])([^'"]+)(\2)/g;
exports.EXPORT_RE = /(\bexport\s+(?:[^'"]+\s+from\s+)??)(['"])([^'"]+)(\2)/g;
exports.REQUIRE_RE = /(\brequire\s*?\(\s*?)(['"])([^'"]+)(\2\s*?\))/g;
exports.SYSTEM_IMPORT_RE = /(\bSystem\.import\s*?\(\s*?)(['"])([^'"]+)(\2\s*?\))/g;

View File

@ -10,7 +10,8 @@
jest.dontMock('../') jest.dontMock('../')
.dontMock('underscore') .dontMock('underscore')
.dontMock('../../DependencyResolver/replacePatterns'); .dontMock('../../DependencyResolver/lib/extractRequires')
.dontMock('../../DependencyResolver/lib/replacePatterns');
jest.mock('path'); jest.mock('path');

View File

@ -12,7 +12,7 @@
const path = require('path'); const path = require('path');
const Activity = require('../Activity'); const Activity = require('../Activity');
const DependencyGraph = require('../DependencyResolver/DependencyGraph'); const DependencyGraph = require('../DependencyResolver/DependencyGraph');
const replacePatterns = require('../DependencyResolver/replacePatterns'); const replacePatterns = require('../DependencyResolver/lib/replacePatterns');
const Polyfill = require('../DependencyResolver/Polyfill'); const Polyfill = require('../DependencyResolver/Polyfill');
const declareOpts = require('../lib/declareOpts'); const declareOpts = require('../lib/declareOpts');
const Promise = require('promise'); const Promise = require('promise');