Initial version
This commit is contained in:
commit
377567bde0
7
.npmignore
Normal file
7
.npmignore
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# Don't publish the src containing ESNext proposal's code. Only publish the bundled output in dist/ and the ES6-transpiled src from lib/
|
||||||
|
.idea/*
|
||||||
|
tests/*
|
||||||
|
node_modules/*
|
||||||
|
rollup.config.js
|
||||||
|
yarn.lock
|
||||||
|
yarn-error.log
|
||||||
60
package.json
Normal file
60
package.json
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
"name": "@cerxes/host",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"author": "Miel Truyen <miel.truyen@cerxes.net>",
|
||||||
|
"description": "A JS-interface to the host-machine. Provides functions for common file-system tasks during build, watch or deployment such as cleaning a dist dir, copying assets etc.",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://git.cerxes.net/cerxes/host.git"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/types": "latest",
|
||||||
|
"@babel/register": "latest",
|
||||||
|
"@babel/cli": "latest",
|
||||||
|
"@babel/core": "latest",
|
||||||
|
"@babel/plugin-proposal-class-properties": "latest",
|
||||||
|
"@babel/plugin-proposal-decorators": "latest",
|
||||||
|
"@babel/plugin-proposal-export-default-from": "latest",
|
||||||
|
"@babel/plugin-proposal-export-namespace-from": "latest",
|
||||||
|
"@babel/plugin-proposal-nullish-coalescing-operator": "latest",
|
||||||
|
"@babel/plugin-proposal-optional-chaining": "latest",
|
||||||
|
"@babel/plugin-proposal-private-methods": "latest",
|
||||||
|
"@babel/preset-env": "latest",
|
||||||
|
"rollup": "latest",
|
||||||
|
"rollup-plugin-babel": "latest",
|
||||||
|
"rollup-plugin-sourcemaps": "latest",
|
||||||
|
"rollup-plugin-node-resolve": "latest",
|
||||||
|
"rollup-plugin-commonjs": "latest",
|
||||||
|
"rollup-plugin-string": "latest",
|
||||||
|
"rollup-plugin-terser": "latest",
|
||||||
|
"rollup-plugin-json": "latest",
|
||||||
|
"builtin-modules": "latest",
|
||||||
|
"jsdoc": "latest",
|
||||||
|
"jest": "latest",
|
||||||
|
"npm-run-all": "latest",
|
||||||
|
"@types/node": "^12.12.12",
|
||||||
|
"@types/zen-observable": "^0.8.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-up": "^1.0.0",
|
||||||
|
"json5": "^2.1.1",
|
||||||
|
"minimatch": "^3.0.4",
|
||||||
|
"zen-observable": "latest"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "npm-run-all -p build-cjs build-es6",
|
||||||
|
"watch": "npm-run-all -p watch-cjs watch-es6",
|
||||||
|
"build-cjs": "rollup -c",
|
||||||
|
"watch-cjs": "rollup -c -w",
|
||||||
|
"build-es6": "npx babel ./src --out-dir=lib",
|
||||||
|
"watch-es6": "npx babel ./src --out-dir=lib -w",
|
||||||
|
"npm-publish": "npm run build && npm publish --registry https://npm.cerxes.net --tag latest",
|
||||||
|
"test": "jest --detectOpenHandles",
|
||||||
|
"test-manual-watch": "cd tests/manual && node -r @babel/register watch.dev.js",
|
||||||
|
"test-manual-pattern": "cd tests/manual && node -r @babel/register watch-glob.dev.js",
|
||||||
|
"test-manual-sync": "cd tests/manual && node -r @babel/register watch-sync.dev.js",
|
||||||
|
"test-manual-processArgs": "cd tests/manual && node -r @babel/register process-args.js"
|
||||||
|
},
|
||||||
|
"module": "./lib/index.js",
|
||||||
|
"main": "./dist/index.js"
|
||||||
|
}
|
||||||
37
rollup.config.js
Normal file
37
rollup.config.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import babel from 'rollup-plugin-babel';
|
||||||
|
import resolve from 'rollup-plugin-node-resolve';
|
||||||
|
import commonjs from 'rollup-plugin-commonjs';
|
||||||
|
import { terser } from 'rollup-plugin-terser';
|
||||||
|
import json from "rollup-plugin-json";
|
||||||
|
// `npm run build` -> `production` is true
|
||||||
|
// `npm run dev` -> `production` is false
|
||||||
|
const production = !process.env.ROLLUP_WATCH;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
input: 'src/index.js',
|
||||||
|
output: {
|
||||||
|
file: 'dist/index.js',
|
||||||
|
format: 'cjs', // common-js bundle as would be expected for node
|
||||||
|
sourcemap: true
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
// Add json support (sadly in rollup we have to do this explicity, despite the NodeJS algorithm supporitng this by defulat
|
||||||
|
json(),
|
||||||
|
// babel
|
||||||
|
babel(),
|
||||||
|
// node_modules
|
||||||
|
resolve({
|
||||||
|
preferBuiltins: true,
|
||||||
|
extensions: ['.mjs', '.js', '.jsx', '.json'],
|
||||||
|
}),
|
||||||
|
// CJS-modules
|
||||||
|
commonjs({
|
||||||
|
'minimatch': ['Minimatch'],
|
||||||
|
}),
|
||||||
|
// minify, but only in production
|
||||||
|
production && terser()
|
||||||
|
],
|
||||||
|
external: [
|
||||||
|
|
||||||
|
]
|
||||||
|
};
|
||||||
21
src/.babelrc
Normal file
21
src/.babelrc
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
[
|
||||||
|
"@babel/preset-env",
|
||||||
|
{
|
||||||
|
"targets": {
|
||||||
|
"node": "current"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"@babel/plugin-proposal-class-properties",
|
||||||
|
"@babel/plugin-proposal-private-methods",
|
||||||
|
"@babel/plugin-proposal-optional-chaining",
|
||||||
|
"@babel/plugin-proposal-nullish-coalescing-operator",
|
||||||
|
"@babel/plugin-proposal-export-namespace-from",
|
||||||
|
"@babel/plugin-proposal-export-default-from",
|
||||||
|
"@babel/plugin-syntax-dynamic-import"
|
||||||
|
]
|
||||||
|
}
|
||||||
36
src/helpers/fs-node.js
Normal file
36
src/helpers/fs-node.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* Describes a node in the file-system
|
||||||
|
*
|
||||||
|
* @class
|
||||||
|
*/
|
||||||
|
export class FsNode{
|
||||||
|
/**
|
||||||
|
* @constructor
|
||||||
|
* @param {FsNode} c
|
||||||
|
*/
|
||||||
|
constructor(c){
|
||||||
|
if(c){
|
||||||
|
this.file = c.file;
|
||||||
|
this.path = c.path;
|
||||||
|
this.stats = c.stats;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relative path of the node
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
file;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Absolute path of the node
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relative path of the node
|
||||||
|
* @type {module:fs.Stats}
|
||||||
|
*/
|
||||||
|
stats;
|
||||||
|
}
|
||||||
131
src/helpers/fs-promises.js
Normal file
131
src/helpers/fs-promises.js
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
|
||||||
|
// Common/General
|
||||||
|
/**
|
||||||
|
* Get file status.
|
||||||
|
*
|
||||||
|
* @param {module:fs.PathLike} path - A path to a file. If a URL is provided, it must use the `file:` protocol.
|
||||||
|
* @return {Promise<module:fs.Stats>}
|
||||||
|
*/
|
||||||
|
export async function stat(path){
|
||||||
|
return await new Promise((resolve, reject) => fs.stat(path, (err, stats) => err ? reject(err) : resolve(stats)));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Similar to stat except this will return null, when the file does not exist instead of throw an error
|
||||||
|
*
|
||||||
|
* @param {module:fs.PathLike} path - A path to a file. If a URL is provided, it must use the `file:` protocol.
|
||||||
|
* @return {Promise<module:fs.Stats>}
|
||||||
|
*/
|
||||||
|
export async function tryStat(path){
|
||||||
|
return await new Promise((resolve, reject) => fs.stat(path, (err, stats) => err && err.code!=='ENOENT' ? reject(err) : resolve(stats||null)));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Directories
|
||||||
|
/**
|
||||||
|
* Asynchronous mkdir(2) - create a directory.
|
||||||
|
*
|
||||||
|
* @param {string} path - A path to a file. If a URL is provided, it must use the `file:` protocol.
|
||||||
|
* @param {Object} [options] - Either the file mode, or an object optionally specifying the file mode and whether parent folders
|
||||||
|
* should be created. If a string is passed, it is parsed as an octal integer. If not specified, defaults to `0o777`.
|
||||||
|
* @param {boolean} [options.recursive] - Indicates whether parent folders should be created.
|
||||||
|
* @param {string} [options.mode] - A file mode. If a string is passed, it is parsed as an octal integer. If not specified 0o777 is used as a default
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
export async function mkdir(path, options) {
|
||||||
|
return await new Promise((resolve, reject) =>
|
||||||
|
fs.mkdir(path, options, (err) => err ? reject(err) : resolve())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronous rmdir(2) - delete a directory.
|
||||||
|
*
|
||||||
|
* @param {string} path A path to a file. If a URL is provided, it must use the `file:` protocol.
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
export async function rmdir(path) {
|
||||||
|
return await new Promise((resolve, reject) =>
|
||||||
|
fs.rmdir(path, (err) => err ? reject(err) : resolve())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronous readdir(3) - read a directory.
|
||||||
|
*
|
||||||
|
* @param {string} path - A path to a file. If a URL is provided, it must use the `file:` protocol.
|
||||||
|
* @param {Object} [options] - The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used.
|
||||||
|
* @param {"ascii" | "utf8" | "utf-8" | "utf16le" | "ucs2" | "ucs-2" | "base64" | "latin1" | "binary" | "hex"} [options.encoding] - Encoding
|
||||||
|
* @param {boolean} [options.withFileTypes] - Include file types
|
||||||
|
* @return {Promise<Array.<string>>}
|
||||||
|
*/
|
||||||
|
export async function readdir(path, options) {
|
||||||
|
return await new Promise((resolve, reject) => fs.readdir(path, options, (err, files) => err ? reject(err) : resolve(files)));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Files
|
||||||
|
/**
|
||||||
|
* Asynchronous delete a name and possibly the file it refers to.
|
||||||
|
*
|
||||||
|
* @param {string} path - A path to a file. If a URL is provided, it must use the `file:` protocol.
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
export async function unlink(path) {
|
||||||
|
return await new Promise((resolve, reject) =>
|
||||||
|
fs.unlink(path, (err) => err ? reject(err) : resolve())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a file
|
||||||
|
*
|
||||||
|
* @param {string} path
|
||||||
|
* @param {Object} [options] - Read options, like encoding and mode
|
||||||
|
* @param {"ascii" | "utf8" | "utf-8" | "utf16le" | "ucs2" | "ucs-2" | "base64" | "latin1" | "binary" | "hex"} [options.encoding] - Encoding
|
||||||
|
* @param {string} [options.flag] - If a flag is not provided, it defaults to `'r'`.
|
||||||
|
* @return {Promise<Buffer>}
|
||||||
|
*/
|
||||||
|
export async function readFile(path, options) {
|
||||||
|
return await new Promise((resolve, reject) =>
|
||||||
|
fs.readFile(path, options, (err, data) => err ? reject(err) : resolve(data))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronously writes data to a file, replacing the file if it already exists. The underlying file will _not_ be closed automatically.
|
||||||
|
* The `FileHandle` must have been opened for writing.
|
||||||
|
* It is unsafe to call `writeFile()` multiple times on the same file without waiting for the `Promise` to be resolved (or rejected).
|
||||||
|
* @param data The data to write. If something other than a `Buffer` or `Uint8Array` is provided, the value is coerced to a string.
|
||||||
|
* @param {Object} [options] - Either the encoding for the file, or an object optionally specifying the encoding, file mode, and flag.
|
||||||
|
* @param {"ascii" | "utf8" | "utf-8" | "utf16le" | "ucs2" | "ucs-2" | "base64" | "latin1" | "binary" | "hex"} [options.encoding] - If `encoding` is not supplied, the default of `'utf8'` is used.
|
||||||
|
* @param {string} [options.mode] - If `mode` is not supplied, the default of `0o666` is used.
|
||||||
|
* If `mode` is a string, it is parsed as an octal integer.
|
||||||
|
* @param {string} [options.flag] - If `flag` is not supplied, the default of `'w'` is used.
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
export async function writeFile(path, data, options) {
|
||||||
|
return await new Promise((resolve, reject) =>
|
||||||
|
fs.writeFile(path, data, options, (err, data) => err ? reject(err) : resolve(data))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronously copies src to dest. By default, dest is overwritten if it already exists.
|
||||||
|
* No arguments other than a possible exception are given to the callback function.
|
||||||
|
* Node.js makes no guarantees about the atomicity of the copy operation.
|
||||||
|
* If an error occurs after the destination file has been opened for writing, Node.js will attempt
|
||||||
|
* to remove the destination.
|
||||||
|
* @param {string} src A path to the source file.
|
||||||
|
* @param {string} dest A path to the destination file.
|
||||||
|
* @param {string} flags An integer that specifies the behavior of the copy operation. The only supported flag is fs.constants.COPYFILE_EXCL, which causes the copy operation to fail if dest already exists.
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
export async function copyFile(src, dest, flags) {
|
||||||
|
return await new Promise((resolve, reject) =>
|
||||||
|
fs.copyFile(src, dest, flags, (err) => err ? reject(err) : resolve())
|
||||||
|
);
|
||||||
|
}
|
||||||
133
src/helpers/sync.js
Normal file
133
src/helpers/sync.js
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import Observable from "zen-observable";
|
||||||
|
import { Host } from "../host";
|
||||||
|
import { WatchEvent } from "./watcher";
|
||||||
|
|
||||||
|
const IS_DEBUG = false;// Hard-code this to true to debug watch-related things
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes an event emitted from syncing a set of files
|
||||||
|
* @class
|
||||||
|
*/
|
||||||
|
export class SyncEvent {
|
||||||
|
/**
|
||||||
|
* @constructor
|
||||||
|
* @param {SyncEvent} c
|
||||||
|
*/
|
||||||
|
constructor(c) {
|
||||||
|
if (c) {
|
||||||
|
this.type = c.type;
|
||||||
|
this.changes = c.changes;
|
||||||
|
this.files = c.files;
|
||||||
|
this.nodes = c.nodes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of watch-event
|
||||||
|
*
|
||||||
|
* @type {'dicovery'|'changes'}
|
||||||
|
*/
|
||||||
|
type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Array<ChangedFile>}
|
||||||
|
*/
|
||||||
|
changes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Array<string>}
|
||||||
|
*/
|
||||||
|
files;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Array<FsNode>}
|
||||||
|
*/
|
||||||
|
nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class
|
||||||
|
* @augments Observable<SyncEvent>
|
||||||
|
*/
|
||||||
|
export class Sync extends Observable {
|
||||||
|
/** @type {Host} */
|
||||||
|
host;
|
||||||
|
|
||||||
|
/** @type {Observable<WatchEvent>} */
|
||||||
|
source;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function
|
||||||
|
* @param {Observable<WatchEvent>} watch
|
||||||
|
* @param {string} srcPath
|
||||||
|
* @param {Host} host
|
||||||
|
*/
|
||||||
|
Sync.create = (watch, srcPath, host) => {
|
||||||
|
let observers = [];
|
||||||
|
|
||||||
|
// TODO might want to sync subscription with the first/last subscriber on the watch-stream
|
||||||
|
|
||||||
|
let subscription;
|
||||||
|
let first = false;
|
||||||
|
|
||||||
|
let start = () => {
|
||||||
|
first = true;
|
||||||
|
if(!subscription) {// Make sure to never subscribe twice
|
||||||
|
subscription = watch.subscribe(async (event) => {
|
||||||
|
let errors = [];
|
||||||
|
let tasks = (
|
||||||
|
first
|
||||||
|
? event.files.map(file => {
|
||||||
|
return host.copy(host.resolve(srcPath, file), file);
|
||||||
|
})
|
||||||
|
: event.changes.map(({ event, file }) => {
|
||||||
|
return event === 'removed' ? host.remove(file) : host.copy(host.resolve(srcPath, file), file)
|
||||||
|
})
|
||||||
|
).map(x=>x.catch(err=>errors.push(err)));
|
||||||
|
first = false;
|
||||||
|
|
||||||
|
let next = new SyncEvent({
|
||||||
|
...event,
|
||||||
|
// Some other data
|
||||||
|
});
|
||||||
|
await Promise.all(tasks);
|
||||||
|
for(let err of errors){
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
for (let observer of observers) {
|
||||||
|
observer.next(next);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let stop = () => {
|
||||||
|
subscription.unsubscribe();
|
||||||
|
subscription = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
let sync = new Sync((observer) => {
|
||||||
|
observers.push(observer);
|
||||||
|
if (observers.length === 1) {
|
||||||
|
start();// Get this party started!!
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// On unsubscribe
|
||||||
|
observers.splice(observers.indexOf(observer), 1);
|
||||||
|
if (observers.length === 0) stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
sync.host = host;
|
||||||
|
sync.stream = watch;
|
||||||
|
sync.stop = stop;
|
||||||
|
sync.start = start;
|
||||||
|
|
||||||
|
start();// Kickstart a sync, whether subscribe is called or not!
|
||||||
|
|
||||||
|
return sync;
|
||||||
|
};
|
||||||
127
src/helpers/task-token.js
Normal file
127
src/helpers/task-token.js
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import Observable from "zen-observable";
|
||||||
|
import process from "process";
|
||||||
|
|
||||||
|
export class CancelError extends Error{
|
||||||
|
// Just a wrapper
|
||||||
|
constructor(){
|
||||||
|
super(...arguments);
|
||||||
|
this.isCancel = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TaskToken{
|
||||||
|
constructor(parent){
|
||||||
|
this._notify = {
|
||||||
|
cancelRequested: null,
|
||||||
|
completed: null,
|
||||||
|
cancelCompleted: null
|
||||||
|
};
|
||||||
|
this._children = new Set();
|
||||||
|
this.onCancelled = new Promise((resolve)=>this._notify.cancelRequested = resolve);
|
||||||
|
this.onCompleted = new Promise((resolve)=>this._notify.completed = resolve);
|
||||||
|
if(parent){
|
||||||
|
this._parent = parent;
|
||||||
|
this._parent._children.add(this);
|
||||||
|
}
|
||||||
|
this.taskLog = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark task as completed
|
||||||
|
*/
|
||||||
|
complete(){
|
||||||
|
if(this._notify.cancelCompleted){
|
||||||
|
this._notify.cancelCompleted();
|
||||||
|
}else {
|
||||||
|
this.completed = true;
|
||||||
|
}
|
||||||
|
if(this._parent){
|
||||||
|
this._parent._children.delete(this);
|
||||||
|
}
|
||||||
|
this._notify.completed();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if task is completed
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
completed = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if cancellation is requested
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
cancelled = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Promise that triggers when cancellation is requested
|
||||||
|
*
|
||||||
|
* @type {Promise<void>}
|
||||||
|
*/
|
||||||
|
onCancelled;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Promise that notifies when task is completed
|
||||||
|
*
|
||||||
|
* @type {Promise<void>}
|
||||||
|
*/
|
||||||
|
onCompleted;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log a message
|
||||||
|
* @param {string} message
|
||||||
|
* @param {string} type
|
||||||
|
* @param {string} tags
|
||||||
|
*/
|
||||||
|
log(message, {
|
||||||
|
type,
|
||||||
|
tags,
|
||||||
|
subTask
|
||||||
|
}){
|
||||||
|
let logItem = {
|
||||||
|
message: message,
|
||||||
|
type: type||'info',
|
||||||
|
tags: Array.from(new Set(tags||[])),
|
||||||
|
timestamp: process.hrtime(),
|
||||||
|
subTask: subTask
|
||||||
|
};
|
||||||
|
this.taskLog.push(logItem);
|
||||||
|
if(this._parent){
|
||||||
|
this._parent.log(message, {type: logItem.type, tags: logItem.tags, subTask: true});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers cancellation. Completes when cancel has completed
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async cancel(){
|
||||||
|
let ownCompletion = new Promise((resolve)=>{
|
||||||
|
this._notify.cancelCompleted = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send the cancel message
|
||||||
|
this.cancelled = true;
|
||||||
|
this._notify.cancelRequested();
|
||||||
|
|
||||||
|
// Wait for child and own completion
|
||||||
|
let childCancels = Array.from(this._children).map(c=>c.cancel());
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
...childCancels,// Cancel child tasks
|
||||||
|
ownCompletion
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throw an error if task cancellation was requested
|
||||||
|
*/
|
||||||
|
throwIfCancelled(){
|
||||||
|
if(this.cancelled){
|
||||||
|
throw new CancelError("Cancel requested");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
107
src/helpers/traverse.js
Normal file
107
src/helpers/traverse.js
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
/*
|
||||||
|
* @module helpers/traverse
|
||||||
|
*/
|
||||||
|
// Export types used in the traverse-functionality
|
||||||
|
import * as nodePath from "path";
|
||||||
|
|
||||||
|
export const SkipNode = Symbol("skip-node");
|
||||||
|
export const SkipChildren = Symbol("skip-children");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {boolean|SkipNode|SkipChildren|null|undefined} TraverselReturn
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes the current state of traversal
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export class TraveralState{
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} file
|
||||||
|
* @param {string} root
|
||||||
|
* @param {module:fs.Stats} stats
|
||||||
|
*/
|
||||||
|
constructor(file, root, stats){
|
||||||
|
this.file = file;
|
||||||
|
this.root = root;
|
||||||
|
this.stats = stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relative path to the file (relative from root)
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
file;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stats of the file
|
||||||
|
*
|
||||||
|
* @type {module:fs.Stats}
|
||||||
|
*/
|
||||||
|
stats;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Root path used when starting traversal, and thus the path from whice file is relative
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
root;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate whether this node has been flagged as to-be-skipped (and not included in the output)
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
skipped = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate whether this node's children have been flagged as to-be-skipped (and not included in the output)
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
skippedChildren = false;
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
/**
|
||||||
|
* Absolute path to the file
|
||||||
|
*
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
get path(){
|
||||||
|
return nodePath.resolve(this.root, this.file);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
/**
|
||||||
|
* Indicate that this node is to be skipped and neither it, nor its children included in the output
|
||||||
|
*
|
||||||
|
* @return {SkipNode}
|
||||||
|
*/
|
||||||
|
skip(){
|
||||||
|
this.skipped = true;
|
||||||
|
return SkipNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate that this node's children are to be skipped and excluded from the output
|
||||||
|
*
|
||||||
|
* @return {SkipChildren}
|
||||||
|
*/
|
||||||
|
skipChildren(){
|
||||||
|
this.skippedChildren = true;
|
||||||
|
return SkipChildren;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for processing nodes when traversing a directory
|
||||||
|
*
|
||||||
|
* @callback TraveralState~traverseCallback
|
||||||
|
* @param {string} file - Config for the stats-call
|
||||||
|
* @param {TraveralState} state - Url of the resource.
|
||||||
|
* @returns {Promise<TraverselReturn>|TraverselReturn} - A truthy return value to include the file into the end results
|
||||||
|
* A falsy value to exclude it. To exclude this node and its children use the SkipNode-symbol. To include this node but exclude
|
||||||
|
* the children return the SkipChildren-symbol
|
||||||
|
*/
|
||||||
81
src/helpers/watch.js
Normal file
81
src/helpers/watch.js
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import Observable from "zen-observable";
|
||||||
|
import minimatch from "minimatch";
|
||||||
|
import { WatchEvent, Watcher } from "./watcher";
|
||||||
|
import { Sync } from "./sync";
|
||||||
|
|
||||||
|
// We've got an annoying circular dependency with Host (and potentially with sync), which could be fixed if we define an interface that Host implements...
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class
|
||||||
|
* @augments Observable<WatchEvent>
|
||||||
|
*/
|
||||||
|
export class Watch extends Observable{
|
||||||
|
/** @type {Host} */
|
||||||
|
host;
|
||||||
|
|
||||||
|
/** @type {string} */
|
||||||
|
path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param pattern
|
||||||
|
* @returns {Watch}
|
||||||
|
*/
|
||||||
|
glob(pattern) {
|
||||||
|
let mm = new minimatch.Minimatch(pattern);
|
||||||
|
let derivedWatch = this.map(event => new WatchEvent({
|
||||||
|
...event,
|
||||||
|
changes: event.changes.filter(({ file }) => mm.match(file)),
|
||||||
|
files: event.files.filter((file) => mm.match(file)),
|
||||||
|
nodes: event.nodes.filter(({ file }) => mm.match(file)),
|
||||||
|
})).filter(event => event.type !== 'changed' || event.changes.length > 0);
|
||||||
|
|
||||||
|
derivedWatch.host = this.host;
|
||||||
|
derivedWatch.path = this.path;
|
||||||
|
derivedWatch.prototype = Watch.prototype;
|
||||||
|
return derivedWatch;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} target
|
||||||
|
*/
|
||||||
|
sync(target){
|
||||||
|
return Sync.create(this, this.host.resolve(this.path), this.host.from(target));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} path - Path to start watching
|
||||||
|
* @param {Host} host - Host to base from when doing operations like Watch.sync
|
||||||
|
* @returns {Watch}
|
||||||
|
*/
|
||||||
|
Watch.create = (path, host)=>{
|
||||||
|
/** @type {Array.<ZenObservable.Observer<WatchEvent>>} */
|
||||||
|
let observers = [];
|
||||||
|
let emitFn = (event)=>{
|
||||||
|
for(let observer of observers){
|
||||||
|
observer.next(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let watcher = new Watcher(path, emitFn);
|
||||||
|
let watch = new Watch(observer => {
|
||||||
|
observers.push(observer);
|
||||||
|
if (observers.length === 1) {
|
||||||
|
watcher.start();// Get this party started!!
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// On unsubscribe
|
||||||
|
observers.splice(observers.indexOf(observer), 1);
|
||||||
|
if (observers.length === 0) watcher.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
watch.host = host.from();// Forked host
|
||||||
|
watch.path = path;
|
||||||
|
|
||||||
|
return watch;
|
||||||
|
};
|
||||||
|
|
||||||
|
export {WatchEvent, Watcher};
|
||||||
360
src/helpers/watcher.js
Normal file
360
src/helpers/watcher.js
Normal file
@ -0,0 +1,360 @@
|
|||||||
|
/**
|
||||||
|
* @module watch
|
||||||
|
*/
|
||||||
|
import nodePath from "path";
|
||||||
|
import fs from "fs";
|
||||||
|
import * as fsp from "./fs-promises";
|
||||||
|
import {FsNode} from "./fs-node";
|
||||||
|
|
||||||
|
const IS_DEBUG = false;// Hard-code this to true to debug watch-related things
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef ChangedFile
|
||||||
|
* @property {'discovered','added','removed','modified'} event
|
||||||
|
* @property {string} file
|
||||||
|
*/
|
||||||
|
export class ChangedFile{
|
||||||
|
/**
|
||||||
|
* @param {ChangedFile} c
|
||||||
|
*/
|
||||||
|
constructor(c){
|
||||||
|
if(c){
|
||||||
|
this.event = c.event;
|
||||||
|
this.file = c.file;
|
||||||
|
this.path = c.path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {'discovered','added','removed','modified'} */
|
||||||
|
event;
|
||||||
|
|
||||||
|
/** @type {string} */
|
||||||
|
file;
|
||||||
|
|
||||||
|
/** @type {string} */
|
||||||
|
path;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes an event emitted from watching a set of files
|
||||||
|
* @class
|
||||||
|
*/
|
||||||
|
export class WatchEvent{
|
||||||
|
/**
|
||||||
|
* @constructor
|
||||||
|
* @param {WatchEvent} c
|
||||||
|
*/
|
||||||
|
constructor(c){
|
||||||
|
if(c){
|
||||||
|
this.type = c.type;
|
||||||
|
this.changes = c.changes;
|
||||||
|
this.files = c.files;
|
||||||
|
this.nodes = c.nodes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of watch-event
|
||||||
|
*
|
||||||
|
* @type {'dicovery'|'changes'}
|
||||||
|
*/
|
||||||
|
type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Array<ChangedFile>}
|
||||||
|
*/
|
||||||
|
changes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Array<string>}
|
||||||
|
*/
|
||||||
|
files;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Array<FsNode>}
|
||||||
|
*/
|
||||||
|
nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal structure to keep track of watched fs-nodes
|
||||||
|
*
|
||||||
|
* @class
|
||||||
|
*/
|
||||||
|
class WatchedNode extends FsNode{
|
||||||
|
/**
|
||||||
|
* @constructor
|
||||||
|
* @param {WatchedNode} c
|
||||||
|
*/
|
||||||
|
constructor(c){
|
||||||
|
super(c);
|
||||||
|
if(c){
|
||||||
|
this.parent = c.parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A watcher instance from NodeJS:fs
|
||||||
|
*
|
||||||
|
* @type {module:fs.FSWatcher}
|
||||||
|
*/
|
||||||
|
fsWatcher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A map of child-items
|
||||||
|
*
|
||||||
|
* @type {Map.<string, WatchedNode>}
|
||||||
|
*/
|
||||||
|
children;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The parent node of this node
|
||||||
|
*
|
||||||
|
* @type {WatchedNode}
|
||||||
|
*/
|
||||||
|
parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Watch object representing some directory being watched
|
||||||
|
*
|
||||||
|
* @class
|
||||||
|
*/
|
||||||
|
export class Watcher{
|
||||||
|
/** @type {WatchedNode} */
|
||||||
|
#root;
|
||||||
|
|
||||||
|
/** Callback for emitting */
|
||||||
|
#emit = ()=>{};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Traverse the tree and list all nodes
|
||||||
|
* @returns {Array<FsNode>}
|
||||||
|
*/
|
||||||
|
#listNodes(){
|
||||||
|
let list = [];
|
||||||
|
let queue = [this.#root];
|
||||||
|
while (queue.length > 0) {
|
||||||
|
/** @type {WatchedNode} */
|
||||||
|
let item = queue.splice(0, 1)[ 0 ];
|
||||||
|
if (item.file) list.push(new FsNode(item));
|
||||||
|
if (item.children) {
|
||||||
|
queue.splice(0, 0, ...Array.from(item.children.values()).sort((a, b) => {
|
||||||
|
let azCompare = a.file < b.file ? -1 : 1;
|
||||||
|
let aIsDir = a.stats && a.stats.isDirectory();
|
||||||
|
let bIsDir = b.stats && b.stats.isDirectory();
|
||||||
|
return aIsDir && !bIsDir ? -1 : (bIsDir && !aIsDir ? 1 : azCompare);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detection
|
||||||
|
#markedNodes = new Set();
|
||||||
|
#queuedRecheck = false;
|
||||||
|
#markRecheck(item){
|
||||||
|
IS_DEBUG && console.log("\n{{ RECHECK QUEUED " + item.file + " }}\n");
|
||||||
|
this.#markedNodes.add(item);
|
||||||
|
if (!this.#queuedRecheck) {
|
||||||
|
this.#queuedRecheck = setTimeout(() => {
|
||||||
|
IS_DEBUG && console.log("\n{{ RECHECKING }}\n");
|
||||||
|
this.#queuedRecheck = false;
|
||||||
|
this.#recheck(false);
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async #recheck(isInitial){
|
||||||
|
// Initialize
|
||||||
|
let changes = new Map();
|
||||||
|
let checking = true;
|
||||||
|
let queue = Array.from(this.#markedNodes);
|
||||||
|
this.#markedNodes = new Set();
|
||||||
|
let rootPath = this.#root.path;
|
||||||
|
|
||||||
|
while (queue.length > 0) {
|
||||||
|
let newQueuedItems = await Promise.all(
|
||||||
|
queue.map(async (queuedItem) => {
|
||||||
|
/** @type {WatchedNode} **/
|
||||||
|
let watcher = queuedItem;
|
||||||
|
let watchFile = watcher.file;
|
||||||
|
let watchPath = watchFile ? nodePath.resolve(rootPath, watchFile) : rootPath;// might want to store this so we don't have to do this everytime
|
||||||
|
let removeQueue = [];
|
||||||
|
let newQueue = [];
|
||||||
|
|
||||||
|
if (changes.get(watcher) === 'removed') return; // SKIP, removal already processed
|
||||||
|
|
||||||
|
let prevStat = watcher.stats ? { size: watcher.stats.size, mtime: watcher.stats.mtimeMs } : null;
|
||||||
|
watcher.stats = await fsp.tryStat(watchPath);
|
||||||
|
let newStat = watcher.stats ? { size: watcher.stats.size, mtime: watcher.stats.mtimeMs } : null;
|
||||||
|
|
||||||
|
if (watcher.stats) {
|
||||||
|
// Node still exists
|
||||||
|
if (watcher.stats.isDirectory()) {
|
||||||
|
let dirContents = await fsp.readdir(watchPath);
|
||||||
|
let oldChildren = watcher.children || new Map();
|
||||||
|
let newChildren = new Map();
|
||||||
|
let changed = [];
|
||||||
|
|
||||||
|
for(let child of dirContents){
|
||||||
|
let file = nodePath.posix.join(...[watcher.file, child].filter(x => x));
|
||||||
|
let path = nodePath.resolve(...[rootPath, watcher.path, child].filter(x => x));
|
||||||
|
let childWatcher = oldChildren.get(file);
|
||||||
|
if(!childWatcher){
|
||||||
|
childWatcher = new WatchedNode({
|
||||||
|
path: path,
|
||||||
|
file: file,
|
||||||
|
parent: watcher
|
||||||
|
});
|
||||||
|
changes.set(childWatcher, 'added');
|
||||||
|
changed.push(childWatcher);
|
||||||
|
}
|
||||||
|
newChildren.set(file, childWatcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (watcher.children) {
|
||||||
|
for (let [key, child] of watcher.children) {
|
||||||
|
if (!newChildren.has(key)) {
|
||||||
|
changes.set(child, 'removed');
|
||||||
|
changed.push(child);
|
||||||
|
removeQueue.push(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watcher.children = newChildren;
|
||||||
|
newQueue.splice(0, 0, ...changed);
|
||||||
|
|
||||||
|
if (changed.length) {
|
||||||
|
if (changes.get(watcher) !== 'added') changes.set(watcher, 'updated');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (JSON.stringify(prevStat) !== JSON.stringify(newStat) && !changes.get(watcher)) {
|
||||||
|
if (!prevStat) changes.set(watcher, 'added');
|
||||||
|
else changes.set(watcher, 'updated');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start a watch if the path exists
|
||||||
|
if (!watcher.fsWatcher) {
|
||||||
|
watcher.fsWatcher = fs.watch(watchPath, {
|
||||||
|
// options
|
||||||
|
}, (eventType, fileName) => {
|
||||||
|
if (checking || !watcher.children) return;// Early exit, this happens when scanning a dir for files. A change is often triggered for directories accessed...
|
||||||
|
let changedFile = nodePath.posix.join(...[watcher.file, fileName].filter(x => x));
|
||||||
|
let changedPath = nodePath.resolve(...[rootPath, watcher.path, fileName].filter(x => x));
|
||||||
|
let child = watcher.children.get(changedFile);
|
||||||
|
IS_DEBUG && console.log("[EV] " + eventType.toUpperCase() + ": " + changedFile);
|
||||||
|
if (!child) {
|
||||||
|
child = new WatchedNode({
|
||||||
|
path: changedPath,
|
||||||
|
file: changedFile,
|
||||||
|
parent: watcher
|
||||||
|
});
|
||||||
|
watcher.children.set(changedFile, child);
|
||||||
|
}
|
||||||
|
this.#markRecheck(child);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
IS_DEBUG && console.log("{{ STARTED WATCH ON " + watchFile + "}}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Node is removed!
|
||||||
|
removeQueue.push(watcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process anything removed in this tree
|
||||||
|
for (let item of removeQueue) {
|
||||||
|
let removednode = removeQueue.splice(0, 1)[ 0 ];
|
||||||
|
changes.set(removednode, 'removed');
|
||||||
|
if (removednode.parent && removednode.parent.children) {
|
||||||
|
IS_DEBUG && console.log(`{{ Removed ${removednode.file} from watch }}`);
|
||||||
|
removednode.parent.children.delete(removednode.file);
|
||||||
|
}
|
||||||
|
if (removednode.fsWatcher) {
|
||||||
|
IS_DEBUG && console.log("{{ STOPPED WATCH ON " + removednode.file + "}}");
|
||||||
|
removednode.fsWatcher.close();
|
||||||
|
delete removednode.fsWatcher;
|
||||||
|
}
|
||||||
|
if (removednode.children) {
|
||||||
|
removeQueue.splice(0, 0, ...Array.from(removednode.children.values));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newQueue;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
queue = [].concat(...newQueuedItems.filter(x => x));
|
||||||
|
}
|
||||||
|
|
||||||
|
checking = false;
|
||||||
|
let nodes = this.#listNodes();
|
||||||
|
let event = new WatchEvent({
|
||||||
|
type: isInitial? 'discovery': 'changed',
|
||||||
|
changes: [],
|
||||||
|
files: nodes.map(({ file }) => file),//nodes.filter(x=>!x.stats||!x.stats.isDirectory()).map(({file})=>file),
|
||||||
|
nodes: nodes
|
||||||
|
});
|
||||||
|
for (let [key, value] of changes) {
|
||||||
|
if (!isInitial || key.file) {
|
||||||
|
event.changes.push(new ChangedFile({
|
||||||
|
event: isInitial && value === 'added' ? 'discovered' : value,
|
||||||
|
file: key.file,
|
||||||
|
path: nodePath.resolve(rootPath, key.file)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (event.changes.length > 0 || isInitial) {
|
||||||
|
this.#emit(event);
|
||||||
|
} else {
|
||||||
|
if (!isInitial) {
|
||||||
|
IS_DEBUG && console.warn("{{ CHECKED WITHOUT SEEING CHANGES }}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param rootPath
|
||||||
|
*/
|
||||||
|
constructor(rootPath, emit){
|
||||||
|
/** @type {WatchedNode} */
|
||||||
|
this.#root = new WatchedNode({
|
||||||
|
file: '',
|
||||||
|
path: rootPath
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#emit = emit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
async start(){
|
||||||
|
IS_DEBUG && console.log("{{ STARTING WATCH }}");
|
||||||
|
this.#markedNodes.add(this.#root);
|
||||||
|
await this.#recheck(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop(){
|
||||||
|
IS_DEBUG && console.log("{{ STOPPING WATCH }}");
|
||||||
|
let queue = [this.#root];
|
||||||
|
while (queue.length > 0) {
|
||||||
|
let watcher = queue.splice(0, 1);
|
||||||
|
if (watcher.fsWatcher) {
|
||||||
|
watcher.fsWatcher.close();
|
||||||
|
delete watcher.fsWatcher;
|
||||||
|
}
|
||||||
|
if (watcher.children) {
|
||||||
|
queue.splice(0, 0, ...watcher.children);
|
||||||
|
delete watcher.children;
|
||||||
|
}
|
||||||
|
if (watcher.stats) {
|
||||||
|
delete watcher.stats;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
674
src/host.js
Normal file
674
src/host.js
Normal file
@ -0,0 +1,674 @@
|
|||||||
|
// NodeJS
|
||||||
|
import nodePath from "path";
|
||||||
|
import { exec, spawn } from "child_process";
|
||||||
|
import process from "process";
|
||||||
|
|
||||||
|
// Libraries
|
||||||
|
import Observable from 'zen-observable';
|
||||||
|
import { AnsiUp } from "ansi-up";
|
||||||
|
import minimatch from "minimatch";
|
||||||
|
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
import * as fsp from "./helpers/fs-promises"
|
||||||
|
import { TraveralState, SkipNode, SkipChildren } from "./helpers/traverse";
|
||||||
|
import { Watch, WatchEvent } from "./helpers/watch";
|
||||||
|
import { FsNode } from "./helpers/fs-node";
|
||||||
|
import { TaskToken } from "./helpers/task-token";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Must haves
|
||||||
|
* - write (file with auto-create parent path, overwrite/append options)
|
||||||
|
* - watch (with glob or regex matching)
|
||||||
|
* - copy
|
||||||
|
* - clean/remove (either clean a dir, or remove with glob or regex matching)
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {TraveralState.traverseCallback | string} TraversalArg
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Host environment (keeps track of current working directory, promisifies fs-functions and provides host-utilities)
|
||||||
|
*/
|
||||||
|
export class Host {
|
||||||
|
/**
|
||||||
|
* @param {Host} obj - Host to clone from
|
||||||
|
* @param {string} obj.workingDirectory - Directory to work in
|
||||||
|
*/
|
||||||
|
constructor({
|
||||||
|
workingDirectory
|
||||||
|
} = {}) {
|
||||||
|
this.workingDirectory = workingDirectory;
|
||||||
|
this.ansiUp = new AnsiUp();
|
||||||
|
}
|
||||||
|
|
||||||
|
logToConsole = true;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a path
|
||||||
|
*
|
||||||
|
* @param {...string} path - Paths to resolve
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
resolve(...path) {
|
||||||
|
if (this.workingDirectory) return nodePath.resolve(this.workingDirectory, ...path);
|
||||||
|
else return nodePath.resolve(...path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get relative path name
|
||||||
|
* @param {...string} path - Paths to resolve
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
relative(...path){
|
||||||
|
let abs = this.resolve(...path);
|
||||||
|
let localRelative = nodePath.relative(this.workingDirectory, abs);
|
||||||
|
return localRelative.split(nodePath.sep).join(nodePath.posix.sep);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like cwd this creates a new Host-node relative to the current one. Unlike cwd though, from does not support creating dirs
|
||||||
|
* and does not check existence before setting the workingDirectory. Because of this Host.from(...) is synchronous.
|
||||||
|
*
|
||||||
|
* @param {string} path - Relative path to switch working directory
|
||||||
|
* @return {Host}
|
||||||
|
*/
|
||||||
|
from(...path) {
|
||||||
|
return new Host({ workingDirectory: this.resolve(...path) })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronous stat(2) - Get file status.
|
||||||
|
*
|
||||||
|
* @param {...string} path - A path to a file.
|
||||||
|
* @return {Promise<module:fs.Stats>}
|
||||||
|
*/
|
||||||
|
async stat(...path) {
|
||||||
|
return await fsp.stat(this.resolve(...path));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Similar to stat except this will return null, when the file does not exist instead of throw an error
|
||||||
|
*
|
||||||
|
* @param {..string} path - A path to a file. If a URL is provided, it must use the `file:` protocol.
|
||||||
|
* @return {Promise<module:fs.Stats>}
|
||||||
|
*/
|
||||||
|
async tryStat(...path) {
|
||||||
|
return await fsp.tryStat(this.resolve(...path));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new Host-node with it's working directory set to the specified path. If desired an option can be passed
|
||||||
|
* to make sure the relevant directories are created first
|
||||||
|
*
|
||||||
|
* @param {string} path - Relative path to switch working directory
|
||||||
|
* @param {Object} [opts] - Options to the command
|
||||||
|
* @param {boolean} [opts.create] - Create the directories if needed
|
||||||
|
* @return {Promise<Host>}
|
||||||
|
*/
|
||||||
|
async cwd(path, { create } = { create: true }) {
|
||||||
|
let resolved = path ? this.resolve(path) : this.workingDirectory;
|
||||||
|
try {
|
||||||
|
let dirStat = await this.tryStat(resolved);
|
||||||
|
if (!dirStat) {
|
||||||
|
if (!create) throw new Error(`Can't switch to ${path}: Directory ${resolved} does not exist`);
|
||||||
|
else {
|
||||||
|
await this.assertDir(resolved, typeof (create) === 'string' ? create : undefined);
|
||||||
|
}
|
||||||
|
} else if (!dirStat.isDirectory()) {
|
||||||
|
throw new Error(`Can't switch to ${path}: ${resolved} is not a directory`);
|
||||||
|
}
|
||||||
|
return new Host({ workingDirectory: resolved });
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make sure the specified directory exists, creating it if needed.
|
||||||
|
*
|
||||||
|
* @param {string} [path]
|
||||||
|
* @param {string} [mode]
|
||||||
|
* @return {Promise<Host>}
|
||||||
|
*/
|
||||||
|
async assertDir(path, mode) {
|
||||||
|
let createQueue = [];
|
||||||
|
let resolved = this.resolve(path);
|
||||||
|
let next = resolved;
|
||||||
|
let stat = await this.tryStat(next);
|
||||||
|
while (!stat) {
|
||||||
|
createQueue.splice(0, 0, next);
|
||||||
|
next = nodePath.dirname(next);
|
||||||
|
stat = await this.tryStat(next);
|
||||||
|
}
|
||||||
|
while (stat) ;
|
||||||
|
|
||||||
|
for (let dir of createQueue) {
|
||||||
|
await this.mkdir(dir, mode).catch(err => {
|
||||||
|
if (err.code === 'EEXIST') return true;// Someone beat us to it
|
||||||
|
else throw err;// Still an error!
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.workingDirectory === resolved) {
|
||||||
|
return this;
|
||||||
|
} else {
|
||||||
|
return new Host({ workingDirectory: resolved });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronously writes data to a file, replacing the file if it already exists. The underlying file will _not_ be closed automatically.
|
||||||
|
* The `FileHandle` must have been opened for writing.
|
||||||
|
* It is unsafe to call `writeFile()` multiple times on the same file without waiting for the `Promise` to be resolved (or rejected).
|
||||||
|
* @param data The data to write. If something other than a `Buffer` or `Uint8Array` is provided, the value is coerced to a string.
|
||||||
|
* @param {Object} [options] - Either the encoding for the file, or an object optionally specifying the encoding, file mode, and flag.
|
||||||
|
* @param {"ascii" | "utf8" | "utf-8" | "utf16le" | "ucs2" | "ucs-2" | "base64" | "latin1" | "binary" | "hex"} [options.encoding] - If `encoding` is not supplied, the default of `'utf8'` is used.
|
||||||
|
* @param {string} [options.mode] - If `mode` is not supplied, the default of `0o666` is used.
|
||||||
|
* If `mode` is a string, it is parsed as an octal integer.
|
||||||
|
* @param {string} [options.flag] - If `flag` is not supplied, the default of `'w'` is used.
|
||||||
|
* @param {boolean} [options.create] - Create the path to the directory if needed
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
async write(path, data, options) {
|
||||||
|
let targetPath = this.resolve(path);
|
||||||
|
let writeFileOpts = {};
|
||||||
|
if (options) {
|
||||||
|
for (let key in ['encoding', 'mode', 'flag']) {
|
||||||
|
if (options.hasOwnProperty(key)) writeFileOpts[ key ] = options[ key ];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let createPath = options && options.create !== undefined ? options.create : true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.writeFile(targetPath, data, writeFileOpts);
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code === 'ENOENT' && createPath) {
|
||||||
|
let dirPath = this.resolve(nodePath.dirname(targetPath));
|
||||||
|
await this.mkdir(dirPath, { recursive: true });
|
||||||
|
await this.writeFile(targetPath, data, writeFileOpts);
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a file
|
||||||
|
*
|
||||||
|
* @param {string} path
|
||||||
|
* @param {Object} [options] - Read options, like encoding and mode
|
||||||
|
* @param {"ascii" | "utf8" | "utf-8" | "utf16le" | "ucs2" | "ucs-2" | "base64" | "latin1" | "binary" | "hex"} [options.encoding] - Encoding
|
||||||
|
* @param {string} [options.flag] - If a flag is not provided, it defaults to `'r'`.
|
||||||
|
* @return {Promise<Buffer>}
|
||||||
|
*/
|
||||||
|
async read(path, options) {
|
||||||
|
return this.readFile(path, options);// Zero special handling needed over the promisified fs-function
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a file or directory. If trying to delee a non-empty directory, the recursive option must be set, or it will throw an error
|
||||||
|
*
|
||||||
|
* @param {string} path
|
||||||
|
* @param {Object} [options] - Optional options
|
||||||
|
* @param {boolean} [options.recursive] - Indicates whether files in a directory should be deleted
|
||||||
|
* @return {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async remove(path, options) {
|
||||||
|
let { recursive } = options || {};
|
||||||
|
let resolved = this.resolve(path);
|
||||||
|
let stat = await this.tryStat(resolved);
|
||||||
|
if (!stat) {
|
||||||
|
return false; // There was nothing to delete
|
||||||
|
} else if (stat.isFile()) {
|
||||||
|
await this.unlink(resolved);
|
||||||
|
} else if (stat.isDirectory()) {
|
||||||
|
let contents = await this.readdir(resolved);
|
||||||
|
if (contents.length > 0) {
|
||||||
|
if (!recursive) throw new Error(`Can't remove non-empty directory ${path} (${resolved})`);
|
||||||
|
else {
|
||||||
|
await Promise.all(contents.map(f => this.remove(this.resolve(path, f), options)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await this.rmdir(path);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Can't remove ${path}: ${resolved} is an unexpected edge-case`);
|
||||||
|
}
|
||||||
|
return true;// Whatever needed deleting got deleted
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Traverse directory at path and return the relative file
|
||||||
|
*
|
||||||
|
* @param {...TraversalArg} [filterOrPath]
|
||||||
|
* @return {Promise<Array.<string>>}
|
||||||
|
*/
|
||||||
|
async traverse(...filterOrPath) {
|
||||||
|
return (await this.traverseNodes(...filterOrPath)).map(node => node.file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Traverse directory at path and return the full-nodes (see return-value). Optionally specify a filter callback
|
||||||
|
*
|
||||||
|
* @param {...TraversalArg} [filterOrPath]
|
||||||
|
* @return {Promise<Array.<TraveralState>>}
|
||||||
|
*/
|
||||||
|
async traverseNodes(...filterOrPath) {
|
||||||
|
let filters = [];
|
||||||
|
let paths = [];
|
||||||
|
for (let value of filterOrPath) {
|
||||||
|
if (value) {
|
||||||
|
if (typeof (value) === 'string') {
|
||||||
|
paths.push(value)
|
||||||
|
} else {
|
||||||
|
filters.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let nodes = [];
|
||||||
|
let resolved = this.resolve(...paths);
|
||||||
|
let queue = (await this.readdir(resolved));
|
||||||
|
while (queue.length > 0) {
|
||||||
|
let file = queue.splice(0, 1)[ 0 ];
|
||||||
|
let path = this.resolve(resolved, file);
|
||||||
|
let stats = await this.stat(path);
|
||||||
|
|
||||||
|
let state = new TraveralState(file, resolved, stats);
|
||||||
|
let result = filters.length === 0 ? true : undefined;
|
||||||
|
for (let filter of filters) {
|
||||||
|
let filterResult = await Promise.resolve(filter(file, state));
|
||||||
|
if (result === undefined || filterResult !== undefined) {
|
||||||
|
result = filterResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (state.skipped) result = SkipNode;
|
||||||
|
else if (state.skippedChildren) result = SkipChildren;
|
||||||
|
else result = !!result;
|
||||||
|
|
||||||
|
if (result === true || result === SkipChildren) {
|
||||||
|
nodes.push(state);
|
||||||
|
}
|
||||||
|
if (result !== SkipNode && result !== SkipChildren) {
|
||||||
|
if (stats.isDirectory()) {
|
||||||
|
queue.splice(0, 0, ...(await this.readdir(path)).map(x => nodePath.posix.join(file, x)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Match a set of files based on glob patterns. The files returned will be written using relative paths .e.g: src/**.*js will return src/main.js
|
||||||
|
*
|
||||||
|
* @param {string} pattern
|
||||||
|
* @return {Promise<Array.<string>>}
|
||||||
|
*/
|
||||||
|
async glob(pattern, options) {
|
||||||
|
// TODO this, properly...
|
||||||
|
return (await this.globNodes(pattern, options)).map(state => state.file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Match a set of files based on glob patterns. The items returned are full traversal-states, thus including stats and an absolute path
|
||||||
|
*
|
||||||
|
* @param {string} pattern
|
||||||
|
* @return {Promise<Array.<TraveralState>>}
|
||||||
|
*/
|
||||||
|
async globNodes(pattern, options) {
|
||||||
|
let { path, pattern: mmPattern } = splitPattern(pattern);
|
||||||
|
|
||||||
|
let mm = mmPattern ? new minimatch.Minimatch(mmPattern, options) : null;
|
||||||
|
if (path) {
|
||||||
|
let derivedHost = this.from(path);
|
||||||
|
let nodes = await derivedHost.traverseNodes(mm ? (file, state) => mm.match(file) : null);
|
||||||
|
return nodes.map(node => Object.assign(node, {
|
||||||
|
file: nodePath.posix.join(...([path, node.file].filter(x => x))),
|
||||||
|
root: this.workingDirectory
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
return await this.traverseNodes(mm ? (file, state) => mm.match(file) : null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Watch a pattern or path for changes to its files
|
||||||
|
* // Suggestion rework this to watch(path) and then glob(pattern) as an observable transformer
|
||||||
|
*
|
||||||
|
* @param {string} path - Path to watch
|
||||||
|
* @return {Watch}
|
||||||
|
*/
|
||||||
|
watch(path) {
|
||||||
|
let resolvedPath = path ? this.resolve(path) : this.workingDirectory;
|
||||||
|
return Watch.create(resolvedPath, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy file(s) from one place to another (TODO: this method aint properly finished yet!)
|
||||||
|
*
|
||||||
|
* @param {string|string[]} from
|
||||||
|
* @param {string} to
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
async copy(from, to) {
|
||||||
|
let resolvedTo = this.resolve(to);
|
||||||
|
|
||||||
|
if (!(from instanceof Array)) {
|
||||||
|
from = [from];
|
||||||
|
}
|
||||||
|
let sources = await Promise.all(
|
||||||
|
from.map(async (src) => {
|
||||||
|
if (!src || typeof (src) === 'string') {
|
||||||
|
let srcPath = this.resolve(src);
|
||||||
|
let stats = await this.tryStat(srcPath);
|
||||||
|
if (!stats) throw new Error(`File ${src} not found!`);
|
||||||
|
return {
|
||||||
|
file: src,
|
||||||
|
path: srcPath,
|
||||||
|
stats: stats
|
||||||
|
}
|
||||||
|
} else if (src && src.stats && src.path) {
|
||||||
|
return src;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
let toStat = await this.tryStat(resolvedTo);
|
||||||
|
|
||||||
|
if (sources.length === 1 && sources[ 0 ].stats.isFile() && (!toStat || toStat.isFile())) {
|
||||||
|
// Simple copy file
|
||||||
|
let toDir = nodePath.dirname(resolvedTo);
|
||||||
|
await this.assertDir(toDir, { create: true });
|
||||||
|
await this.copyFile(sources[ 0 ].path, resolvedTo);
|
||||||
|
} else {
|
||||||
|
throw new Error("Currently unsupported!");
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: the idea is to make a sync(glob, targetDir) here, which would watch the glob pattern and sync files to the targetDir
|
||||||
|
|
||||||
|
// Promisified FS-functions (might want to make these as static?
|
||||||
|
// The naming in fs is inconsistent, should we unify them here?
|
||||||
|
/**
|
||||||
|
* Asynchronous mkdir(2) - create a directory.
|
||||||
|
* @param {string} path - A path to a file. If a URL is provided, it must use the `file:` protocol.
|
||||||
|
* @param {Object} [options] - Either the file mode, or an object optionally specifying the file mode and whether parent folders
|
||||||
|
* should be created. If a string is passed, it is parsed as an octal integer. If not specified, defaults to `0o777`.
|
||||||
|
* @param {boolean} [options.recursive] - Indicates whether parent folders should be created.
|
||||||
|
* @param {string} [options.mode] - A file mode. If a string is passed, it is parsed as an octal integer. If not specified 0o777 is used as a default
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
async mkdir(path, options) {
|
||||||
|
return await fsp.mkdir(path, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronous rmdir(2) - delete a directory.
|
||||||
|
* @param {...string} path - A path to a file. If a URL is provided, it must use the `file:` protocol.
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
async rmdir(...path) {
|
||||||
|
return await fsp.rmdir(this.resolve(...path));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronous delete a name and possibly the file it refers to.
|
||||||
|
* @param {string} path - A path to a file. If a URL is provided, it must use the `file:` protocol.
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
async unlink(...path) {
|
||||||
|
return await fsp.unlink(this.resolve(...path));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronous readdir(3) - read a directory.
|
||||||
|
* @param {string} path - A path to a file. If a URL is provided, it must use the `file:` protocol.
|
||||||
|
* @param {Object} [options] - The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used.
|
||||||
|
* @param {"ascii" | "utf8" | "utf-8" | "utf16le" | "ucs2" | "ucs-2" | "base64" | "latin1" | "binary" | "hex"} [options.encoding] - Encoding
|
||||||
|
* @param {boolean} [options.withFileTypes] - Include file types
|
||||||
|
* @return {Promise<Array.<string>>}
|
||||||
|
*/
|
||||||
|
async readdir(path, options) {
|
||||||
|
return await fsp.readdir(this.resolve(path), options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a file
|
||||||
|
*
|
||||||
|
* @param {string} path
|
||||||
|
* @param {Object} [options] - Read options, like encoding and mode
|
||||||
|
* @param {"ascii" | "utf8" | "utf-8" | "utf16le" | "ucs2" | "ucs-2" | "base64" | "latin1" | "binary" | "hex"} [options.encoding] - Encoding
|
||||||
|
* @param {string} [options.flag] - If a flag is not provided, it defaults to `'r'`.
|
||||||
|
* @return {Promise<Buffer>}
|
||||||
|
*/
|
||||||
|
async readFile(path, options) {
|
||||||
|
return await fsp.readFile(this.resolve(path), options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronously writes data to a file, replacing the file if it already exists. The underlying file will _not_ be closed automatically.
|
||||||
|
* The `FileHandle` must have been opened for writing.
|
||||||
|
* It is unsafe to call `writeFile()` multiple times on the same file without waiting for the `Promise` to be resolved (or rejected).
|
||||||
|
* @param {string} path - Path to write to
|
||||||
|
* @param {Buffer|string} data - The data to write. If something other than a `Buffer` or `Uint8Array` is provided, the value is coerced to a string.
|
||||||
|
* @param {Object} [options] - Either the encoding for the file, or an object optionally specifying the encoding, file mode, and flag.
|
||||||
|
* @param {"ascii" | "utf8" | "utf-8" | "utf16le" | "ucs2" | "ucs-2" | "base64" | "latin1" | "binary" | "hex"} [options.encoding] - If `encoding` is not supplied, the default of `'utf8'` is used.
|
||||||
|
* @param {string} [options.mode] - If `mode` is not supplied, the default of `0o666` is used.
|
||||||
|
* If `mode` is a string, it is parsed as an octal integer.
|
||||||
|
* @param {string} [options.flag] - If `flag` is not supplied, the default of `'w'` is used.
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
async writeFile(path, data, options) {
|
||||||
|
return await fsp.writeFile(this.resolve(path), data, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronously copies src to dest. By default, dest is overwritten if it already exists.
|
||||||
|
* No arguments other than a possible exception are given to the callback function.
|
||||||
|
* Node.js makes no guarantees about the atomicity of the copy operation.
|
||||||
|
* If an error occurs after the destination file has been opened for writing, Node.js will attempt
|
||||||
|
* to remove the destination.
|
||||||
|
* @param {string} src A path to the source file.
|
||||||
|
* @param {string} dest A path to the destination file.
|
||||||
|
* @param {string} flags An integer that specifies the behavior of the copy operation. The only supported flag is fs.constants.COPYFILE_EXCL, which causes the copy operation to fail if dest already exists.
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
async copyFile(path, dest, flags) {
|
||||||
|
let busyAttemptsRemain = 3;
|
||||||
|
while (busyAttemptsRemain) {
|
||||||
|
--busyAttemptsRemain;
|
||||||
|
let done = await fsp.copyFile(this.resolve(path), dest, flags)
|
||||||
|
.then(() => true)
|
||||||
|
.catch(err => {
|
||||||
|
if (err.code === 'EBUSY' && busyAttemptsRemain) {
|
||||||
|
return new Promise((resolve, reject) => setTimeout(() => resolve(false), 10));
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (done) return;
|
||||||
|
// TODO want to know how much this happens (a retry because resource is busy...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TO BE UPDATED/UNDECIDED
|
||||||
|
/**
|
||||||
|
* @param message
|
||||||
|
* @param options
|
||||||
|
* @param task
|
||||||
|
*/
|
||||||
|
log(message, options, task) {
|
||||||
|
let messageObj = {
|
||||||
|
html: this.ansiUp.ansi_to_html(message).replace(/\n/gm, '<br/>').replace(/ /gm, ' '),// Keep newlines and spaces preserved into html
|
||||||
|
text: this.ansiUp.ansi_to_text(message),
|
||||||
|
};
|
||||||
|
if (!messageObj.html.endsWith('<br/>')) {
|
||||||
|
messageObj.html += '<br/>';
|
||||||
|
}
|
||||||
|
let cleanedMessage = messageObj.text.trim();//cleanUpMessage(message);
|
||||||
|
if (cleanedMessage && this.logToConsole) {
|
||||||
|
if (options?.type === 'error') {
|
||||||
|
console.error(cleanedMessage);
|
||||||
|
} else if (options?.type === 'warning') {
|
||||||
|
console.warn(cleanedMessage);
|
||||||
|
} else {
|
||||||
|
console.log(cleanedMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (task) {
|
||||||
|
task.log(messageObj, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(commands, options, taskToken) {
|
||||||
|
let results = [];
|
||||||
|
for (let command of commands) {
|
||||||
|
if (command !== null && command !== undefined) {
|
||||||
|
results.push(
|
||||||
|
await this.spawn(command, options, taskToken)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// NO-OP (without this here it the function does not appear to do anything)
|
||||||
|
await new Promise((resolve, reject) => resolve());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Old mapped from fs (but modified more heavily...)
|
||||||
|
exec(command, options = {}, taskToken) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let cwd = options.workingDirectory ? path.resolve(options.workingDirectory) : this.workingDirectory;
|
||||||
|
let commandThread = exec(command, Object.assign({}, options || {}, { cwd: cwd }), (err, stdout, stderr) => err ? reject(err) : resolve({
|
||||||
|
out: stdout,
|
||||||
|
err: stderr
|
||||||
|
}));
|
||||||
|
|
||||||
|
commandThread?.stdout.on('data', (data) => {
|
||||||
|
this.log(data, { type: "info" }, taskToken);
|
||||||
|
});
|
||||||
|
|
||||||
|
commandThread?.stderr.on('data', (data) => {
|
||||||
|
this.log(data, { type: "error" }, taskToken);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
spawn(command, options, taskToken) {
|
||||||
|
if (taskToken) taskToken.throwIfCancelled();
|
||||||
|
let spawnTask = new TaskToken(taskToken);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
this.log("> " + command, { type: "info" }, spawnTask);
|
||||||
|
|
||||||
|
let splitCommand = splitArgs(command);
|
||||||
|
let spawnCommand = splitCommand[ 0 ];
|
||||||
|
let cwd = options?.workingDirectory ? nodePath.resolve(options.workingDirectory) : this.workingDirectory;
|
||||||
|
if (spawnCommand === 'npm' && process.platform === 'win32') spawnCommand = 'npm.cmd';// Odd fix, but it does its job
|
||||||
|
else if (spawnCommand === 'yarn' && process.platform === 'win32') spawnCommand = 'yarn.cmd';// Odd fix, but it does its job
|
||||||
|
let commandThread = spawn(spawnCommand, splitCommand.slice(1), Object.assign({
|
||||||
|
encoding: 'utf8'
|
||||||
|
}, options || {}, { cwd: cwd })
|
||||||
|
);
|
||||||
|
let ended = false;
|
||||||
|
|
||||||
|
if (taskToken) taskToken.onCancelled.then(() => {
|
||||||
|
if (!ended) {
|
||||||
|
this.log("Cancelling command " + command, { type: "warning" }, spawnTask);
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
// kill wont work in windows
|
||||||
|
spawn("taskkill", ["/pid", commandThread.pid, '/f', '/t']);
|
||||||
|
} else {
|
||||||
|
commandThread.kill('SIGKILL');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
commandThread.stdout.setEncoding('utf8');
|
||||||
|
commandThread.stdout.on('data', (data) => {
|
||||||
|
this.log(data, { type: "info" }, spawnTask);
|
||||||
|
});
|
||||||
|
|
||||||
|
commandThread.stderr.setEncoding('utf8');
|
||||||
|
commandThread.stderr.on('data', (data) => {
|
||||||
|
this.log(data, { type: "error" }, spawnTask);
|
||||||
|
});
|
||||||
|
|
||||||
|
commandThread?.on('error', (error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
commandThread?.on('close', (code) => {
|
||||||
|
ended = true;
|
||||||
|
spawnTask.complete();
|
||||||
|
try {
|
||||||
|
spawnTask.throwIfCancelled();
|
||||||
|
resolve({ code: code, log: spawnTask.taskLog });
|
||||||
|
} catch (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
/**
|
||||||
|
* Splits a command into its seperate args (for fs.spawn i believe?)
|
||||||
|
* @param {string} command
|
||||||
|
* @returns {Array.<string>}
|
||||||
|
*/
|
||||||
|
function splitArgs(command) {
|
||||||
|
let reg = /([^'" ]+|(?:['"][^'"]+['"]))/g;
|
||||||
|
let match, lastIndex = 0;
|
||||||
|
let args = [];
|
||||||
|
do {
|
||||||
|
match = reg.exec(command);
|
||||||
|
if (match) {
|
||||||
|
args.push(match[ 0 ]);
|
||||||
|
lastIndex = match.index + match[ 0 ].length + 1;
|
||||||
|
} else {
|
||||||
|
lastIndex = command.length;
|
||||||
|
}
|
||||||
|
} while (lastIndex < command.length);
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splits a glob pattern into a fixed part, and a matcher part
|
||||||
|
* @param {string} pattern
|
||||||
|
* @returns {{path:string, pattern:string}}
|
||||||
|
*/
|
||||||
|
function splitPattern(pattern) {
|
||||||
|
if (!pattern) return { path: pattern };
|
||||||
|
let rawPatternParts = pattern.split('/');
|
||||||
|
|
||||||
|
let patternSymbols = new Set("*.?!+{}[]()|@".split(''));
|
||||||
|
for (let i = 0; i < rawPatternParts.length; ++i) {
|
||||||
|
let part = rawPatternParts[ i ];
|
||||||
|
if ((part !== '..' && part !== '.' && part.split('').find(c => patternSymbols.has(c)))) {
|
||||||
|
// stop this loop, we found the start of the pattern we we're looking for
|
||||||
|
return {
|
||||||
|
path: rawPatternParts.slice(0, i).join('/'),
|
||||||
|
pattern: rawPatternParts.slice(i).join('/')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { path: pattern, pattern: '' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add types as statics
|
||||||
|
Host.Node = FsNode;
|
||||||
|
Host.WatchEvent = WatchEvent;
|
||||||
|
|
||||||
|
// Export a default instance
|
||||||
|
export const host = new Host();
|
||||||
|
export default host;
|
||||||
6
src/index.js
Normal file
6
src/index.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import {Host, host} from "./host";
|
||||||
|
|
||||||
|
export {TaskToken} from "./helpers/task-token"; // Needs updating, this comes from the cerxes-alpha-node runner, and is intended for cancaling yarn/npm install jobs
|
||||||
|
export {Host, host};
|
||||||
|
export {processArgs} from "./process-args";
|
||||||
|
export default host;// Is this necessary?
|
||||||
11
src/process-args.js
Normal file
11
src/process-args.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
const args = process.argv;
|
||||||
|
|
||||||
|
// TODO this whole file
|
||||||
|
|
||||||
|
export const processArgs = {
|
||||||
|
cmd: args[0],
|
||||||
|
args: args.slice(1),
|
||||||
|
// TODO: Flags
|
||||||
|
// TODO: Options/Variables/whatever-you-call-it
|
||||||
|
// TODO: env-vars?
|
||||||
|
};
|
||||||
21
tests/.babelrc
Normal file
21
tests/.babelrc
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
[
|
||||||
|
"@babel/preset-env",
|
||||||
|
{
|
||||||
|
"targets": {
|
||||||
|
"node": "current"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
[ "@babel/plugin-proposal-decorators" , { "legacy": true }],
|
||||||
|
[ "@babel/plugin-proposal-class-properties", { "loose": true } ],
|
||||||
|
[ "@babel/plugin-proposal-private-methods", {"loose": true } ],
|
||||||
|
[ "@babel/plugin-proposal-optional-chaining" ],
|
||||||
|
[ "@babel/plugin-proposal-nullish-coalescing-operator" ],
|
||||||
|
[ "@babel/plugin-proposal-export-namespace-from" ],
|
||||||
|
[ "@babel/plugin-proposal-export-default-from" ]
|
||||||
|
]
|
||||||
|
}
|
||||||
1
tests/host/basic/dist/other/another-file.txt
vendored
Normal file
1
tests/host/basic/dist/other/another-file.txt
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
I serve to fill a tree of files
|
||||||
1
tests/host/basic/dist/other/more-files.out
vendored
Normal file
1
tests/host/basic/dist/other/more-files.out
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
I also serve to fill a tree of files!
|
||||||
1
tests/host/basic/dist/test-file.txt
vendored
Normal file
1
tests/host/basic/dist/test-file.txt
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
I am a very basic test-file yo!
|
||||||
1
tests/host/basic/test-file.out
Normal file
1
tests/host/basic/test-file.out
Normal file
@ -0,0 +1 @@
|
|||||||
|
I am the first basic test-file!
|
||||||
33
tests/host/host.basic.test.js
Normal file
33
tests/host/host.basic.test.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import {Host} from "../../dist";
|
||||||
|
import {resolve} from "path";
|
||||||
|
|
||||||
|
test('host.basic', async ()=>{
|
||||||
|
let host = new Host({workingDirectory: __dirname});
|
||||||
|
|
||||||
|
await host.remove("basic", {recursive: true});// Make sure dir is removed and we have clean start (could have the user pass a force flag here..
|
||||||
|
let testHost = await host.cwd('basic');
|
||||||
|
|
||||||
|
await testHost.write('test-file.out', "I am the first basic test-file!");
|
||||||
|
await testHost.write('dist/test-file.txt', "I am a very basic test-file yo!");
|
||||||
|
await testHost.write('dist/other/another-file.txt', "I serve to fill a tree of files");
|
||||||
|
await testHost.write('dist/other/more-files.out', "I also serve to fill a tree of files!");
|
||||||
|
|
||||||
|
let tree = await testHost.traverse();
|
||||||
|
expect(tree.length).toBe(6); // Expecting 6 results, because it should include the directories as well
|
||||||
|
|
||||||
|
let txtFiles = await testHost.glob('**/*.txt');
|
||||||
|
expect(txtFiles.length).toBe(2);
|
||||||
|
|
||||||
|
let outFiles = await testHost.glob('**/*.out');
|
||||||
|
expect(outFiles.length).toBe(2);
|
||||||
|
|
||||||
|
|
||||||
|
let subHost = testHost.from('dist/other');
|
||||||
|
let subOutFiles = await subHost.glob('../../**/*.out');
|
||||||
|
expect(subOutFiles.length).toBe(2);
|
||||||
|
|
||||||
|
|
||||||
|
/** @type {module:fs.Stats} */
|
||||||
|
let stats = await host.stat('basic/test-file.out');
|
||||||
|
expect(stats.isFile()).toBe(true);
|
||||||
|
});
|
||||||
12
tests/host/host.relative.test.js
Normal file
12
tests/host/host.relative.test.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import {Host} from "../../dist";
|
||||||
|
|
||||||
|
test('host.basic', async ()=>{
|
||||||
|
let host = new Host({workingDirectory: __dirname});
|
||||||
|
let distHost = host.from("dist");
|
||||||
|
|
||||||
|
let relMain = distHost.relative(host.resolve("dist/main.js"));
|
||||||
|
expect(relMain).toBe('main.js');
|
||||||
|
|
||||||
|
let relAsset = distHost.relative(host.resolve("dist/assets/icon.svg"));
|
||||||
|
expect(relAsset).toBe('assets/icon.svg');
|
||||||
|
});
|
||||||
1
tests/host/test/dist/other/another-file.txt
vendored
Normal file
1
tests/host/test/dist/other/another-file.txt
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
I serve to fill a tree of files
|
||||||
1
tests/host/test/dist/other/more-files.out
vendored
Normal file
1
tests/host/test/dist/other/more-files.out
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
I also serve to fill a tree of files!
|
||||||
1
tests/host/test/dist/test-file.txt
vendored
Normal file
1
tests/host/test/dist/test-file.txt
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
I am a very basic test-file yo!
|
||||||
1
tests/host/test/test-file.out
Normal file
1
tests/host/test/test-file.out
Normal file
@ -0,0 +1 @@
|
|||||||
|
I am the first basic test-file!
|
||||||
1
tests/manual/dist/assets/icon2.svg
vendored
Normal file
1
tests/manual/dist/assets/icon2.svg
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg>I'm an new example icon</svg>
|
||||||
|
After Width: | Height: | Size: 34 B |
1
tests/manual/dist/assets/logo.png
vendored
Normal file
1
tests/manual/dist/assets/logo.png
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
I'm to lazy to make a miniature example image'
|
||||||
1
tests/manual/dist/index.html
vendored
Normal file
1
tests/manual/dist/index.html
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
<html><body>I am no longer the same entry-html file!</body></html>
|
||||||
5
tests/manual/process-args-dummy.js
Normal file
5
tests/manual/process-args-dummy.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import {processArgs} from "../../dist";
|
||||||
|
let rawArgs = process.argv;
|
||||||
|
|
||||||
|
console.log(`Raw: ${JSON.stringify(rawArgs)}`);
|
||||||
|
console.log(`Processed: ${JSON.stringify(processArgs)}`);
|
||||||
16
tests/manual/process-args.js
Normal file
16
tests/manual/process-args.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import {host, Host} from "../../dist";
|
||||||
|
|
||||||
|
/** @type {Host} **/// Would really wish we can eliminate the need for this...
|
||||||
|
const local = host.from(__dirname);
|
||||||
|
|
||||||
|
let dummyScript = `process-args-dummy`;
|
||||||
|
|
||||||
|
let testCommands = [
|
||||||
|
`-xfv arg1 var1="some option value"`
|
||||||
|
];
|
||||||
|
|
||||||
|
for(let testCmd of testCommands){
|
||||||
|
let cmd = `node -r @babel/register ${dummyScript} ${testCmd}`;
|
||||||
|
console.log(`Test: ${cmd}`);
|
||||||
|
local.exec(cmd);
|
||||||
|
}
|
||||||
1
tests/manual/src/.babelrc
Normal file
1
tests/manual/src/.babelrc
Normal file
@ -0,0 +1 @@
|
|||||||
|
//I represent another source-file whose edit shouldn't have been seen!
|
||||||
1
tests/manual/src/assets/icon2.svg
Normal file
1
tests/manual/src/assets/icon2.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg>I'm an new example icon</svg>
|
||||||
|
After Width: | Height: | Size: 34 B |
1
tests/manual/src/assets/logo.png
Normal file
1
tests/manual/src/assets/logo.png
Normal file
@ -0,0 +1 @@
|
|||||||
|
I'm to lazy to make a miniature example image'
|
||||||
1
tests/manual/src/common/another-source-file.js
Normal file
1
tests/manual/src/common/another-source-file.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
//I represent a service or common part of sorts
|
||||||
1
tests/manual/src/index.html
Normal file
1
tests/manual/src/index.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<html><body>I am no longer the same entry-html file!</body></html>
|
||||||
1
tests/manual/src/main.js
Normal file
1
tests/manual/src/main.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
//I represent a main-file
|
||||||
1
tests/manual/test/dist/other/another-file.txt
vendored
Normal file
1
tests/manual/test/dist/other/another-file.txt
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
I am no longer the same file i used to be!
|
||||||
1
tests/manual/test/dist/other/more-files.out
vendored
Normal file
1
tests/manual/test/dist/other/more-files.out
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
I also serve to fill a tree of files!
|
||||||
1
tests/manual/test/dist/test-file2.txt
vendored
Normal file
1
tests/manual/test/dist/test-file2.txt
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
I am a new test-file!
|
||||||
1
tests/manual/test/dist2/test-file.txt
Normal file
1
tests/manual/test/dist2/test-file.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
I am a new test-file!
|
||||||
1
tests/manual/test/test-file.out
Normal file
1
tests/manual/test/test-file.out
Normal file
@ -0,0 +1 @@
|
|||||||
|
I am the first basic test-file!
|
||||||
62
tests/manual/watch-glob.dev.js
Normal file
62
tests/manual/watch-glob.dev.js
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import {Host} from "../../dist";
|
||||||
|
|
||||||
|
async function testWatch(){
|
||||||
|
let host = new Host({workingDirectory: __dirname});
|
||||||
|
|
||||||
|
await host.remove("test", {recursive: true});// Make sure dir is removed and we have clean start (could have the user pass a force flag here..
|
||||||
|
|
||||||
|
let initialized = false;
|
||||||
|
let testHost = await host.cwd('test');
|
||||||
|
|
||||||
|
await testHost.write('test-file.out', "I am the first basic test-file!");
|
||||||
|
await testHost.write('dist/test-file.txt', "I am a very basic test-file yo!");
|
||||||
|
await testHost.write('dist/other/another-file.txt', "I serve to fill a tree of files");
|
||||||
|
await testHost.write('dist/other/more-files.out', "I also serve to fill a tree of files!");
|
||||||
|
initialized = true;
|
||||||
|
|
||||||
|
// TODO starting a watch on a dir before it is created does not work yet!!
|
||||||
|
let watchSub = host.watch("test").glob("**/*.txt").subscribe(({changes, files})=>{
|
||||||
|
console.log(`----------\nChanges:\n${
|
||||||
|
changes.map(x=>x.event.toUpperCase() + ": " + x.file).join('\n')
|
||||||
|
}\nFiles:\n${files.join("\n")}\n----------`);
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise((resolve,reject)=>setTimeout(()=>resolve(),1000));
|
||||||
|
|
||||||
|
// Watch a remove having occurred
|
||||||
|
console.log("\n** Removing dist/test-file.txt");
|
||||||
|
await testHost.remove('dist/test-file.txt');
|
||||||
|
console.log("** Removed dist/test-file.txt\n");
|
||||||
|
|
||||||
|
await new Promise((resolve,reject)=>setTimeout(()=>resolve(),1000));
|
||||||
|
|
||||||
|
// Watch an add having occurred
|
||||||
|
console.log("\n** Adding dist/test-file2.txt");
|
||||||
|
await testHost.write('dist/test-file2.txt', 'I am a new test-file!');
|
||||||
|
console.log("** Added dist/test-file2.txt\n");
|
||||||
|
|
||||||
|
await new Promise((resolve,reject)=>setTimeout(()=>resolve(),1000));
|
||||||
|
|
||||||
|
// Watch a copy having occurred
|
||||||
|
console.log("\n** Copying dist/test-file2.txt to dist2/test-file.txt");
|
||||||
|
await testHost.copy('dist/test-file2.txt', "dist2/test-file.txt");
|
||||||
|
console.log("** Copied dist2/test-file.txt\n");
|
||||||
|
|
||||||
|
await new Promise((resolve,reject)=>setTimeout(()=>resolve(), 1000));// clearly our current process is slow
|
||||||
|
|
||||||
|
// Watch an edit having occurred
|
||||||
|
console.log("\n** Writing dist/other/another-file.txt");
|
||||||
|
await testHost.write('dist/other/another-file.txt', 'I am no longer the same file i used to be!');
|
||||||
|
console.log("** Written dist/other/another-file.txt\n");
|
||||||
|
|
||||||
|
await new Promise((resolve,reject)=>setTimeout(()=>resolve(), 3000));// clearly our current process is slow
|
||||||
|
|
||||||
|
console.log("UNSUBSCRIBING!");
|
||||||
|
|
||||||
|
// No longer watching should close all file handles!
|
||||||
|
watchSub.unsubscribe();
|
||||||
|
|
||||||
|
console.log("DONE!");
|
||||||
|
}
|
||||||
|
|
||||||
|
testWatch();
|
||||||
74
tests/manual/watch-sync.dev.js
Normal file
74
tests/manual/watch-sync.dev.js
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import {Host} from "../../dist";
|
||||||
|
|
||||||
|
async function testWatch(){
|
||||||
|
let host = new Host({workingDirectory: __dirname});
|
||||||
|
let initialized = false;
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
await host.remove("src", {recursive: true});// Make sure dir is removed and we have clean start (could have the user pass a force flag here..
|
||||||
|
await host.remove("dist", {recursive: true});// Make sure dir is removed and we have clean start (could have the user pass a force flag here..
|
||||||
|
let srcHost = await host.cwd('src');
|
||||||
|
await srcHost.write('main.js', "//I represent a main-file");
|
||||||
|
await srcHost.write('assets/icon.svg', "<svg>I'm an example icon</svg>");
|
||||||
|
await srcHost.write('assets/logo.png', "I'm to lazy to make a miniature example image'");
|
||||||
|
await srcHost.write('common/another-source-file.js', "//I represent a service or common part of sorts");
|
||||||
|
await srcHost.write('.babelrc', "//I represent another source-file to be excluded from being copied");
|
||||||
|
await srcHost.write('index.html', "<html><body>I represent the entry-html file!</body></html>");
|
||||||
|
initialized = true;
|
||||||
|
|
||||||
|
console.log("Initialized, starting sync");
|
||||||
|
|
||||||
|
// Syntax like this for the moment! (this might change in the future...)
|
||||||
|
let syncSub = host.watch("src").glob("**/*.!(js|babelrc|json|scss)").sync("dist").subscribe(sync=>{
|
||||||
|
let changes = sync.changes;
|
||||||
|
console.log(`----------\nSYNCED Changes:\n${
|
||||||
|
changes.map(x=>x.event.toUpperCase() + ": " + x.file).join('\n')
|
||||||
|
}\n`);
|
||||||
|
});
|
||||||
|
|
||||||
|
let watchSub = host.watch("src").glob("**/*.!(js|babelrc|json|scss)").subscribe((event)=>{
|
||||||
|
let {changes, files} = event;
|
||||||
|
console.log(`----------\nOBSERVED Changes:\n${
|
||||||
|
changes.map(x=>x.event.toUpperCase() + ": " + x.file).join('\n')
|
||||||
|
}\nFiles:\n${files.join("\n")}\n----------`);
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise((resolve,reject)=>setTimeout(()=>resolve(),1000));
|
||||||
|
|
||||||
|
// Watch a remove having occurred
|
||||||
|
console.log("** Removing assets/icon.svg");
|
||||||
|
await srcHost.remove('assets/icon.svg');
|
||||||
|
console.log("** Removed assets/icon.svg\n");
|
||||||
|
|
||||||
|
await new Promise((resolve,reject)=>setTimeout(()=>resolve(),1000));
|
||||||
|
|
||||||
|
// Watch an add having occurred
|
||||||
|
console.log("** Adding assets/icon2.svg");
|
||||||
|
await srcHost.write('assets/icon2.svg', "<svg>I'm an new example icon</svg>");
|
||||||
|
console.log("** Added assets/icon2.svg\n");
|
||||||
|
|
||||||
|
await new Promise((resolve,reject)=>setTimeout(()=>resolve(),1000));
|
||||||
|
|
||||||
|
// Watch an edit having occurred
|
||||||
|
console.log("** Writing index.html");
|
||||||
|
await srcHost.write('index.html', "<html><body>I am no longer the same entry-html file!</body></html>");
|
||||||
|
console.log("** Written index.html\n");
|
||||||
|
|
||||||
|
await new Promise((resolve,reject)=>setTimeout(()=>resolve(), 1000));// clearly our current process is slow
|
||||||
|
|
||||||
|
// Test an unwatched edit having occurred
|
||||||
|
console.log("** Writing .babelrc");
|
||||||
|
await srcHost.write('.babelrc', "//I represent another source-file whose edit shouldn't have been seen!");
|
||||||
|
console.log("** Written babelrc\n");
|
||||||
|
|
||||||
|
await new Promise((resolve,reject)=>setTimeout(()=>resolve(), 3000));// clearly our current process is slow
|
||||||
|
|
||||||
|
console.log("UNSUBSCRIBING!");
|
||||||
|
|
||||||
|
// No longer watching should close all file handles!
|
||||||
|
watchSub.unsubscribe();
|
||||||
|
syncSub.unsubscribe();
|
||||||
|
console.log("DONE!");
|
||||||
|
}
|
||||||
|
|
||||||
|
testWatch();
|
||||||
58
tests/manual/watch.dev.js
Normal file
58
tests/manual/watch.dev.js
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import {Host} from "../../dist";
|
||||||
|
|
||||||
|
async function testWatch(){
|
||||||
|
let host = new Host({workingDirectory: __dirname});
|
||||||
|
|
||||||
|
await host.remove("test", {recursive: true});// Make sure dir is removed and we have clean start (could have the user pass a force flag here..
|
||||||
|
|
||||||
|
let initialized = false;
|
||||||
|
let testHost = await host.cwd('test');
|
||||||
|
|
||||||
|
await testHost.write('test-file.out', "I am the first basic test-file!");
|
||||||
|
await testHost.write('dist/test-file.txt', "I am a very basic test-file yo!");
|
||||||
|
await testHost.write('dist/other/another-file.txt', "I serve to fill a tree of files");
|
||||||
|
await testHost.write('dist/other/more-files.out', "I also serve to fill a tree of files!");
|
||||||
|
initialized = true;
|
||||||
|
|
||||||
|
// TODO starting a watch on a dir before it is created does not work yet!!
|
||||||
|
let watchSub = host.watch("test").subscribe(({changes, files})=>{
|
||||||
|
console.log(`----------\nChanges:\n${
|
||||||
|
changes.map(x=>x.event.toUpperCase() + ": " + x.file).join('\n')
|
||||||
|
}\nFiles:\n${files.join("\n")}\n----------`);
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise((resolve,reject)=>setTimeout(()=>resolve(),1000));// clearly our current process is slow
|
||||||
|
|
||||||
|
// Watch a remove having occurred
|
||||||
|
console.log("** Removing dist/test-file.txt");
|
||||||
|
await testHost.remove('dist/test-file.txt');
|
||||||
|
console.log("** Removed dist/test-file.txt\n");
|
||||||
|
|
||||||
|
await new Promise((resolve,reject)=>setTimeout(()=>resolve(),1000));// clearly our current process is slow
|
||||||
|
|
||||||
|
// Watch an add having occurred
|
||||||
|
console.log("** Adding dist/test-file2.txt");
|
||||||
|
await testHost.write('dist/test-file2.txt', 'I am a new test-file!');
|
||||||
|
console.log("** Added dist/test-file2.txt\n");
|
||||||
|
|
||||||
|
await new Promise((resolve,reject)=>setTimeout(()=>resolve(),1000));// clearly our current process is slow
|
||||||
|
|
||||||
|
// Watch a move having occurred TODO we don't have this feature yet!
|
||||||
|
//await testHost.move('dist/test-file2.txt', 'dist2/test-file.txt');
|
||||||
|
|
||||||
|
// Watch an edit having occurred
|
||||||
|
console.log("** Writing dist/other/another-file.txt");
|
||||||
|
await testHost.write('dist/other/another-file.txt', 'I am no longer the same file i used to be!');
|
||||||
|
console.log("** Written dist/other/another-file.txt\n");
|
||||||
|
|
||||||
|
await new Promise((resolve,reject)=>setTimeout(()=>resolve(), 3000));// clearly our current process is slow
|
||||||
|
|
||||||
|
console.log("UNSUBSCRIBING!");
|
||||||
|
|
||||||
|
// No longer watching should close all file handles!
|
||||||
|
watchSub.unsubscribe();
|
||||||
|
|
||||||
|
console.log("DONE!");
|
||||||
|
}
|
||||||
|
|
||||||
|
testWatch();
|
||||||
Loading…
x
Reference in New Issue
Block a user