metro-memory-fs: add watch()

Reviewed By: rubennorte

Differential Revision: D7443797

fbshipit-source-id: 2ffdfb3649caf057c42313e36e9ff35e70f4f759
This commit is contained in:
Jean Lauliac 2018-04-04 02:50:20 -07:00 committed by Facebook Github Bot
parent 72a66fa8e4
commit 6d8b06dcd9
2 changed files with 202 additions and 8 deletions

View File

@ -367,6 +367,57 @@ it('able to list files of a directory', () => {
expect(fs.readdirSync('/baz')).toEqual(['foo.txt', 'bar.txt', 'glo.txt']);
});
describe('watch', () => {
it('reports changed files', () => {
const changedPaths = [];
fs.writeFileSync('/foo.txt', '');
fs.writeFileSync('/bar.txt', '');
const watcher = collectWatchEvents('/', {}, changedPaths);
fs.writeFileSync('/foo.txt', 'test');
fs.writeFileSync('/bar.txt', 'tadam');
expect(changedPaths).toEqual([
['change', 'foo.txt'],
['change', 'bar.txt'],
]);
watcher.close();
});
it('does not report nested changed files if non-recursive', () => {
const changedPaths = [];
fs.mkdirSync('/foo');
fs.writeFileSync('/foo/bar.txt', '');
const watcher = collectWatchEvents('/', {}, changedPaths);
fs.writeFileSync('/foo/bar.txt', 'test');
expect(changedPaths).toEqual([]);
watcher.close();
});
it('does report nested changed files if recursive', () => {
const changedPaths = [];
fs.mkdirSync('/foo');
fs.writeFileSync('/foo/bar.txt', '');
const watcher = collectWatchEvents('/', {recursive: true}, changedPaths);
fs.writeFileSync('/foo/bar.txt', 'test');
expect(changedPaths).toEqual([['change', 'foo/bar.txt']]);
watcher.close();
});
it('reports created files', () => {
const changedPaths = [];
const watcher = collectWatchEvents('/', {}, changedPaths);
const fd = fs.openSync('/foo.txt', 'w');
expect(changedPaths).toEqual([['rename', 'foo.txt']]);
fs.closeSync(fd);
watcher.close();
});
function collectWatchEvents(entPath, options, events) {
return fs.watch(entPath, options, (eventName, filePath) => {
events.push([eventName, filePath]);
});
}
});
it('throws when trying to read inexistent file', () => {
expectFsError('ENOENT', () => fs.readFileSync('/foo.txt'));
});

View File

