Skip to content

Commit

Permalink
feat: introduce watch mode (#393)
Browse files Browse the repository at this point in the history
* feat: introduce `watch` mode

* ci: add `watch` tests

* ci: fix Deno permissions

* ci: fix tests

* ci: fix tests

* docs: add `--watch` and `--watch-interval`
  • Loading branch information
wellwelwel authored Jun 14, 2024
1 parent 7b94a13 commit 8b816f7
Show file tree
Hide file tree
Showing 10 changed files with 476 additions and 6 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ node_modules
/coverage
.env
.nyc_output
/.temp
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ That's it 🎉
- [**test**](https://poku.io/docs/documentation/helpers/test)
- [**describe**](https://poku.io/docs/documentation/helpers/describe) and [**it**](https://poku.io/docs/documentation/helpers/it)
- [**beforeEach**](https://poku.io/docs/category/beforeeach-and-aftereach) and [**afterEach**](https://poku.io/docs/category/beforeeach-and-aftereach)
- [**watch**](https://poku.io/docs/documentation/poku/options/watch)
- **Processes**
- [**kill**](https://poku.io/docs/documentation/processes/kill) (_terminate Ports, Port Ranges and PIDs_)
- [**getPIDs**](https://poku.io/docs/documentation/processes/get-pids) (_get all processes IDs using ports and port ranges_)
Expand Down
41 changes: 37 additions & 4 deletions src/bin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import {
hasArg,
argToArray,
} from '../helpers/get-arg.js';
import { poku } from '../modules/poku.js';
import { kill } from '../modules/processes.js';
import { fileResults } from '../configs/files.js';
import { platformIsValid } from '../helpers/get-runtime.js';
import { format } from '../helpers/format.js';
import { write } from '../helpers/logs.js';
import type { Configs } from '../@types/poku.js';
import { hr } from '../helpers/hr.js';
import { watch } from '../services/watch.js';
import { poku } from '../modules/poku.js';
import { kill } from '../modules/processes.js';
import type { Configs } from '../@types/poku.js';

(async () => {
const dirs = (() => {
Expand Down Expand Up @@ -46,6 +48,7 @@ import { hr } from '../helpers/hr.js';
const quiet = hasArg('quiet');
const debug = hasArg('debug');
const failFast = hasArg('fail-fast');
const watchMode = hasArg('watch');

const concurrency = parallel
? Number(getArg('concurrency')) || undefined
Expand Down Expand Up @@ -85,6 +88,7 @@ import { hr } from '../helpers/hr.js';
debug,
failFast,
concurrency,
noExit: watchMode,
deno: {
allow: denoAllow,
deny: denoDeny,
Expand All @@ -102,7 +106,36 @@ import { hr } from '../helpers/hr.js';
console.dir(options, { depth: null, colors: true });
}

poku(dirs, options);
await poku(dirs, options);

if (watchMode) {
const executing = new Set<string>();
const interval = Number(getArg('watch-interval')) || 1500;

fileResults.success.clear();
fileResults.fail.clear();

hr();
write(`Watching: ${dirs.join(', ')}`);

dirs.forEach((dir) => {
watch(dir, (file, event) => {
if (event === 'change') {
if (executing.has(file)) return;

executing.add(file);
fileResults.success.clear();
fileResults.fail.clear();

poku(file, options).then(() => {
setTimeout(() => {
executing.delete(file);
}, interval);
});
}
});
});
}
})();

/* c8 ignore stop */
2 changes: 1 addition & 1 deletion src/modules/each.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export const beforeEach = (
*/
export const afterEach = (
callback: () => unknown,
options: Omit<EachOptions, 'immediate'>
options?: Omit<EachOptions, 'immediate'>
): Control => {
each.after.test = typeof options?.test === 'boolean' ? options.test : true;
each.after.assert =
Expand Down
30 changes: 30 additions & 0 deletions src/polyfills/fs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/* c8 ignore start */

import {
stat as nodeStat,
readdir as nodeReaddir,
type Dirent,
type Stats,
} from 'node:fs';

export const readdir = (
path: string,
options: { withFileTypes: true }
): Promise<Dirent[]> =>
new Promise((resolve, reject) => {
nodeReaddir(path, options, (err, entries) => {
if (err) reject(err);
else resolve(entries);
});
});

export const stat = (path: string): Promise<Stats> => {
return new Promise((resolve, reject) => {
nodeStat(path, (err, stats) => {
if (err) reject(err);
else resolve(stats);
});
});
};

/* c8 ignore stop */
131 changes: 131 additions & 0 deletions src/services/watch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/* c8 ignore next */
import { watch as nodeWatch, type FSWatcher } from 'node:fs';
import { join } from 'node:path';
import { readdir, stat } from '../polyfills/fs.js';
import { listFiles } from '../modules/list-files.js';

/* c8 ignore next */
export type WatchCallback = (file: string, event: string) => void;

class Watcher {
private rootDir: string;
private files: string[] = [];
private fileWatchers: Map<string, FSWatcher> = new Map();
private dirWatchers: Map<string, FSWatcher> = new Map();
private callback: WatchCallback;

constructor(rootDir: string, callback: WatchCallback) {
this.rootDir = rootDir;
this.callback = callback;
}

private watchFile(filePath: string) {
/* c8 ignore next */
if (this.fileWatchers.has(filePath)) return;

const watcher = nodeWatch(filePath, (eventType) => {
this.callback(filePath, eventType);
});

/* c8 ignore start */
watcher.on('error', () => {
return;
});
/* c8 ignore stop */

this.fileWatchers.set(filePath, watcher);
}

private unwatchFiles() {
for (const [filePath, watcher] of this.fileWatchers) {
watcher.close();
this.fileWatchers.delete(filePath);
}
}

private watchFiles(filePaths: string[]) {
this.unwatchFiles();

for (const filePath of filePaths) {
this.watchFile(filePath);
}
}

private async watchDirectory(dir: string) {
if (this.dirWatchers.has(dir)) return;

const watcher = nodeWatch(dir, async (_, filename) => {
if (filename) {
const fullPath = join(dir, filename);

this.files = await listFiles(this.rootDir);
this.watchFiles(this.files);

try {
const stats = await stat(fullPath);
if (stats.isDirectory()) await this.watchDirectory(fullPath);
/* c8 ignore start */
} catch {}
/* c8 ignore stop */
}
});

/* c8 ignore start */
watcher.on('error', () => {
return;
});
/* c8 ignore stop */

this.dirWatchers.set(dir, watcher);

const entries = await readdir(dir, { withFileTypes: true });

for (const entry of entries) {
if (entry.isDirectory()) {
const fullPath = join(dir, entry.name);
await this.watchDirectory(fullPath);
}
}
}

public async start() {
try {
const stats = await stat(this.rootDir);

if (stats.isDirectory()) {
this.files = await listFiles(this.rootDir);

this.watchFiles(this.files);
await this.watchDirectory(this.rootDir);
} else this.watchFile(this.rootDir);
/* c8 ignore start */
} catch (err) {
throw new Error(
`Path does not exist or is not accessible: ${this.rootDir}`
);
}
/* c8 ignore stop */
}

public stop() {
this.unwatchFiles();
this.unwatchDirectories();
}

private unwatchDirectories() {
for (const [dirPath, watcher] of this.dirWatchers) {
watcher.close();
this.dirWatchers.delete(dirPath);
}
}
/* c8 ignore next */
}

/* c8 ignore next */
export const watch = async (path: string, callback: WatchCallback) => {
const watcher = new Watcher(path, callback);

await watcher.start();

return watcher;
};
2 changes: 1 addition & 1 deletion test/docker/deno/latest.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ COPY ./fixtures ./fixtures
RUN apk add lsof
RUN deno run --allow-read --allow-write --allow-env --allow-run tools/compatibility/deno.ts

CMD ["deno", "run", "--allow-read", "--allow-env", "--allow-run", "test/run.test.ts"]
CMD ["deno", "run", "--allow-read", "--allow-write", "--allow-hrtime", "--allow-env", "--allow-run", "test/run.test.ts"]
3 changes: 3 additions & 0 deletions test/run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@ import { poku } from '../src/modules/poku.js';
poku(['test/unit', 'test/integration', 'test/e2e'], {
parallel: true,
debug: true,
deno: {
allow: ['read', 'write', 'hrtime', 'env', 'run', 'net'],
},
});
Loading

0 comments on commit 8b816f7

Please sign in to comment.