not finished
This commit is contained in:
185
packages/node-browser/src/child_process.ts
Normal file
185
packages/node-browser/src/child_process.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import * as cp from "child_process";
|
||||
import * as stream from "stream";
|
||||
import * as events from "events";
|
||||
import * as net from "net";
|
||||
import { wush, Session } from "@coder/wush";
|
||||
import { useBuffer, throwUnimplementedError, throwSyncError } from "./util";
|
||||
|
||||
/**
|
||||
* Readable stream.
|
||||
*/
|
||||
class Readable extends stream.Readable {
|
||||
|
||||
/**
|
||||
* Read a chunk.
|
||||
*/
|
||||
public _read(_size: number): void {
|
||||
// There is nothing to actually read.
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of ChildProcess for the browser.
|
||||
*/
|
||||
class ChildProcess extends events.EventEmitter implements cp.ChildProcess {
|
||||
|
||||
public connected: boolean = true;
|
||||
public killed: boolean = false;
|
||||
public pid = 0;
|
||||
public stdin: stream.Writable;
|
||||
public stdout: Readable;
|
||||
public stderr: Readable;
|
||||
public stdio: [stream.Writable, stream.Readable, stream.Readable];
|
||||
|
||||
private emitter = new events.EventEmitter();
|
||||
|
||||
public constructor(private session: Session) {
|
||||
super();
|
||||
|
||||
this.emitter = new events.EventEmitter();
|
||||
this.stdin = new stream.Writable();
|
||||
this.stdin._write = (
|
||||
chunk: any, // tslint:disable-line no-any so we can match the Node API.
|
||||
_encoding: string,
|
||||
callback: (error?: Error) => void,
|
||||
): void => {
|
||||
session.sendStdin(chunk.toString());
|
||||
callback();
|
||||
};
|
||||
this.stdout = new Readable();
|
||||
this.stderr = new Readable();
|
||||
this.stdio = [this.stdin, this.stdout, this.stderr];
|
||||
|
||||
session.onDone((exitCode) => {
|
||||
this.emitter.emit("exit", exitCode);
|
||||
});
|
||||
|
||||
session.onDisconnect(() => {
|
||||
this.emitter.emit("exit", -1);
|
||||
});
|
||||
|
||||
session.onStdout((data) => {
|
||||
this.stdout.emit("data", data);
|
||||
});
|
||||
|
||||
session.onStderr((data) => {
|
||||
this.stderr.emit("data", data);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill the session.
|
||||
*/
|
||||
public kill(): void {
|
||||
this.session.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Not implemented.
|
||||
*/
|
||||
public disconnect(): void {
|
||||
throwUnimplementedError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Not implemented.
|
||||
*/
|
||||
public ref(): void {
|
||||
throwUnimplementedError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Not implemented.
|
||||
*/
|
||||
public unref(): void {
|
||||
throwUnimplementedError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Not implemented.
|
||||
*/
|
||||
public send(
|
||||
_message: any, // tslint:disable-line no-any so we can match the Node API.
|
||||
_sendHandle?: net.Socket | net.Server | ((error: Error) => void),
|
||||
_options?: cp.MessageOptions | ((error: Error) => void),
|
||||
_callback?: (error: Error) => void,
|
||||
): boolean {
|
||||
throw throwUnimplementedError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add event listener.
|
||||
*/
|
||||
public on(
|
||||
eventName: string,
|
||||
callback: (...args: any[]) => void, // tslint:disable-line no-any so we can match the Node API.
|
||||
): this {
|
||||
this.emitter.on(eventName, callback);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// tslint:disable only-arrow-functions
|
||||
function exec(
|
||||
command: string,
|
||||
options?: { encoding?: BufferEncoding | string | "buffer" | null } & cp.ExecOptions | null | ((error: Error | null, stdout: string, stderr: string) => void) | ((error: Error | null, stdout: Buffer, stderr: Buffer) => void),
|
||||
callback?: ((error: Error | null, stdout: string, stderr: string) => void) | ((error: Error | null, stdout: Buffer, stderr: Buffer) => void),
|
||||
): cp.ChildProcess {
|
||||
const process = new ChildProcess(wush.execute({ command }));
|
||||
|
||||
let stdout = "";
|
||||
process.stdout.on("data", (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
let stderr = "";
|
||||
process.stderr.on("data", (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
process.on("exit", (exitCode) => {
|
||||
const error = exitCode !== 0 ? new Error(stderr) : null;
|
||||
if (typeof options === "function") {
|
||||
callback = options;
|
||||
}
|
||||
// @ts-ignore not sure how to make this work.
|
||||
callback(
|
||||
error,
|
||||
useBuffer(options) ? Buffer.from(stdout) : stdout,
|
||||
useBuffer(options) ? Buffer.from(stderr) : stderr,
|
||||
);
|
||||
});
|
||||
|
||||
return process;
|
||||
}
|
||||
|
||||
function fork(modulePath: string): cp.ChildProcess {
|
||||
return new ChildProcess(wush.execute({
|
||||
command: `node ${modulePath}`,
|
||||
}));
|
||||
}
|
||||
|
||||
function spawn(_command: string, _args?: ReadonlyArray<string>, _options?: cp.SpawnOptions): cp.ChildProcess {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
// tslint:enable only-arrow-functions
|
||||
|
||||
// To satisfy the types.
|
||||
// tslint:disable no-any
|
||||
exec.__promisify__ = undefined as any;
|
||||
// tslint:enable no-any
|
||||
|
||||
const exp: typeof cp = {
|
||||
exec,
|
||||
execFile: throwUnimplementedError,
|
||||
execFileSync: throwSyncError,
|
||||
execSync: throwSyncError,
|
||||
fork,
|
||||
spawn,
|
||||
spawnSync: throwSyncError,
|
||||
};
|
||||
|
||||
export = exp;
|
||||
1
packages/node-browser/src/empty.ts
Normal file
1
packages/node-browser/src/empty.ts
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = {};
|
||||
715
packages/node-browser/src/fs.ts
Normal file
715
packages/node-browser/src/fs.ts
Normal file
@@ -0,0 +1,715 @@
|
||||
import { ChildProcess } from "child_process";
|
||||
import * as fs from "fs";
|
||||
import { EventEmitter } from "events";
|
||||
import { promisify } from "util";
|
||||
import { Writable } from "stream";
|
||||
import { exec } from "./child_process";
|
||||
import {
|
||||
bashCommand, throwUnimplementedError, throwSyncError, escapePath,
|
||||
useBuffer, NewlineInputBuffer, Queue,
|
||||
} from "./util";
|
||||
|
||||
/**
|
||||
* An open file.
|
||||
*/
|
||||
interface IOpenFile {
|
||||
readonly path: fs.PathLike;
|
||||
position: number | undefined;
|
||||
}
|
||||
|
||||
type ReaddirCallback = (error?: NodeJS.ErrnoException, files?: string[]) => void;
|
||||
|
||||
/**
|
||||
* Queue for readdir.
|
||||
*/
|
||||
class ReaddirQueue extends Queue<ReaddirCallback> {
|
||||
|
||||
public async run(items: Map<string, ReaddirCallback[]>): Promise<void> {
|
||||
const keys = Array.from(items.keys());
|
||||
try {
|
||||
const stdio = await promisify(exec)(`bash -c '${keys.map((key) => `cd ${escapePath(key)} && ls -1a; echo;`).join(" ")}'`);
|
||||
stdio.stdout.trim().split("\n\n").forEach((split, index) => {
|
||||
const path = keys[index];
|
||||
const cbs = items.get(path);
|
||||
if (split.indexOf("does not exist") !== -1) {
|
||||
cbs.forEach((cb) => {
|
||||
cb({
|
||||
code: "ENOENT",
|
||||
message: "No such file or directory " + path,
|
||||
name: "Not found",
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const files = split.trim().split("\n");
|
||||
cbs.forEach((cb) => {
|
||||
cb(undefined, files.filter((f) => f !== "." && f !== ".."));
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
items.forEach((cbs) => cbs.forEach((cb) => cb(new Error("failed to ls"))));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type StatCallback = (error?: NodeJS.ErrnoException, stats?: fs.Stats) => void;
|
||||
|
||||
/**
|
||||
* Queue for stat.
|
||||
*/
|
||||
class StatQueue extends Queue<StatCallback> {
|
||||
|
||||
public constructor() {
|
||||
super(100);
|
||||
}
|
||||
|
||||
public async run(items: Map<string, StatCallback[]>): Promise<void> {
|
||||
try {
|
||||
const stats = await this.stat(Array.from(items.keys()));
|
||||
items.forEach((callbacks, path) => {
|
||||
if (stats.has(path)) {
|
||||
callbacks.forEach((cb) => {
|
||||
cb(undefined, stats.get(path));
|
||||
});
|
||||
} else {
|
||||
callbacks.forEach((cb) => {
|
||||
cb({
|
||||
code: "ENOENT",
|
||||
message: "No such file or directory " + path,
|
||||
name: "Not found",
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
items.forEach((callbacks) => {
|
||||
callbacks.forEach((cb) => {
|
||||
cb({
|
||||
code: "ECMDFAIL",
|
||||
message: "failed to stat",
|
||||
name: "failed to stat",
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform stat on multiple paths. Invalid files are ignored.
|
||||
*/
|
||||
private async stat(paths: string[]): Promise<Map<string, fs.Stats>> {
|
||||
const map = new Map<string, fs.Stats>();
|
||||
const pathsStr = paths.map(escapePath).join(" ");
|
||||
const resp = await promisify(exec)(
|
||||
`bash -c "stat ${pathsStr} -c \\\\'%n\\\\',%s,%F,%Y,%a,%g,%u,%X,%W,%d,%i,%b,%B,%Z,%h,%t"`,
|
||||
);
|
||||
resp.stdout.split("\n").forEach((stat) => {
|
||||
const matches = stat.trim().match(/(^'.*'|[^',\s]+)(?=\s*,|\s*$)/g);
|
||||
if (!matches || matches.length < 16) {
|
||||
return;
|
||||
}
|
||||
|
||||
const name = matches[0].substring(1, matches[0].length -1);
|
||||
const size = parseInt(matches[1], 10);
|
||||
const fileType = matches[2];
|
||||
const mtime = new Date(parseInt(matches[3], 10) * 1000);
|
||||
const mode = parseInt(matches[4], 10);
|
||||
const gid = parseInt(matches[5], 10);
|
||||
const uid = parseInt(matches[6], 10);
|
||||
const atime = new Date(parseInt(matches[7], 10) * 1000);
|
||||
const birthtime = new Date(parseInt(matches[8], 10) * 1000);
|
||||
const dev = parseInt(matches[9], 10);
|
||||
const ino = parseInt(matches[10], 10);
|
||||
const blocks = parseInt(matches[11], 10);
|
||||
const blksize = parseInt(matches[12], 10);
|
||||
const ctime = new Date(parseInt(matches[13], 10) * 1000);
|
||||
const nlink = parseInt(matches[14], 10);
|
||||
const rdev = parseInt(matches[15], 10);
|
||||
|
||||
map.set(name, {
|
||||
atime: atime,
|
||||
atimeMs: atime.getTime(),
|
||||
birthtime,
|
||||
birthtimeMs: birthtime.getTime(),
|
||||
blksize,
|
||||
blocks,
|
||||
ctime,
|
||||
ctimeMs: ctime.getTime(),
|
||||
dev,
|
||||
gid,
|
||||
ino,
|
||||
isBlockDevice: (): boolean => fileType === "block special file",
|
||||
isCharacterDevice: (): boolean => fileType === "character special file",
|
||||
isDirectory: (): boolean => fileType === "directory",
|
||||
isFIFO: (): boolean => fileType === "fifo",
|
||||
isFile: (): boolean => fileType === "regular file",
|
||||
isSocket: (): boolean => fileType === "socket",
|
||||
isSymbolicLink: (): boolean => fileType === "symbolic link",
|
||||
mode,
|
||||
mtime,
|
||||
mtimeMs: mtime.getTime(),
|
||||
nlink,
|
||||
rdev,
|
||||
size,
|
||||
uid,
|
||||
});
|
||||
});
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type ReadFileCallback = (err: NodeJS.ErrnoException, content: string) => void;
|
||||
|
||||
/**
|
||||
* Queue for readFile.
|
||||
*/
|
||||
class ReadFileQueue extends Queue<ReadFileCallback> {
|
||||
|
||||
public constructor() {
|
||||
super(100);
|
||||
}
|
||||
|
||||
public async run(items: Map<string, ReadFileCallback[]>): Promise<void> {
|
||||
try {
|
||||
throwUnimplementedError();
|
||||
} catch (error) {
|
||||
items.forEach((cbs) => cbs.forEach((cb) => cb(error, "")));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Watcher extends EventEmitter implements fs.FSWatcher {
|
||||
|
||||
public constructor(private readonly process: ChildProcess) {
|
||||
super();
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this.process.kill();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class WriteStream extends Writable implements fs.WriteStream {
|
||||
|
||||
public path: string;
|
||||
|
||||
private process: ChildProcess;
|
||||
|
||||
public constructor(path: fs.PathLike) {
|
||||
super();
|
||||
|
||||
this.path = path.toString();
|
||||
this.process = exec(`cat > ${escapePath(this.path)}`);
|
||||
setTimeout(() => {
|
||||
// Set timeout so listeners have time to register.
|
||||
this.emit("open");
|
||||
}, 0);
|
||||
this.process.on("exit", () => {
|
||||
this.emit("close");
|
||||
});
|
||||
}
|
||||
|
||||
public get bytesWritten(): number {
|
||||
throw throwUnimplementedError();
|
||||
}
|
||||
|
||||
// tslint:disable-next-line no-any
|
||||
public _write(chunk: any, _encoding: string, callback: () => void): void {
|
||||
this.process.stdin.write(chunk);
|
||||
callback();
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this.process.kill();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Used to identify files by descriptor.
|
||||
let lastFileDescriptor = 0;
|
||||
const readdirQueue = new ReaddirQueue();
|
||||
const readFileQueue = new ReadFileQueue();
|
||||
const statQueue = new StatQueue();
|
||||
const openFiles = new Map<number, IOpenFile>();
|
||||
|
||||
// tslint:disable only-arrow-functions
|
||||
// A common pattern is to exec and call the callback with an error or null.
|
||||
function execAndCallback(command: string, callback: (err: NodeJS.ErrnoException) => void): void {
|
||||
promisify(exec)(command).then(() => {
|
||||
callback(null as any); // tslint:disable-line no-any
|
||||
}).catch((error) => {
|
||||
callback(error);
|
||||
});
|
||||
}
|
||||
|
||||
function appendFile(
|
||||
path: fs.PathLike | number,
|
||||
data: any, // tslint:disable-line no-any
|
||||
options?: { encoding?: string | null; mode?: string | number; flag?: string; } | string | undefined | null | ((err: NodeJS.ErrnoException) => void),
|
||||
callback?: (err: NodeJS.ErrnoException) => void,
|
||||
): void {
|
||||
if (typeof options === "function") {
|
||||
callback = options;
|
||||
}
|
||||
|
||||
if (typeof path === "number") {
|
||||
if (!openFiles.has(path)) {
|
||||
// @ts-ignore not sure how to make this work.
|
||||
return callback(new Error("not open"), undefined as any); // tslint:disable-line no-any
|
||||
}
|
||||
path = openFiles.get(path).path;
|
||||
}
|
||||
|
||||
const process = exec(`${data ? "cat >>" : "touch"} ${escapePath(path.toString())}`, (error) => {
|
||||
callback!(error as any); // tslint:disable-line no-any
|
||||
});
|
||||
if (data) {
|
||||
process.stdin.write(data);
|
||||
}
|
||||
}
|
||||
|
||||
function close(fd: number, callback: (err: NodeJS.ErrnoException) => void): void {
|
||||
if (!openFiles.has(fd)) {
|
||||
return callback(new Error("file wasnt open"));
|
||||
}
|
||||
|
||||
openFiles.delete(fd);
|
||||
callback(null as any); // tslint:disable-line no-any
|
||||
}
|
||||
|
||||
function createWriteStream(path: fs.PathLike, _options?: string | {
|
||||
flags?: string;
|
||||
encoding?: string;
|
||||
fd?: number;
|
||||
mode?: number;
|
||||
}): fs.WriteStream {
|
||||
return new WriteStream(path);
|
||||
}
|
||||
|
||||
function exists(path: fs.PathLike, callback: (exists: boolean) => void): void {
|
||||
const pathStr = escapePath(path.toString());
|
||||
const command = bashCommand(
|
||||
`if [ -d ${pathStr} ]; then echo true;`
|
||||
+ ` elif [ -f ${pathStr} ]; then echo true;`
|
||||
+ ` elif [ -s ${pathStr} ]; then echo true;`
|
||||
+ "fi",
|
||||
);
|
||||
promisify(exec)(command).then((stdio) => {
|
||||
callback(stdio.stdout.trim() === "true");
|
||||
}).catch(() => {
|
||||
callback(false);
|
||||
});
|
||||
}
|
||||
|
||||
function fstat(fd: number, callback: (err: NodeJS.ErrnoException, stats: fs.Stats) => void): void {
|
||||
if (!openFiles.has(fd)) {
|
||||
return callback(new Error("not open"), null as any); // tslint:disable-line no-any
|
||||
}
|
||||
stat(openFiles.get(fd).path, callback);
|
||||
}
|
||||
|
||||
function futimes(
|
||||
fd: number,
|
||||
atime: string | number | Date,
|
||||
mtime: string | number | Date,
|
||||
callback: (err: NodeJS.ErrnoException) => void,
|
||||
): void {
|
||||
if (!openFiles.has(fd)) {
|
||||
return callback(new Error("not opened"));
|
||||
}
|
||||
|
||||
const openFile = openFiles.get(fd);
|
||||
const command = [
|
||||
{ flag: "a", time: atime },
|
||||
{ flag: "m", time: mtime },
|
||||
]
|
||||
.filter((item) => !!item.time)
|
||||
.map((item) => `touch -${item.flag} --date="${item.time}" ${escapePath(openFile.path.toString())}`)
|
||||
.join(";");
|
||||
|
||||
if (command.length === 0) {
|
||||
return callback(new Error("atime or mtime required"));
|
||||
}
|
||||
|
||||
execAndCallback(command, callback);
|
||||
}
|
||||
|
||||
function lstat(path: fs.PathLike, callback: (err: NodeJS.ErrnoException, stats: fs.Stats) => void): void {
|
||||
stat(path, callback);
|
||||
}
|
||||
|
||||
function mkdir(
|
||||
path: fs.PathLike, mode: number | string | undefined | null | ((err: NodeJS.ErrnoException) => void),
|
||||
callback?: (err: NodeJS.ErrnoException) => void,
|
||||
): void {
|
||||
execAndCallback(
|
||||
`mkdir -p ${escapePath(path.toString())}`,
|
||||
typeof mode === "function" ? mode : callback!,
|
||||
);
|
||||
}
|
||||
|
||||
function open(
|
||||
path: fs.PathLike,
|
||||
flags: string | number, mode: number | string | undefined | null | ((err: NodeJS.ErrnoException, fd: number) => void),
|
||||
callback?: (err: NodeJS.ErrnoException, fd: number) => void,
|
||||
): void {
|
||||
if (typeof mode === "function") {
|
||||
callback = mode;
|
||||
}
|
||||
|
||||
// Don't touch if read-only.
|
||||
const promise = flags !== "r"
|
||||
? promisify(exec)(`touch ${escapePath(path.toString())}`)
|
||||
.then(() => Promise.resolve())
|
||||
.catch((error) => {
|
||||
if (error.message.indexOf("No such file or directory") !== -1) {
|
||||
return Promise.reject({
|
||||
code: "ENOENT",
|
||||
message: "No such file or directory " + path,
|
||||
name: "Not found",
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
})
|
||||
: Promise.resolve();
|
||||
|
||||
promise.then(() => {
|
||||
const id = lastFileDescriptor++;
|
||||
openFiles.set(id, {
|
||||
path,
|
||||
position: undefined,
|
||||
});
|
||||
callback!(null as any, id); // tslint:disable-line no-any
|
||||
}).catch((error) => {
|
||||
callback!(error, undefined as any); // tslint:disable-line no-any
|
||||
});
|
||||
}
|
||||
|
||||
function read<TBuffer extends Buffer | Uint8Array>(
|
||||
fd: number,
|
||||
buffer: TBuffer,
|
||||
offset: number,
|
||||
length: number,
|
||||
position: number | null,
|
||||
callback?: (err: NodeJS.ErrnoException, bytesRead: number, buffer: TBuffer) => void,
|
||||
): void {
|
||||
if (!openFiles.has(fd)) {
|
||||
if (callback) {
|
||||
// tslint:disable-next-line no-any
|
||||
callback(new Error("not opened"), undefined as any, undefined as any);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const hasPosition = typeof position === "number";
|
||||
const openFile = openFiles.get(fd);
|
||||
|
||||
if (!hasPosition) {
|
||||
position = openFile.position || 0;
|
||||
}
|
||||
|
||||
readFile(openFile.path, (error, data) => {
|
||||
if (error) {
|
||||
if (callback) {
|
||||
// tslint:disable-next-line no-any
|
||||
callback(error, undefined as any, undefined as any);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const output = data.slice(position!, position! + length);
|
||||
if (output.length !== 0) {
|
||||
buffer.set(output, offset);
|
||||
}
|
||||
|
||||
if (!hasPosition) {
|
||||
if (typeof openFile.position !== "undefined") {
|
||||
openFile.position += output.length;
|
||||
} else {
|
||||
openFile.position = output.length;
|
||||
}
|
||||
openFiles.set(fd, openFile);
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
callback(null as any, output.length, buffer); // tslint:disable-line no-any
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function readFile(
|
||||
path: fs.PathLike | number,
|
||||
options: { encoding?: string | null; flag?: string; } | string | undefined | null | ((err: NodeJS.ErrnoException, data: Buffer) => void),
|
||||
callback?: ((err: NodeJS.ErrnoException, data: Buffer | string) => void) | ((err: NodeJS.ErrnoException, data: Buffer) => void) | ((err: NodeJS.ErrnoException, data: string) => void),
|
||||
): void {
|
||||
if (typeof options === "function") {
|
||||
callback = options;
|
||||
}
|
||||
|
||||
if (typeof path === "number") {
|
||||
if (!openFiles.has(path)) {
|
||||
// @ts-ignore not sure how to make this work.
|
||||
return callback(new Error("not open"), undefined as any); // tslint:disable-line no-any
|
||||
}
|
||||
path = openFiles.get(path).path;
|
||||
}
|
||||
|
||||
readFileQueue.add(path.toString(), (error, result) => {
|
||||
// @ts-ignore not sure how to make this work.
|
||||
callback(
|
||||
error,
|
||||
result && useBuffer(options) ? Buffer.from(result) : result,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function readdir(
|
||||
path: fs.PathLike,
|
||||
options: { encoding?: string | null } | string | undefined | null | ((err: NodeJS.ErrnoException, files: string[]) => void),
|
||||
callback?: ((err: NodeJS.ErrnoException, files: string[]) => void) | ((err: NodeJS.ErrnoException, files: Buffer[]) => void) | ((err: NodeJS.ErrnoException, files: Array<string | Buffer>) => void),
|
||||
): void {
|
||||
if (typeof options === "function") {
|
||||
callback = options;
|
||||
}
|
||||
readdirQueue.add(path.toString(), (error, files) => {
|
||||
if (typeof options === "function") {
|
||||
callback = options;
|
||||
}
|
||||
// @ts-ignore not sure how to make this work.
|
||||
callback(
|
||||
error,
|
||||
files && useBuffer(options)
|
||||
? files.map((f) => Buffer.from(f))
|
||||
: files,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function realpath(
|
||||
path: fs.PathLike,
|
||||
options: { encoding?: string | null } | string | undefined | null | ((err: NodeJS.ErrnoException, resolvedPath: string) => void),
|
||||
callback?: ((err: NodeJS.ErrnoException, resolvedPath: string) => void) | ((err: NodeJS.ErrnoException, resolvedPath: Buffer) => void) | ((err: NodeJS.ErrnoException, resolvedPath: string | Buffer) => void),
|
||||
): void {
|
||||
if (typeof options === "function") {
|
||||
callback = options;
|
||||
}
|
||||
// @ts-ignore not sure how to make this work.
|
||||
callback(
|
||||
null,
|
||||
useBuffer(options) ? Buffer.from(path.toString()) : path.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
function rename(oldPath: fs.PathLike, newPath: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void {
|
||||
promisify(exec)(`mv ${escapePath(oldPath.toString())} ${escapePath(newPath.toString())}`).then(() => {
|
||||
callback(null as any); // tslint:disable-line no-any
|
||||
}).catch((error) => {
|
||||
callback(error.message.indexOf("No such file or directory") !== -1 ? {
|
||||
code: "ENOENT",
|
||||
message: "No such file or directory " + oldPath,
|
||||
name: "Not found",
|
||||
} : error);
|
||||
});
|
||||
}
|
||||
|
||||
function rmdir(path: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void {
|
||||
execAndCallback(`rmdir ${escapePath(path.toString())}`, callback);
|
||||
}
|
||||
|
||||
function stat(path: fs.PathLike, callback: (err: NodeJS.ErrnoException, stats: fs.Stats) => void): void {
|
||||
statQueue.add(path.toString(), (error, stats) => {
|
||||
callback(error as any, stats as any); // tslint:disable-line no-any
|
||||
});
|
||||
}
|
||||
|
||||
function unlink(path: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void {
|
||||
execAndCallback(`unlink ${escapePath(path.toString())}`, callback);
|
||||
}
|
||||
|
||||
function watch(
|
||||
filename: fs.PathLike,
|
||||
options: { encoding?: string | null, persistent?: boolean, recursive?: boolean } | string | undefined | null | ((event: string, filename: string) => void),
|
||||
listener?: ((event: string, filename: string) => void) | ((event: string, filename: Buffer) => void),
|
||||
): fs.FSWatcher {
|
||||
const buffer = new NewlineInputBuffer((msg): void => {
|
||||
msg = msg.trim();
|
||||
const index = msg.lastIndexOf(":");
|
||||
const events = msg.substring(index + 1).split(",");
|
||||
const baseFilename = msg.substring(0, index).split("/").pop();
|
||||
events.forEach((event) => {
|
||||
switch (event) {
|
||||
// Rename is emitted when a file appears or disappears in the directory.
|
||||
case "CREATE":
|
||||
case "DELETE":
|
||||
case "MOVED_FROM":
|
||||
case "MOVED_TO":
|
||||
watcher.emit("rename", baseFilename);
|
||||
break;
|
||||
case "CLOSE_WRITE":
|
||||
watcher.emit("change", baseFilename);
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const process = exec(`inotifywait ${escapePath(filename.toString())} -m --format "%w%f:%e"`);
|
||||
process.on("exit", (exitCode) => {
|
||||
watcher.emit("error", new Error(`process terminated unexpectedly with code ${exitCode}`));
|
||||
});
|
||||
process.stdout.on("data", (data) => {
|
||||
buffer.push(data);
|
||||
});
|
||||
|
||||
const watcher = new Watcher(process);
|
||||
watcher.on("change", (filename) => {
|
||||
// @ts-ignore not sure how to make this work.
|
||||
listener("change", useBuffer(options) ? Buffer.from(filename) : filename);
|
||||
});
|
||||
watcher.on("rename", (filename) => {
|
||||
// @ts-ignore not sure how to make this work.
|
||||
listener("rename", useBuffer(options) ? Buffer.from(filename) : filename);
|
||||
});
|
||||
|
||||
return watcher;
|
||||
}
|
||||
|
||||
function writeFile(
|
||||
path: fs.PathLike | number,
|
||||
data: any, // tslint:disable-line no-any
|
||||
options: { encoding?: string | null; mode?: number | string; flag?: string; } | string | undefined | null | ((err: NodeJS.ErrnoException) => void),
|
||||
callback?: (err: NodeJS.ErrnoException) => void,
|
||||
): void {
|
||||
if (typeof options === "function") {
|
||||
callback = options;
|
||||
}
|
||||
|
||||
if (typeof path === "number") {
|
||||
if (!openFiles.has(path)) {
|
||||
// @ts-ignore not sure how to make this work.
|
||||
return callback(new Error("not open"), undefined as any); // tslint:disable-line no-any
|
||||
}
|
||||
path = openFiles.get(path).path;
|
||||
}
|
||||
|
||||
const process = exec(`${data ? "cat >" : "touch"} ${escapePath(path.toString())}`, (error) => {
|
||||
callback!(error as any); // tslint:disable-line no-any
|
||||
});
|
||||
if (data) {
|
||||
process.stdin.write(data);
|
||||
}
|
||||
}
|
||||
// tslint:enable only-arrow-functions
|
||||
|
||||
// Just to satisfy the types.
|
||||
// tslint:disable no-any
|
||||
appendFile.__promisify__ = undefined as any;
|
||||
close.__promisify__ = undefined as any;
|
||||
exists.__promisify__ = undefined as any;
|
||||
fstat.__promisify__ = undefined as any;
|
||||
futimes.__promisify__ = undefined as any;
|
||||
lstat.__promisify__ = undefined as any;
|
||||
mkdir.__promisify__ = undefined as any;
|
||||
open.__promisify__ = undefined as any;
|
||||
read.__promisify__ = undefined as any;
|
||||
readFile.__promisify__ = undefined as any;
|
||||
readdir.__promisify__ = undefined as any;
|
||||
realpath.__promisify__ = undefined as any;
|
||||
rename.__promisify__ = undefined as any;
|
||||
rmdir.__promisify__ = undefined as any;
|
||||
stat.__promisify__ = undefined as any;
|
||||
unlink.__promisify__ = undefined as any;
|
||||
writeFile.__promisify__ = undefined as any;
|
||||
// tslint:enable no-any
|
||||
|
||||
const exp: typeof fs = {
|
||||
constants: fs.constants,
|
||||
Stats: fs.Stats,
|
||||
ReadStream: fs.ReadStream,
|
||||
WriteStream: fs.WriteStream,
|
||||
|
||||
access: throwUnimplementedError,
|
||||
accessSync: throwSyncError,
|
||||
appendFile,
|
||||
appendFileSync: throwSyncError,
|
||||
chmod: throwUnimplementedError,
|
||||
chmodSync: throwSyncError,
|
||||
chown: throwUnimplementedError,
|
||||
chownSync: throwSyncError,
|
||||
close,
|
||||
copyFile: throwUnimplementedError,
|
||||
copyFileSync: throwSyncError,
|
||||
closeSync: throwSyncError,
|
||||
createReadStream: throwUnimplementedError,
|
||||
createWriteStream,
|
||||
exists,
|
||||
existsSync: throwSyncError,
|
||||
fchmod: throwUnimplementedError,
|
||||
fchmodSync: throwSyncError,
|
||||
fchown: throwUnimplementedError,
|
||||
fchownSync: throwSyncError,
|
||||
fdatasync: throwUnimplementedError,
|
||||
fdatasyncSync: throwSyncError,
|
||||
fstat,
|
||||
fstatSync: throwSyncError,
|
||||
fsync: throwUnimplementedError,
|
||||
fsyncSync: throwSyncError,
|
||||
ftruncate: throwUnimplementedError,
|
||||
ftruncateSync: throwSyncError,
|
||||
futimes,
|
||||
futimesSync: throwSyncError,
|
||||
lchmod: throwUnimplementedError,
|
||||
lchmodSync: throwSyncError,
|
||||
lchown: throwUnimplementedError,
|
||||
lchownSync: throwSyncError,
|
||||
link: throwUnimplementedError,
|
||||
linkSync: throwSyncError,
|
||||
lstat,
|
||||
lstatSync: throwSyncError,
|
||||
mkdir,
|
||||
mkdirSync: throwSyncError,
|
||||
mkdtemp: throwUnimplementedError,
|
||||
mkdtempSync: throwSyncError,
|
||||
open,
|
||||
openSync: throwSyncError,
|
||||
read,
|
||||
readFile,
|
||||
readFileSync: throwSyncError,
|
||||
readSync: throwSyncError,
|
||||
readdir,
|
||||
readdirSync: throwSyncError,
|
||||
readlink: throwUnimplementedError,
|
||||
readlinkSync: throwSyncError,
|
||||
realpath,
|
||||
realpathSync: throwSyncError,
|
||||
rename,
|
||||
renameSync: throwSyncError,
|
||||
rmdir,
|
||||
rmdirSync: throwSyncError,
|
||||
stat,
|
||||
statSync: throwSyncError,
|
||||
symlink: throwUnimplementedError,
|
||||
symlinkSync: throwSyncError,
|
||||
truncate: throwUnimplementedError,
|
||||
truncateSync: throwSyncError,
|
||||
unlink,
|
||||
unlinkSync: throwSyncError,
|
||||
unwatchFile: throwUnimplementedError,
|
||||
utimes: throwUnimplementedError,
|
||||
utimesSync: throwSyncError,
|
||||
watch,
|
||||
watchFile: throwUnimplementedError,
|
||||
write: throwUnimplementedError,
|
||||
writeFile,
|
||||
writeFileSync: throwSyncError,
|
||||
writeSync: throwSyncError,
|
||||
};
|
||||
|
||||
export = exp;
|
||||
2
packages/node-browser/src/index.ts
Normal file
2
packages/node-browser/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import { bashCommand, escapePath, isBrowserEnvironment } from "./util";
|
||||
export { bashCommand, escapePath, isBrowserEnvironment };
|
||||
70
packages/node-browser/src/net.ts
Normal file
70
packages/node-browser/src/net.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import * as net from "net";
|
||||
|
||||
/**
|
||||
* Implementation of Socket for the browser.
|
||||
*/
|
||||
class Socket extends net.Socket {
|
||||
|
||||
public connect(): this {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of Server for the browser.
|
||||
*/
|
||||
class Server extends net.Server {
|
||||
|
||||
public listen(
|
||||
_port?: number | any | net.ListenOptions, // tslint:disable-line no-any so we can match the Node API.
|
||||
_hostname?: string | number | Function,
|
||||
_backlog?: number | Function,
|
||||
_listeningListener?: Function,
|
||||
): this {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// tslint:disable only-arrow-functions
|
||||
function connect(): net.Socket {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
function createConnection(): net.Socket {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
function isIP(_input: string): number {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
function isIPv4(_input: string): boolean {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
function isIPv6(_input: string): boolean {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
function createServer(
|
||||
_options?: { allowHalfOpen?: boolean, pauseOnConnect?: boolean } | ((socket: net.Socket) => void),
|
||||
_connectionListener?: (socket: net.Socket) => void,
|
||||
): Server {
|
||||
return new Server();
|
||||
}
|
||||
// tslint:enable only-arrow-functions
|
||||
|
||||
const exp: typeof net = {
|
||||
Socket,
|
||||
Server,
|
||||
connect,
|
||||
createConnection,
|
||||
isIP,
|
||||
isIPv4,
|
||||
isIPv6,
|
||||
createServer,
|
||||
};
|
||||
|
||||
export = exp;
|
||||
137
packages/node-browser/src/util.ts
Normal file
137
packages/node-browser/src/util.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
// The type doesn't matter for these since we're just throwing.
|
||||
// tslint:disable no-any
|
||||
export const throwUnimplementedError = (): any => {
|
||||
throw new Error("not implemented");
|
||||
};
|
||||
// In case the types except the promisify property.
|
||||
throwUnimplementedError.__promisify__ = undefined as any;
|
||||
// This one seems to be a mistake in the types for `link`.
|
||||
throwUnimplementedError.link = undefined as any;
|
||||
export const throwSyncError = (): any => {
|
||||
throw new Error("sync is not supported");
|
||||
};
|
||||
// tslint:enable no-any
|
||||
|
||||
/**
|
||||
* Return true if the options specify to use a Buffer instead of string.
|
||||
*/
|
||||
export const useBuffer = (options: { encoding?: string | null } | string | undefined | null | Function): boolean => {
|
||||
return options === "buffer"
|
||||
|| (!!options && typeof options !== "string" && typeof options !== "function"
|
||||
&& (options.encoding === "buffer" || options.encoding === null));
|
||||
};
|
||||
|
||||
/**
|
||||
* Run a command with bash.
|
||||
*/
|
||||
export const bashCommand = (command: string): string => {
|
||||
return `bash -c "${command.replace(/"/g, "\\\"")}"`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return true if we're in a browser environment (including web workers).
|
||||
*/
|
||||
export const isBrowserEnvironment = (): boolean => {
|
||||
return typeof process === "undefined" || typeof process.stdout === "undefined";
|
||||
};
|
||||
|
||||
/**
|
||||
* Escape a path. This prevents any issues with file names that have quotes,
|
||||
* spaces, braces, etc.
|
||||
*/
|
||||
export const escapePath = (path: string): string => {
|
||||
return `'${path.replace(/'/g, "'\\''")}'`;
|
||||
};
|
||||
|
||||
/**
|
||||
* This queues up items then runs on all the items at once after a timeout. Each
|
||||
* item has a callback that expects the response for that item which is the
|
||||
* extending class's responsibility to call.
|
||||
*
|
||||
* You can also specify a maximum number of items to keep in the queue.
|
||||
*/
|
||||
export abstract class Queue<T> {
|
||||
|
||||
private items: Map<string, T[]>;
|
||||
private timeout: number | NodeJS.Timer | undefined;
|
||||
private max: number | undefined;
|
||||
private timeoutDelay = 1;
|
||||
|
||||
public constructor(max?: number) {
|
||||
this.items = new Map();
|
||||
this.run = run;
|
||||
this.max = max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an item to the queue.
|
||||
*/
|
||||
public add(key: string, callback: T): void {
|
||||
if (this.items.has(key)) {
|
||||
this.items.get(key)!.push(callback);
|
||||
} else {
|
||||
this.items.set(key, [callback]);
|
||||
}
|
||||
|
||||
const run = (): void => {
|
||||
// tslint:disable-next-line no-any because NodeJS.Timer is valid.
|
||||
clearTimeout(this.timeout as any);
|
||||
this.timeout = undefined;
|
||||
const newMap = new Map(this.items);
|
||||
this.items.clear();
|
||||
this.run(newMap);
|
||||
};
|
||||
|
||||
if (typeof this.max !== "undefined" && this.items.size >= this.max) {
|
||||
return run();
|
||||
}
|
||||
|
||||
if (typeof this.timeout === "undefined") {
|
||||
this.timeout = setTimeout(() => {
|
||||
run();
|
||||
}, this.timeoutDelay);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run on the specified items then call their callbacks.
|
||||
*/
|
||||
protected abstract run(items: Map<string, T[]>): void;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for safely taking input and turning it into separate messages.
|
||||
* Assumes that messages are split by newlines.
|
||||
*/
|
||||
export class NewlineInputBuffer {
|
||||
|
||||
private callback: (msg: string) => void;
|
||||
private buffer: string | undefined;
|
||||
|
||||
public constructor(callback: (msg: string) => void) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add data to be buffered.
|
||||
*/
|
||||
public push(data: string | Uint8Array): void {
|
||||
let input = typeof data === "string" ? data : data.toString();
|
||||
if (this.buffer) {
|
||||
input = this.buffer + input;
|
||||
this.buffer = undefined;
|
||||
}
|
||||
const lines = input.split("\n");
|
||||
const length = lines.length - 1;
|
||||
const lastLine = lines[length];
|
||||
if (lastLine.length > 0) {
|
||||
this.buffer = lastLine;
|
||||
}
|
||||
lines.pop(); // This is either the line we buffered or an empty string.
|
||||
for (let i = 0; i < length; ++i) {
|
||||
this.callback(lines[i]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user