@ -15,8 +15,11 @@ const constants = require('constants');
const path = require('path');
const stream = require('stream');
const {EventEmitter} = require('events');
type NodeBase = {|
id: number,
watchers: Array<NodeWatcher>,
|};
type DirectoryNode = {|
@ -39,6 +42,11 @@ type SymbolicLinkNode = {|
type EntityNode = DirectoryNode | FileNode | SymbolicLinkNode;
type NodeWatcher = {
recursive: boolean,
listener: (eventType: 'change' | 'rename', filePath: string) => void,
};
type Encoding =
| 'ascii'
| 'base64'
@ -52,11 +60,13 @@ type Encoding =
type Resolution = {|
+basename: string,
+dirNode: DirectoryNode,
+dirPath: Array<[string, EntityNode]>,
+node: ?EntityNode,
+realpath: string,
|};
type Descriptor = {|
+nodePath: Array<[string, EntityNode]>,
+node: FileNode,
+readable: boolean,
+writable: boolean,
@ -185,6 +195,8 @@ class MemoryFs {
}
closeSync = (fd: number): void => {
const desc = this._getDesc(fd);
this._emitFileChange(desc.nodePath.slice(), {eventType: 'change'});
this._fds.delete(fd);
};
@ -389,9 +401,10 @@ class MemoryFs {
throw makeError('EEXIST', filePath, 'directory or file already exists');
}
dirNode.entries.set(basename, {
type: 'symbolicLink',
id: this._getId(),
target: pathStr(target),
type: 'symbolicLink',
watchers: [],
});
};
@ -512,8 +525,49 @@ class MemoryFs {
return st;
};
watch = (
filePath: string | Buffer,
options?:
| {
encoding?: Encoding,
recursive?: boolean,
persistent?: boolean,
}
| Encoding,
listener?: (
eventType: 'rename' | 'change',
filePath: ?string | Buffer,
) => mixed,
) => {
filePath = pathStr(filePath);
const {node} = this._resolve(filePath);
if (node == null) {
throw makeError('ENOENT', filePath, 'no such file or directory');
}
let encoding, recursive, persistent;
if (typeof options === 'string') {
encoding = options;
} else if (options != null) {
({encoding, recursive, persistent} = options);
}
const watcher = new FSWatcher(node, {
encoding: encoding != null ? encoding : 'utf8',
recursive: recursive != null ? recursive : false,
persistent: persistent != null ? persistent : false,
});
if (listener != null) {
watcher.on('change', listener);
}
return watcher;
};
_makeDir() {
return {type: 'directory', id: this._getId(), entries: new Map()};
return {
entries: new Map(),
id: this._getId(),
type: 'directory',
watchers: [],
};
}
_getId() {
@ -530,13 +584,21 @@ class MemoryFs {
}
const {writable = false, readable = false} = spec;
const {exclusive, mustExist, truncate} = spec;
let {dirNode, node, basename} = this._resolve(filePath);
let {dirNode, node, basename, dirPath} = this._resolve(filePath);
let nodePath;
if (node == null) {
if (mustExist) {
throw makeError('ENOENT', filePath, 'no such file or directory');
}
node = {type: 'file', id: this._getId(), content: new Buffer(0)};
node = {
content: new Buffer(0),
id: this._getId(),
type: 'file',
watchers: [],
};
dirNode.entries.set(basename, node);
nodePath = dirPath.concat([[basename, node]]);
this._emitFileChange(nodePath.slice(), {eventType: 'rename'});
} else {
if (exclusive) {
throw makeError('EEXIST', filePath, 'directory or file already exists');
@ -547,8 +609,15 @@ class MemoryFs {
if (truncate) {
node.content = new Buffer(0);
}
nodePath = dirPath.concat([[basename, node]]);
}
return this._getFd(filePath, {node, position: 0, writable, readable});
return this._getFd(filePath, {
nodePath,
node,
position: 0,
readable,
writable,
});
}
/**
@ -593,9 +662,21 @@ class MemoryFs {
const {nodePath} = context;
return {
realpath: drive + nodePath.map(x => x[0]).join(path.sep),
dirNode: (nodePath[nodePath.length - 2][1]: $FlowFixMe),
dirNode: (() => {
const dirNode =
nodePath.length >= 2
? nodePath[nodePath.length - 2][1]
: context.node;
if (dirNode == null || dirNode.type !== 'directory') {
throw new Error('failed to resolve');
}
return dirNode;
})(),
node: context.node,
basename: (nodePath[nodePath.length - 1][0]: $FlowFixMe),
basename: nullthrows(nodePath[nodePath.length - 1][0]),
dirPath: nodePath
.slice(0, -1)
.map(nodePair => [nodePair[0], nullthrows(nodePair[1])]),
};
}
@ -687,6 +768,26 @@ class MemoryFs {
}
return desc;
}
_emitFileChange(
nodePath: Array<[string, EntityNode]>,
options: {eventType: 'rename' | 'change'},
): void {
const node = nodePath.pop();
let filePath = node[0];
let recursive = false;
while (nodePath.length > 0) {
const dirNode = nodePath.pop();
for (const watcher of dirNode[1].watchers) {
if (recursive && !watcher.recursive) {
continue;
}
watcher.listener(options.eventType, filePath);
}
filePath = path.join(dirNode[0], filePath);
recursive = true;
}
}
}
class Stats {
@ -870,6 +971,41 @@ class WriteFileStream extends stream.Writable {
}
}
class FSWatcher extends EventEmitter {
_encoding: Encoding;
_node: EntityNode;
_nodeWatcher: NodeWatcher;
constructor(
node: EntityNode,
options: {encoding: Encoding, recursive: boolean, persistent: boolean},
) {
super();
this._encoding = options.encoding;
this._nodeWatcher = {
recursive: options.recursive,
listener: this._listener,
};
node.watchers.push(this._nodeWatcher);
this._node = node;
}
close() {
this._node.watchers.splice(this._node.watchers.indexOf(this._nodeWatcher));
}
_listener = (eventType, filePath: string) => {
const encFilePath =
this._encoding === 'buffer' ? Buffer.from(filePath, 'utf8') : filePath;
try {
this.emit('change', eventType, encFilePath);
} catch (error) {
this.close();
this.emit('error', error);
}
};
}
function checkPathLength(entNames, filePath) {
if (entNames.length > 32) {
throw makeError(
@ -888,7 +1024,7 @@ function pathStr(filePath: string | Buffer): string {
return filePath.toString('utf8');
}
function makeError(code, filePath, message) {
function makeError(code: string, filePath: ?string, message: string) {
const err: $FlowFixMe = new Error(
filePath != null
? `${code}: \`${filePath}\`: ${message}`
@ -900,4 +1036,11 @@ function makeError(code, filePath, message) {
return err;
}
function nullthrows<T>(x: ?T): T {
if (x == null) {
throw new Error('item was null or undefined');
}
return x;
}
module.exports = MemoryFs;