mirror of https://github.com/status-im/metro.git
metro-memory-fs: add watch()
Reviewed By: rubennorte Differential Revision: D7443797 fbshipit-source-id: 2ffdfb3649caf057c42313e36e9ff35e70f4f759
This commit is contained in:
parent
72a66fa8e4
commit
6d8b06dcd9
|
@ -367,6 +367,57 @@ it('able to list files of a directory', () => {
|
||||||
expect(fs.readdirSync('/baz')).toEqual(['foo.txt', 'bar.txt', 'glo.txt']);
|
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', () => {
|
it('throws when trying to read inexistent file', () => {
|
||||||
expectFsError('ENOENT', () => fs.readFileSync('/foo.txt'));
|
expectFsError('ENOENT', () => fs.readFileSync('/foo.txt'));
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,8 +15,11 @@ const constants = require('constants');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const stream = require('stream');
|
const stream = require('stream');
|
||||||
|
|
||||||
|
const {EventEmitter} = require('events');
|
||||||
|
|
||||||
type NodeBase = {|
|
type NodeBase = {|
|
||||||
id: number,
|
id: number,
|
||||||
|
watchers: Array<NodeWatcher>,
|
||||||
|};
|
|};
|
||||||
|
|
||||||
type DirectoryNode = {|
|
type DirectoryNode = {|
|
||||||
|
@ -39,6 +42,11 @@ type SymbolicLinkNode = {|
|
||||||
|
|
||||||
type EntityNode = DirectoryNode | FileNode | SymbolicLinkNode;
|
type EntityNode = DirectoryNode | FileNode | SymbolicLinkNode;
|
||||||
|
|
||||||
|
type NodeWatcher = {
|
||||||
|
recursive: boolean,
|
||||||
|
listener: (eventType: 'change' | 'rename', filePath: string) => void,
|
||||||
|
};
|
||||||
|
|
||||||
type Encoding =
|
type Encoding =
|
||||||
| 'ascii'
|
| 'ascii'
|
||||||
| 'base64'
|
| 'base64'
|
||||||
|
@ -52,11 +60,13 @@ type Encoding =
|
||||||
type Resolution = {|
|
type Resolution = {|
|
||||||
+basename: string,
|
+basename: string,
|
||||||
+dirNode: DirectoryNode,
|
+dirNode: DirectoryNode,
|
||||||
|
+dirPath: Array<[string, EntityNode]>,
|
||||||
+node: ?EntityNode,
|
+node: ?EntityNode,
|
||||||
+realpath: string,
|
+realpath: string,
|
||||||
|};
|
|};
|
||||||
|
|
||||||
type Descriptor = {|
|
type Descriptor = {|
|
||||||
|
+nodePath: Array<[string, EntityNode]>,
|
||||||
+node: FileNode,
|
+node: FileNode,
|
||||||
+readable: boolean,
|
+readable: boolean,
|
||||||
+writable: boolean,
|
+writable: boolean,
|
||||||
|
@ -185,6 +195,8 @@ class MemoryFs {
|
||||||
}
|
}
|
||||||
|
|
||||||
closeSync = (fd: number): void => {
|
closeSync = (fd: number): void => {
|
||||||
|
const desc = this._getDesc(fd);
|
||||||
|
this._emitFileChange(desc.nodePath.slice(), {eventType: 'change'});
|
||||||
this._fds.delete(fd);
|
this._fds.delete(fd);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -389,9 +401,10 @@ class MemoryFs {
|
||||||
throw makeError('EEXIST', filePath, 'directory or file already exists');
|
throw makeError('EEXIST', filePath, 'directory or file already exists');
|
||||||
}
|
}
|
||||||
dirNode.entries.set(basename, {
|
dirNode.entries.set(basename, {
|
||||||
type: 'symbolicLink',
|
|
||||||
id: this._getId(),
|
id: this._getId(),
|
||||||
target: pathStr(target),
|
target: pathStr(target),
|
||||||
|
type: 'symbolicLink',
|
||||||
|
watchers: [],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -512,8 +525,49 @@ class MemoryFs {
|
||||||
return st;
|
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() {
|
_makeDir() {
|
||||||
return {type: 'directory', id: this._getId(), entries: new Map()};
|
return {
|
||||||
|
entries: new Map(),
|
||||||
|
id: this._getId(),
|
||||||
|
type: 'directory',
|
||||||
|
watchers: [],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
_getId() {
|
_getId() {
|
||||||
|
@ -530,13 +584,21 @@ class MemoryFs {
|
||||||
}
|
}
|
||||||
const {writable = false, readable = false} = spec;
|
const {writable = false, readable = false} = spec;
|
||||||
const {exclusive, mustExist, truncate} = 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 (node == null) {
|
||||||
if (mustExist) {
|
if (mustExist) {
|
||||||
throw makeError('ENOENT', filePath, 'no such file or directory');
|
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);
|
dirNode.entries.set(basename, node);
|
||||||
|
nodePath = dirPath.concat([[basename, node]]);
|
||||||
|
this._emitFileChange(nodePath.slice(), {eventType: 'rename'});
|
||||||
} else {
|
} else {
|
||||||
if (exclusive) {
|
if (exclusive) {
|
||||||
throw makeError('EEXIST', filePath, 'directory or file already exists');
|
throw makeError('EEXIST', filePath, 'directory or file already exists');
|
||||||
|
@ -547,8 +609,15 @@ class MemoryFs {
|
||||||
if (truncate) {
|
if (truncate) {
|
||||||
node.content = new Buffer(0);
|
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;
|
const {nodePath} = context;
|
||||||
return {
|
return {
|
||||||
realpath: drive + nodePath.map(x => x[0]).join(path.sep),
|
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,
|
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;
|
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 {
|
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) {
|
function checkPathLength(entNames, filePath) {
|
||||||
if (entNames.length > 32) {
|
if (entNames.length > 32) {
|
||||||
throw makeError(
|
throw makeError(
|
||||||
|
@ -888,7 +1024,7 @@ function pathStr(filePath: string | Buffer): string {
|
||||||
return filePath.toString('utf8');
|
return filePath.toString('utf8');
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeError(code, filePath, message) {
|
function makeError(code: string, filePath: ?string, message: string) {
|
||||||
const err: $FlowFixMe = new Error(
|
const err: $FlowFixMe = new Error(
|
||||||
filePath != null
|
filePath != null
|
||||||
? `${code}: \`${filePath}\`: ${message}`
|
? `${code}: \`${filePath}\`: ${message}`
|
||||||
|
@ -900,4 +1036,11 @@ function makeError(code, filePath, message) {
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function nullthrows<T>(x: ?T): T {
|
||||||
|
if (x == null) {
|
||||||
|
throw new Error('item was null or undefined');
|
||||||
|
}
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = MemoryFs;
|
module.exports = MemoryFs;
|
||||||
|
|
Loading…
Reference in New Issue