mirror of
https://github.com/status-im/metro.git
synced 2025-02-07 08:44:26 +00:00
Summary: public Adds the ability to read files partially with `readWhile` to `Fastfs` This feature will be used by for building the haste map, where we only need to read the doc block (if present) to extract the provided module name. Reviewed By: martinbigio Differential Revision: D2878093 fb-gh-sync-id: 219cf6d5962b89eeb42c728e176bf9fcf6a67e9c
377 lines
9.0 KiB
JavaScript
377 lines
9.0 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.
|
|
*/
|
|
'use strict';
|
|
|
|
const Promise = require('promise');
|
|
const {EventEmitter} = require('events');
|
|
|
|
const fs = require('graceful-fs');
|
|
const path = require('path');
|
|
|
|
const open = Promise.denodeify(fs.open);
|
|
const readFile = Promise.denodeify(fs.readFile);
|
|
const stat = Promise.denodeify(fs.stat);
|
|
|
|
const hasOwn = Object.prototype.hasOwnProperty;
|
|
|
|
const NOT_FOUND_IN_ROOTS = 'NotFoundInRootsError';
|
|
|
|
class Fastfs extends EventEmitter {
|
|
constructor(name, roots, fileWatcher, {ignore, crawling, activity}) {
|
|
super();
|
|
this._name = name;
|
|
this._fileWatcher = fileWatcher;
|
|
this._ignore = ignore;
|
|
this._roots = roots.map(root => new File(root, { isDir: true }));
|
|
this._fastPaths = Object.create(null);
|
|
this._crawling = crawling;
|
|
this._activity = activity;
|
|
}
|
|
|
|
build() {
|
|
const rootsPattern = new RegExp(
|
|
'^(' + this._roots.map(root => escapeRegExp(root.path)).join('|') + ')'
|
|
);
|
|
|
|
return this._crawling.then(files => {
|
|
let fastfsActivity;
|
|
const activity = this._activity;
|
|
if (activity) {
|
|
fastfsActivity = activity.startEvent('Building in-memory fs for ' + this._name);
|
|
}
|
|
files.forEach(filePath => {
|
|
if (filePath.match(rootsPattern)) {
|
|
const newFile = new File(filePath, { isDir: false });
|
|
const parent = this._fastPaths[path.dirname(filePath)];
|
|
if (parent) {
|
|
parent.addChild(newFile);
|
|
} else {
|
|
this._add(newFile);
|
|
for (let file = newFile; file; file = file.parent) {
|
|
if (!this._fastPaths[file.path]) {
|
|
this._fastPaths[file.path] = file;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
if (activity) {
|
|
activity.endEvent(fastfsActivity);
|
|
}
|
|
this._fileWatcher.on('all', this._processFileChange.bind(this));
|
|
});
|
|
}
|
|
|
|
stat(filePath) {
|
|
return Promise.resolve().then(() => {
|
|
const file = this._getFile(filePath);
|
|
return file.stat();
|
|
});
|
|
}
|
|
|
|
getAllFiles() {
|
|
// one-level-deep flatten of files
|
|
return [].concat(...this._roots.map(root => root.getFiles()));
|
|
}
|
|
|
|
findFilesByExt(ext, { ignore } = {}) {
|
|
return this.findFilesByExts([ext], {ignore});
|
|
}
|
|
|
|
findFilesByExts(exts, { ignore } = {}) {
|
|
return this.getAllFiles()
|
|
.filter(file => (
|
|
exts.indexOf(file.ext()) !== -1 && (!ignore || !ignore(file.path))
|
|
))
|
|
.map(file => file.path);
|
|
}
|
|
|
|
findFilesByName(name, { ignore } = {}) {
|
|
return this.getAllFiles()
|
|
.filter(
|
|
file => path.basename(file.path) === name &&
|
|
(!ignore || !ignore(file.path))
|
|
)
|
|
.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) {
|
|
throw new Error(`Unable to find file with path: ${filePath}`);
|
|
}
|
|
return file.read();
|
|
}
|
|
|
|
readWhile(filePath, predicate) {
|
|
const file = this._getFile(filePath);
|
|
if (!file) {
|
|
throw new Error(`Unable to find file with path: ${filePath}`);
|
|
}
|
|
return file.readWhile(predicate);
|
|
}
|
|
|
|
closest(filePath, name) {
|
|
for (let file = this._getFile(filePath).parent;
|
|
file;
|
|
file = file.parent) {
|
|
if (file.children[name]) {
|
|
return file.children[name].path;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
fileExists(filePath) {
|
|
let file;
|
|
try {
|
|
file = this._getFile(filePath);
|
|
} catch (e) {
|
|
if (e.type === NOT_FOUND_IN_ROOTS) {
|
|
return false;
|
|
}
|
|
throw e;
|
|
}
|
|
|
|
return file && !file.isDir;
|
|
}
|
|
|
|
dirExists(filePath) {
|
|
let file;
|
|
try {
|
|
file = this._getFile(filePath);
|
|
} catch (e) {
|
|
if (e.type === NOT_FOUND_IN_ROOTS) {
|
|
return false;
|
|
}
|
|
throw e;
|
|
}
|
|
|
|
return file && file.isDir;
|
|
}
|
|
|
|
matches(dir, pattern) {
|
|
const dirFile = this._getFile(dir);
|
|
if (!dirFile.isDir) {
|
|
throw new Error(`Expected file ${dirFile.path} to be a directory`);
|
|
}
|
|
|
|
return Object.keys(dirFile.children)
|
|
.filter(name => name.match(pattern))
|
|
.map(name => path.join(dirFile.path, name));
|
|
}
|
|
|
|
_getRoot(filePath) {
|
|
for (let i = 0; i < this._roots.length; i++) {
|
|
const possibleRoot = this._roots[i];
|
|
if (isDescendant(possibleRoot.path, filePath)) {
|
|
return possibleRoot;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
_getAndAssertRoot(filePath) {
|
|
const root = this._getRoot(filePath);
|
|
if (!root) {
|
|
const error = new Error(`File ${filePath} not found in any of the roots`);
|
|
error.type = NOT_FOUND_IN_ROOTS;
|
|
throw error;
|
|
}
|
|
return root;
|
|
}
|
|
|
|
_getFile(filePath) {
|
|
filePath = path.normalize(filePath);
|
|
if (!hasOwn.call(this._fastPaths, filePath)) {
|
|
this._fastPaths[filePath] = this._getAndAssertRoot(filePath).getFileFromPath(filePath);
|
|
}
|
|
|
|
return this._fastPaths[filePath];
|
|
}
|
|
|
|
_add(file) {
|
|
this._getAndAssertRoot(file.path).addChild(file);
|
|
}
|
|
|
|
_processFileChange(type, filePath, root, fstat) {
|
|
const absPath = path.join(root, filePath);
|
|
if (this._ignore(absPath) || (fstat && fstat.isDirectory())) {
|
|
return;
|
|
}
|
|
|
|
// Make sure this event belongs to one of our roots.
|
|
if (!this._getRoot(absPath)) {
|
|
return;
|
|
}
|
|
|
|
if (type === 'delete' || type === 'change') {
|
|
const file = this._getFile(absPath);
|
|
if (file) {
|
|
file.remove();
|
|
}
|
|
}
|
|
|
|
delete this._fastPaths[path.normalize(absPath)];
|
|
|
|
if (type !== 'delete') {
|
|
this._add(new File(absPath, { isDir: false }));
|
|
}
|
|
|
|
this.emit('change', type, filePath, root, fstat);
|
|
}
|
|
}
|
|
|
|
class File {
|
|
constructor(filePath, { isDir }) {
|
|
this.path = filePath;
|
|
this.isDir = Boolean(isDir);
|
|
if (this.isDir) {
|
|
this.children = Object.create(null);
|
|
}
|
|
}
|
|
|
|
read() {
|
|
if (!this._read) {
|
|
this._read = readFile(this.path, 'utf8');
|
|
}
|
|
return this._read;
|
|
}
|
|
|
|
readWhile(predicate) {
|
|
const CHUNK_SIZE = 512;
|
|
let result = '';
|
|
|
|
return open(this.path, 'r').then(fd => {
|
|
/* global Buffer: true */
|
|
const buffer = new Buffer(CHUNK_SIZE);
|
|
const p = new Promise((resolve, reject) => {
|
|
let counter = 0;
|
|
const callback = (error, bytesRead) => {
|
|
if (error) {
|
|
reject();
|
|
return;
|
|
}
|
|
|
|
const chunk = buffer.toString('utf8', 0, bytesRead);
|
|
result += chunk;
|
|
if (bytesRead > 0 && predicate(chunk, counter++, result)) {
|
|
readChunk(fd, buffer, callback);
|
|
} else {
|
|
if (bytesRead === 0 && !this._read) { // reached EOF
|
|
this._read = Promise.resolve(result);
|
|
}
|
|
resolve(result);
|
|
}
|
|
};
|
|
readChunk(fd, buffer, callback);
|
|
});
|
|
|
|
p.catch(() => fs.close(fd));
|
|
return p;
|
|
});
|
|
|
|
function readChunk(fd, buffer, callback) {
|
|
fs.read(fd, buffer, 0, CHUNK_SIZE, null, callback);
|
|
}
|
|
}
|
|
|
|
stat() {
|
|
if (!this._stat) {
|
|
this._stat = stat(this.path);
|
|
}
|
|
|
|
return this._stat;
|
|
}
|
|
|
|
addChild(file) {
|
|
const parts = path.relative(this.path, file.path).split(path.sep);
|
|
|
|
if (parts.length === 0) {
|
|
return;
|
|
}
|
|
|
|
if (parts.length === 1) {
|
|
this.children[parts[0]] = file;
|
|
file.parent = this;
|
|
} else if (this.children[parts[0]]) {
|
|
this.children[parts[0]].addChild(file);
|
|
} else {
|
|
const dir = new File(path.join(this.path, parts[0]), { isDir: true });
|
|
dir.parent = this;
|
|
this.children[parts[0]] = dir;
|
|
dir.addChild(file);
|
|
}
|
|
}
|
|
|
|
getFileFromPath(filePath) {
|
|
const parts = path.relative(this.path, filePath)
|
|
.split(path.sep);
|
|
|
|
/*eslint consistent-this:0*/
|
|
let file = this;
|
|
for (let i = 0; i < parts.length; i++) {
|
|
const fileName = parts[i];
|
|
if (!fileName) {
|
|
continue;
|
|
}
|
|
|
|
if (!file || !file.isDir) {
|
|
// File not found.
|
|
return null;
|
|
}
|
|
|
|
file = file.children[fileName];
|
|
}
|
|
|
|
return file;
|
|
}
|
|
|
|
getFiles() {
|
|
let files = [];
|
|
Object.keys(this.children).forEach(key => {
|
|
const file = this.children[key];
|
|
if (file.isDir) {
|
|
files = files.concat(file.getFiles());
|
|
} else {
|
|
files.push(file);
|
|
}
|
|
});
|
|
return files;
|
|
}
|
|
|
|
ext() {
|
|
return path.extname(this.path).replace(/^\./, '');
|
|
}
|
|
|
|
remove() {
|
|
if (!this.parent) {
|
|
throw new Error(`No parent to delete ${this.path} from`);
|
|
}
|
|
|
|
delete this.parent.children[path.basename(this.path)];
|
|
}
|
|
}
|
|
|
|
function isDescendant(root, child) {
|
|
return path.relative(root, child).indexOf('..') !== 0;
|
|
}
|
|
|
|
function escapeRegExp(str) {
|
|
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
|
|
}
|
|
|
|
module.exports = Fastfs;
|