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

View File

@ -97,8 +97,10 @@ class ResolutionRequest {
);
}
getOrderedDependencies(response) {
return Promise.resolve().then(() => {
getOrderedDependencies(response, mocksPattern) {
return this._getAllMocks(mocksPattern).then(mocks => {
response.setMocks(mocks);
const entry = this._moduleCache.getModule(this._entryPath);
const visited = Object.create(null);
visited[entry.hash()] = true;
@ -110,8 +112,20 @@ class ResolutionRequest {
depNames.map(name => this.resolveDependency(mod, name))
).then((dependencies) => [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();
const filteredPairs = [];
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) {
toModuleName = normalizePath(toModuleName);
@ -349,6 +377,7 @@ class ResolutionRequest {
_resetResolutionCache() {
this._immediateResolutionCache = Object.create(null);
}
}

View File

@ -13,6 +13,7 @@ class ResolutionResponse {
this.dependencies = [];
this.asyncDependencies = [];
this.mainModuleId = null;
this.mocks = null;
this._mappings = Object.create(null);
this._finalized = false;
}
@ -64,6 +65,10 @@ class ResolutionResponse {
}
}
setMocks(mocks) {
this.mocks = mocks;
}
getResolvedDependencyPairs(module) {
this._assertFinalized();
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,
platforms,
cache,
extensions,
mocksPattern,
extractRequires,
}) {
this._opts = {
activity: activity || defaultActivity,
@ -49,6 +52,9 @@ class DependencyGraph {
providesModuleNodeModules,
platforms: platforms || [],
cache,
extensions: extensions || ['js', 'json'],
mocksPattern,
extractRequires,
};
this._cache = this._opts.cache;
this._helpers = new Helpers(this._opts);
@ -70,7 +76,7 @@ class DependencyGraph {
const allRoots = this._opts.roots.concat(this._opts.assetRoots_DEPRECATED);
this._crawling = crawl(allRoots, {
ignore: this._opts.ignoreFilePath,
exts: ['js', 'json'].concat(this._opts.assetExts),
exts: this._opts.extensions.concat(this._opts.assetExts),
fileWatcher: this._opts.fileWatcher,
});
this._crawling.then((files) => activity.endEvent(crawlActivity));
@ -88,10 +94,15 @@ class DependencyGraph {
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({
fastfs: this._fastfs,
extensions: this._opts.extensions,
moduleCache: this._moduleCache,
assetExts: this._opts.exts,
helpers: this._helpers,
@ -138,12 +149,16 @@ class DependencyGraph {
const response = new ResolutionResponse();
return Promise.all([
req.getOrderedDependencies(response),
req.getOrderedDependencies(response, this._opts.mocksPattern),
req.getAsyncDependencies(response),
]).then(() => response);
});
}
matchFilesByPattern(pattern) {
return this.load().then(() => this._fastfs.matchFilesByPattern(pattern));
}
_getRequestPlatform(entryPath, platform) {
if (platform == null) {
platform = getPlatformExtension(entryPath);
@ -208,6 +223,7 @@ class DependencyGraph {
return this._loading;
});
}
}
function NotFoundError() {

View File

@ -11,11 +11,11 @@
const docblock = require('./DependencyGraph/docblock');
const isAbsolutePath = require('absolute-path');
const path = require('path');
const replacePatterns = require('./replacePatterns');
const extractRequires = require('./lib/extractRequires');
class Module {
constructor(file, fastfs, moduleCache, cache) {
constructor(file, fastfs, moduleCache, cache, extractor) {
if (!isAbsolutePath(file)) {
throw new Error('Expected file to be absolute path but got ' + file);
}
@ -26,12 +26,11 @@ class Module {
this._fastfs = fastfs;
this._moduleCache = moduleCache;
this._cache = cache;
this._extractor = extractor;
}
isHaste() {
return this._cache.get(this.path, 'haste', () =>
this._read().then(data => !!data.id)
);
return this._read().then(data => !!data.id);
}
getName() {
@ -67,19 +66,17 @@ class Module {
}
getDependencies() {
return this._cache.get(this.path, 'dependencies', () =>
this._read().then(data => data.dependencies)
);
}
invalidate() {
this._cache.invalidate(this.path);
return this._read().then(data => data.dependencies);
}
getAsyncDependencies() {
return this._read().then(data => data.asyncDependencies);
}
invalidate() {
this._cache.invalidate(this.path);
}
_read() {
if (!this._reading) {
this._reading = this._fastfs.readFile(this.path).then(content => {
@ -96,7 +93,7 @@ class Module {
if ('extern' in moduleDocBlock) {
data.dependencies = [];
} else {
var dependencies = extractRequires(content);
var dependencies = (this._extractor || extractRequires)(content).deps;
data.dependencies = dependencies.sync;
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;

View File

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

View File

@ -11,18 +11,19 @@
jest
.dontMock('absolute-path')
.dontMock('../fastfs')
.dontMock('../replacePatterns')
.dontMock('../lib/extractRequires')
.dontMock('../lib/replacePatterns')
.dontMock('../DependencyGraph/docblock')
.dontMock('../Module');
jest
.mock('fs');
var Fastfs = require('../fastfs');
var Module = require('../Module');
var ModuleCache = require('../ModuleCache');
var Promise = require('promise');
var fs = require('fs');
const Fastfs = require('../fastfs');
const Module = require('../Module');
const ModuleCache = require('../ModuleCache');
const Promise = require('promise');
const fs = require('fs');
describe('Module', () => {
const fileWatcher = {
@ -30,17 +31,31 @@ describe('Module', () => {
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', () => {
function expectAsyncDependenciesToEqual(expected) {
var fastfs = new Fastfs(
const fastfs = new Fastfs(
'test',
['/root'],
fileWatcher,
{crawling: Promise.resolve(['/root/index.js']), ignore: []},
);
const cache = new Cache();
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 =>
expect(actual).toEqual(expected)
@ -94,4 +109,43 @@ describe('Module', () => {
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()));
}
findFilesByExt(ext, { ignore }) {
findFilesByExt(ext, { ignore } = {}) {
return this.findFilesByExts([ext], {ignore});
}
findFilesByExts(exts, { ignore } = {}) {
return this.getAllFiles()
.filter(
file => file.ext() === ext && (!ignore || !ignore(file.path))
)
.filter(file => (
exts.indexOf(file.ext()) !== -1 && (!ignore || !ignore(file.path))
))
.map(file => file.path);
}
findFilesByExts(exts) {
return this.getAllFiles()
.filter(file => exts.indexOf(file.ext()) !== -1)
.map(file => file.path);
}
findFilesByName(name, { ignore }) {
findFilesByName(name, { ignore } = {}) {
return this.getAllFiles()
.filter(
file => path.basename(file.path) === name &&
@ -102,6 +100,12 @@ class Fastfs extends EventEmitter {
.map(file => file.path);
}
matchFilesByPattern(pattern) {
return this.getAllFiles()
.filter(file => file.path.match(pattern))
.map(file => file.path);
}
readFile(filePath) {
const file = this._getFile(filePath);
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('../')
.dontMock('underscore')
.dontMock('../../DependencyResolver/replacePatterns');
.dontMock('../../DependencyResolver/lib/extractRequires')
.dontMock('../../DependencyResolver/lib/replacePatterns');
jest.mock('path');

View File

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