Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 7675846
Showing
6 changed files
with
742 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
language: minimal | ||
|
||
os: | ||
- osx | ||
- linux | ||
|
||
before_install: | ||
- export PATH=$HOME/.deno/bin:$PATH | ||
|
||
install: | ||
- curl -L https://deno.land/x/install/install.py | python | ||
|
||
script: | ||
- deno -v | ||
- deno test.ts --allow-write --allow-run |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
# Watch | ||
|
||
A pure deno file watcher. | ||
|
||
## Example | ||
|
||
```typescript | ||
import watch from "https://raw.githubusercontent.com/jinjor/deno-watch/1.0.0/main.ts"; | ||
|
||
for await (const changes of watch("src")) { | ||
console.log(changes.added); | ||
console.log(changes.modified); | ||
console.log(changes.deleted); | ||
} | ||
``` | ||
|
||
```typescript | ||
const end = watch("src").start(changes => { | ||
console.log(changes); | ||
}); | ||
``` | ||
|
||
## Options | ||
|
||
Written in the [source code](./index.ts). | ||
|
||
## Benchmark | ||
|
||
``` | ||
test Benchmark | ||
generated 10930 files. | ||
[Add] | ||
took 183ms to traverse 11232 files | ||
took 147ms to traverse 11542 files | ||
took 142ms to traverse 11845 files | ||
[Modify] | ||
took 139ms to traverse 11891 files | ||
took 136ms to traverse 11891 files | ||
took 154ms to traverse 11891 files | ||
[Delete] | ||
took 138ms to traverse 11608 files | ||
took 134ms to traverse 11274 files | ||
took 145ms to traverse 10960 files | ||
... ok | ||
``` | ||
|
||
Try yourself: | ||
|
||
``` | ||
deno https://raw.githubusercontent.com/jinjor/deno-watch/master/test.ts --allow-write | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,299 @@ | ||
import { | ||
readDir, | ||
readlink, | ||
lstatSync, | ||
lstat, | ||
readDirSync, | ||
readlinkSync, | ||
FileInfo | ||
} from "deno"; | ||
|
||
/** The result of checking in one loop */ | ||
export class Changes { | ||
/** Paths of added files */ | ||
added: string[] = []; | ||
/** Paths of modified files */ | ||
modified: string[] = []; | ||
/** Paths of deleted files */ | ||
deleted: string[] = []; | ||
/** The time[posix ms] when the checking started. */ | ||
startTime: number; | ||
/** The time[posix ms] when the checking ended. */ | ||
endTime: number; | ||
/** Current file count */ | ||
fileCount = 0; | ||
/** added + modified + deleted */ | ||
get length(): number { | ||
return this.added.length + this.modified.length + this.deleted.length; | ||
} | ||
/** all changed paths */ | ||
get all(): string[] { | ||
return [...this.added, ...this.modified, ...this.deleted]; | ||
} | ||
/** The time[ms] took for checking. */ | ||
get time() { | ||
return this.endTime - this.startTime; | ||
} | ||
} | ||
|
||
/** Options */ | ||
export interface Options { | ||
/** The minimum interval[ms] of checking loop. | ||
* The next checking can be delayed until user program ends. | ||
* | ||
* |<------------------ interval ----------------->|<--------------- | ||
* |<-- checking -->| |<-- checking --> | ||
* |<--- user program --->| | ||
* | ||
* | ||
* |<---------- interval --------->| |<----------------------- | ||
* |<-- checking -->| |<-- checking --> | ||
* |<--- user program --->| | ||
*/ | ||
interval?: number; | ||
/** If true, watcher checks the symlinked files/directories too. */ | ||
followSymlink?: boolean; | ||
/** Ignores something like .gitignore, .vscode, etc. */ | ||
ignoreDotFiles?: boolean; | ||
/** Path to search in regex (ex. "\.(ts|css)$") */ | ||
test?: RegExp | string; | ||
/** Path to ignore in regex. */ | ||
ignore?: RegExp | string; | ||
} | ||
|
||
/** The watcher */ | ||
export interface Watcher extends AsyncIterable<Changes> { | ||
start(callback: (changes: Changes) => Promise<void> | void): () => void; | ||
} | ||
|
||
const defaultOptions = { | ||
interval: 1000, | ||
followSymlink: false, | ||
ignoreDotFiles: true, | ||
test: /.*/, | ||
ignore: /$^/ | ||
}; | ||
|
||
/** | ||
* Watch directories and detect changes. | ||
* @example | ||
* // Basic usage. | ||
* for await (const changes of watch("src")) { | ||
* console.log(changes.added); | ||
* console.log(changes.modified); | ||
* console.log(changes.deleted); | ||
* } | ||
* @example | ||
* // Kill watcher from outside of the loop. | ||
* const end = watch("src").start(changes => { | ||
* console.log(changes); | ||
* }); | ||
* end(); | ||
* @param dirs | ||
* @param options | ||
*/ | ||
export default function watch( | ||
dirs: string | string[], | ||
options?: Options | ||
): Watcher { | ||
const dirs_ = Array.isArray(dirs) ? dirs : [dirs]; | ||
options = Object.assign({}, defaultOptions, options); | ||
return { | ||
[Symbol.asyncIterator]() { | ||
return run(dirs_, options); | ||
}, | ||
start: function(callback: (changes: Changes) => Promise<void> | void) { | ||
const state = { | ||
abort: false, | ||
timeout: null | ||
}; | ||
(async () => { | ||
for await (const changes of run(dirs_, options, state)) { | ||
await callback(changes); | ||
} | ||
})(); | ||
return () => { | ||
state.abort = true; | ||
if (state.timeout) { | ||
clearTimeout(state.timeout); | ||
} | ||
}; | ||
} | ||
}; | ||
} | ||
async function* run( | ||
dirs: string[], | ||
options: Options, | ||
state = { | ||
abort: false, | ||
timeout: null | ||
} | ||
) { | ||
const { interval, followSymlink } = options; | ||
const filter = makeFilter(options); | ||
let lastStartTime = Date.now(); | ||
let files = {}; | ||
collect(files, dirs, followSymlink, filter); | ||
|
||
while (!state.abort) { | ||
let waitTime = Math.max(0, interval - (Date.now() - lastStartTime)); | ||
await new Promise(resolve => { | ||
state.timeout = setTimeout(resolve, waitTime); | ||
}); | ||
state.timeout = null; | ||
lastStartTime = Date.now(); | ||
let changes = new Changes(); | ||
changes.startTime = lastStartTime; | ||
|
||
changes.fileCount = Object.keys(files).length; | ||
const newFiles = {}; | ||
await detectChanges(files, newFiles, dirs, followSymlink, filter, changes); | ||
files = newFiles; | ||
|
||
changes.endTime = Date.now(); | ||
if (changes.length) { | ||
yield changes; | ||
} | ||
} | ||
} | ||
|
||
function makeFilter({ test, ignore, ignoreDotFiles }: Options) { | ||
const testRegex = typeof test === "string" ? new RegExp(test) : test; | ||
const ignoreRegex = typeof ignore === "string" ? new RegExp(ignore) : ignore; | ||
return function filter(f: FileInfo, path: string) { | ||
if (ignoreDotFiles) { | ||
const splitted = path.split("/"); | ||
const name = f.name || splitted[splitted.length - 1]; | ||
if (name.charAt(0) === ".") { | ||
return false; | ||
} | ||
} | ||
if (f.isFile()) { | ||
if (!testRegex.test(path)) { | ||
return false; | ||
} | ||
if (ignoreRegex.test(path)) { | ||
return false; | ||
} | ||
} | ||
return true; | ||
}; | ||
} | ||
|
||
async function detectChanges( | ||
prev: any, | ||
curr: any, | ||
dirs: string[], | ||
followSymlink: boolean, | ||
filter: (f: FileInfo, path: string) => boolean, | ||
changes: Changes | ||
): Promise<void> { | ||
await walk(prev, curr, dirs, followSymlink, filter, changes); | ||
Array.prototype.push.apply(changes.deleted, Object.keys(prev)); | ||
} | ||
|
||
async function walk( | ||
prev: any, | ||
curr: any, | ||
files: (string | FileInfo)[], | ||
followSymlink: boolean, | ||
filter: (f: FileInfo, path: string) => boolean, | ||
changes: Changes | ||
): Promise<void> { | ||
const promises = []; | ||
for (let f of files) { | ||
let linkPath; | ||
let path; | ||
let info; | ||
if (typeof f === "string") { | ||
path = f; | ||
info = await (followSymlink ? statTraverse : lstat)(f); | ||
} else if (f.isSymlink() && followSymlink) { | ||
linkPath = f.path; | ||
info = await statTraverse(f.path); | ||
path = info.path; | ||
} else { | ||
path = f.path; | ||
info = f; | ||
} | ||
if (!path) { | ||
throw new Error("path not found"); | ||
} | ||
if (!filter(info, linkPath || path)) { | ||
continue; | ||
} | ||
if (info.isDirectory()) { | ||
const files = await readDir(path); | ||
promises.push(walk(prev, curr, files, followSymlink, filter, changes)); | ||
} else if (info.isFile()) { | ||
if (curr[path]) { | ||
continue; | ||
} | ||
curr[path] = info.modified || info.created; | ||
if (!prev[path]) { | ||
changes.added.push(path); | ||
} else if (prev[path] < curr[path]) { | ||
changes.modified.push(path); | ||
} | ||
delete prev[path]; | ||
} | ||
} | ||
await Promise.all(promises); | ||
} | ||
|
||
function collect( | ||
all: any, | ||
files: (string | FileInfo)[], | ||
followSymlink: boolean, | ||
filter: (f: FileInfo, path?: string) => boolean | ||
): void { | ||
for (let f of files) { | ||
let linkPath; | ||
let path; | ||
let info; | ||
if (typeof f === "string") { | ||
path = f; | ||
info = (followSymlink ? statTraverseSync : lstatSync)(f); | ||
} else if (f.isSymlink() && followSymlink) { | ||
linkPath = f.path; | ||
path = readlinkSync(f.path); | ||
info = statTraverseSync(path); | ||
} else { | ||
path = f.path; | ||
info = f; | ||
} | ||
if (!path) { | ||
throw new Error("path not found"); | ||
} | ||
if (!filter(info, linkPath || path)) { | ||
continue; | ||
} | ||
if (info.isDirectory()) { | ||
collect(all, readDirSync(path), followSymlink, filter); | ||
} else if (info.isFile()) { | ||
all[path] = info.modified || info.created; | ||
} | ||
} | ||
} | ||
|
||
// Workaround for non-linux | ||
async function statTraverse(path: string): Promise<FileInfo> { | ||
const info = await lstat(path); | ||
if (info.isSymlink()) { | ||
const targetPath = await readlink(path); | ||
return statTraverse(targetPath); | ||
} else { | ||
info.path = path; | ||
return info; | ||
} | ||
} | ||
function statTraverseSync(path: string): FileInfo { | ||
const info = lstatSync(path); | ||
if (info.isSymlink()) { | ||
const targetPath = readlinkSync(path); | ||
return statTraverseSync(targetPath); | ||
} else { | ||
info.path = path; | ||
return info; | ||
} | ||
} |
Oops, something went wrong.