@allurereport/directory-watcher 3.0.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # Directory Watcher
2
+
3
+ [<img src="https://allurereport.org/public/img/allure-report.svg" height="85px" alt="Allure Report logo" align="right" />](https://allurereport.org "Allure Report")
4
+
5
+ - Learn more about Allure Report at https://allurereport.org
6
+ - 📚 [Documentation](https://allurereport.org/docs/) – discover official documentation for Allure Report
7
+ - ❓ [Questions and Support](https://github.com/orgs/allure-framework/discussions/categories/questions-support) – get help from the team and community
8
+ - 📢 [Official announcements](https://github.com/orgs/allure-framework/discussions/categories/announcements) – be in touch with the latest updates
9
+ - 💬 [General Discussion ](https://github.com/orgs/allure-framework/discussions/categories/general-discussion) – engage in casual conversations, share insights and ideas with the community
10
+
11
+ ---
12
+
13
+ ## Overview
14
+
15
+ Directory Watcher is an utility to track file system changes which supports wildcard patterns.
16
+
17
+ ## Install
18
+
19
+ Use your favorite package manager to install the package:
20
+
21
+ ```shell
22
+ npm add @allurereport/directory-watcher
23
+ yarn add @allurereport/directory-watcher
24
+ pnpm add @allurereport/directory-watcher
25
+ ```
@@ -0,0 +1,7 @@
1
+ declare const watchDirectory: (directory: string, handler: (eventName: "add" | "addDir" | "change" | "unlink" | "unlinkDir", path: string) => void | Promise<void>, options?: {
2
+ usePolling?: boolean;
3
+ ignoreInitial?: boolean;
4
+ }) => () => Promise<void>;
5
+ export default watchDirectory;
6
+ export type { Watcher } from "./watcher.js";
7
+ export { newFilesInDirectoryWatcher, allureResultsDirectoriesWatcher, delayedFileProcessingWatcher, } from "./watcher.js";
package/dist/index.js ADDED
@@ -0,0 +1,15 @@
1
+ import { watch as chokidarWatch } from "chokidar";
2
+ import console from "node:console";
3
+ const watchDirectory = (directory, handler, options = {}) => {
4
+ const { usePolling = false, ignoreInitial = false } = options;
5
+ const watcher = chokidarWatch(directory, { persistent: true, usePolling, ignoreInitial });
6
+ watcher.on("all", async (eventName, path) => {
7
+ await handler(eventName, path);
8
+ });
9
+ watcher.on("error", (error) => {
10
+ console.log("error", error);
11
+ });
12
+ return () => watcher.close();
13
+ };
14
+ export default watchDirectory;
15
+ export { newFilesInDirectoryWatcher, allureResultsDirectoriesWatcher, delayedFileProcessingWatcher, } from "./watcher.js";
@@ -0,0 +1,30 @@
1
+ import type { Dirent } from "node:fs";
2
+ export declare const isFileNotFoundError: (e: unknown) => e is Error;
3
+ export declare const difference: (before: Set<string>, after: Set<string>) => [Set<string>, Set<string>];
4
+ export declare const findMatching: (watchDirectory: string, existingResults: Set<string>, match: (dirent: Dirent) => boolean, maximumDepth?: number) => Promise<void>;
5
+ interface WatchOptions {
6
+ indexDelay?: number;
7
+ abortController?: AbortController;
8
+ }
9
+ export interface Watcher {
10
+ abort: (immediately?: boolean) => Promise<void>;
11
+ initialScan: () => Promise<void>;
12
+ watchEnd: () => Promise<void>;
13
+ }
14
+ interface WatchNewFilesOptions extends WatchOptions {
15
+ recursive?: boolean;
16
+ indexDelay?: number;
17
+ ignoreInitial?: boolean;
18
+ abortController?: AbortController;
19
+ }
20
+ export declare const newFilesInDirectoryWatcher: (directory: string, onNewFile: (file: string, dirent: Dirent) => Promise<void>, options?: WatchNewFilesOptions) => Watcher;
21
+ export declare const allureResultsDirectoriesWatcher: (directory: string, update: (newAllureResults: Set<string>, deletedAllureResults: Set<string>) => Promise<void>, options?: WatchOptions) => Watcher;
22
+ interface FileContentWatcherOptions extends WatchOptions {
23
+ minProcessingDelay?: number;
24
+ maxProcessingDelay?: number;
25
+ }
26
+ export declare const delayedFileProcessingWatcher: (processFile: (file: string) => Promise<void>, options?: FileContentWatcherOptions) => Watcher & {
27
+ addFile: (file: string) => Promise<void>;
28
+ };
29
+ export declare const md5File: (path: string) => Promise<string>;
30
+ export {};
@@ -0,0 +1,277 @@
1
+ import console from "node:console";
2
+ import { createHash } from "node:crypto";
3
+ import { createReadStream } from "node:fs";
4
+ import { opendir, realpath, stat } from "node:fs/promises";
5
+ import { join } from "node:path";
6
+ import { setImmediate, setTimeout } from "node:timers/promises";
7
+ export const isFileNotFoundError = (e) => e instanceof Error && "code" in e && e.code === "ENOENT";
8
+ export const difference = (before, after) => {
9
+ const added = new Set();
10
+ const deleted = new Set(before);
11
+ for (const value of after) {
12
+ if (!deleted.has(value)) {
13
+ added.add(value);
14
+ }
15
+ else {
16
+ deleted.delete(value);
17
+ }
18
+ }
19
+ return [added, deleted];
20
+ };
21
+ export const findMatching = async (watchDirectory, existingResults, match, maximumDepth = 5) => {
22
+ try {
23
+ const dir = await opendir(watchDirectory);
24
+ for await (const dirent of dir) {
25
+ const path = join(dirent.parentPath ?? dirent.path, dirent.name);
26
+ if (dirent.name.at(0) === "." || dirent.name === "node_modules") {
27
+ continue;
28
+ }
29
+ if (existingResults.has(path)) {
30
+ continue;
31
+ }
32
+ if (match(dirent)) {
33
+ existingResults.add(path);
34
+ continue;
35
+ }
36
+ if (dirent.isDirectory() && maximumDepth > 0) {
37
+ await findMatching(path, existingResults, match, maximumDepth - 1);
38
+ }
39
+ }
40
+ }
41
+ catch (e) {
42
+ if (isFileNotFoundError(e)) {
43
+ existingResults.clear();
44
+ return;
45
+ }
46
+ console.error("can't read directory", e);
47
+ }
48
+ };
49
+ const findFiles = async (watchDirectory, existingResults, onNewFile, recursive) => {
50
+ try {
51
+ const dir = await opendir(watchDirectory, { recursive });
52
+ for await (const dirent of dir) {
53
+ if (dirent.isDirectory()) {
54
+ continue;
55
+ }
56
+ const path = join(dirent.parentPath ?? dirent.path, dirent.name);
57
+ if (existingResults.has(path)) {
58
+ continue;
59
+ }
60
+ try {
61
+ await onNewFile(path, dirent);
62
+ existingResults.add(path);
63
+ }
64
+ catch (e) {
65
+ if (!isFileNotFoundError(e)) {
66
+ console.error("can't process file", path, e);
67
+ }
68
+ }
69
+ }
70
+ }
71
+ catch (e) {
72
+ if (isFileNotFoundError(e)) {
73
+ existingResults.clear();
74
+ return;
75
+ }
76
+ console.error("can't read directory", e);
77
+ }
78
+ };
79
+ const singleIteration = async (callback, ...ac) => {
80
+ return setImmediate(undefined, { signal: AbortSignal.any(ac.map((c) => c.signal)) })
81
+ .then(() => callback())
82
+ .catch((err) => {
83
+ if (err.name === "AbortError") {
84
+ return;
85
+ }
86
+ console.error("can't execute callback", err);
87
+ });
88
+ };
89
+ const repeatedIteration = async (indexInterval, callback, ...ac) => {
90
+ return setTimeout(indexInterval, undefined, { signal: AbortSignal.any(ac.map((c) => c.signal)) })
91
+ .then(() => callback())
92
+ .then(() => repeatedIteration(indexInterval, callback, ...ac));
93
+ };
94
+ const noop = async () => { };
95
+ const watch = (initialCallback, iterationCallback, doneCallback, options = {}) => {
96
+ const { indexDelay = 300, abortController: haltAc = new AbortController() } = options;
97
+ const gracefulShutdownAc = new AbortController();
98
+ const init = singleIteration(initialCallback, haltAc);
99
+ const timeout = init
100
+ .then(() => repeatedIteration(indexDelay, iterationCallback, haltAc, gracefulShutdownAc))
101
+ .catch((err) => {
102
+ if (err.name === "AbortError") {
103
+ return;
104
+ }
105
+ console.error("can't execute callback", err);
106
+ })
107
+ .then(() => singleIteration(doneCallback, haltAc));
108
+ return {
109
+ abort: async (immediately = false) => {
110
+ if (immediately) {
111
+ haltAc.abort();
112
+ }
113
+ else {
114
+ gracefulShutdownAc.abort();
115
+ }
116
+ await timeout;
117
+ },
118
+ initialScan: async () => {
119
+ await init;
120
+ },
121
+ watchEnd: async () => {
122
+ await timeout;
123
+ },
124
+ };
125
+ };
126
+ export const newFilesInDirectoryWatcher = (directory, onNewFile, options = {}) => {
127
+ const { recursive = true, ignoreInitial = false, ...rest } = options;
128
+ const indexedFiles = new Set();
129
+ const initialCallback = async () => {
130
+ await findFiles(directory, indexedFiles, ignoreInitial ? noop : onNewFile, recursive);
131
+ };
132
+ const iterationCallback = async () => {
133
+ await findFiles(directory, indexedFiles, onNewFile, recursive);
134
+ };
135
+ return watch(initialCallback, iterationCallback, iterationCallback, rest);
136
+ };
137
+ export const allureResultsDirectoriesWatcher = (directory, update, options = {}) => {
138
+ let previousAllureResults = new Set();
139
+ const callback = async () => {
140
+ const currentAllureResults = new Set();
141
+ await findMatching(directory, currentAllureResults, (dirent) => dirent.isDirectory() && dirent.name === "allure-results");
142
+ const [added, deleted] = difference(previousAllureResults, currentAllureResults);
143
+ await update(added, deleted);
144
+ previousAllureResults = currentAllureResults;
145
+ };
146
+ return watch(callback, callback, callback, options);
147
+ };
148
+ const calculateInfo = async (file) => {
149
+ try {
150
+ const stats = await stat(file);
151
+ if (!stats.isFile()) {
152
+ return null;
153
+ }
154
+ const size = stats.size;
155
+ const mtimeMs = stats.mtimeMs;
156
+ const timestamp = Date.now();
157
+ return { size, mtimeMs, timestamp };
158
+ }
159
+ catch (e) {
160
+ if (isFileNotFoundError(e)) {
161
+ return null;
162
+ }
163
+ throw e;
164
+ }
165
+ };
166
+ const waitUntilFileStopChanging = async (file, info, options) => {
167
+ const start = Date.now();
168
+ const { maxWait, minWait } = options;
169
+ const prev = { ...info };
170
+ while (true) {
171
+ const now = Date.now();
172
+ if (now - start > maxWait) {
173
+ return false;
174
+ }
175
+ const sinceChange = now - prev.timestamp;
176
+ if (sinceChange < minWait) {
177
+ await setTimeout(Math.min(0, maxWait, minWait - sinceChange + 1));
178
+ }
179
+ const current = await calculateInfo(file);
180
+ if (!current) {
181
+ return false;
182
+ }
183
+ const sameSize = current.size === prev.size;
184
+ const sameMtimeMs = current.mtimeMs === prev.mtimeMs;
185
+ if (sameSize && sameMtimeMs) {
186
+ return true;
187
+ }
188
+ prev.size = current.size;
189
+ prev.mtimeMs = current.mtimeMs;
190
+ prev.timestamp = current.timestamp;
191
+ }
192
+ };
193
+ export const delayedFileProcessingWatcher = (processFile, options = {}) => {
194
+ const { minProcessingDelay = 200, maxProcessingDelay = 10000, ...rest } = options;
195
+ const files = new Map();
196
+ const success = new Set();
197
+ const errors = new Set();
198
+ const addFile = async (file) => {
199
+ try {
200
+ const filePath = await realpath(file, { encoding: "utf-8" });
201
+ const info = await calculateInfo(filePath);
202
+ if (!info) {
203
+ return;
204
+ }
205
+ files.set(filePath, info);
206
+ }
207
+ catch (e) {
208
+ if (isFileNotFoundError(e)) {
209
+ return;
210
+ }
211
+ throw e;
212
+ }
213
+ };
214
+ const callback = async () => {
215
+ for (const [file, info] of files) {
216
+ const now = Date.now();
217
+ const sinceChange = now - info.timestamp;
218
+ if (sinceChange < minProcessingDelay) {
219
+ continue;
220
+ }
221
+ try {
222
+ await processFile(file);
223
+ }
224
+ catch (e) {
225
+ if (!isFileNotFoundError(e)) {
226
+ console.log(`could not process file ${file}`, e);
227
+ }
228
+ errors.add(file);
229
+ }
230
+ files.delete(file);
231
+ success.add(file);
232
+ }
233
+ };
234
+ const doneCallback = async () => {
235
+ for (const [file, info] of files) {
236
+ const waitedSuccessfully = await waitUntilFileStopChanging(file, info, {
237
+ minWait: minProcessingDelay,
238
+ maxWait: maxProcessingDelay,
239
+ });
240
+ if (waitedSuccessfully) {
241
+ try {
242
+ await processFile(file);
243
+ }
244
+ catch (e) {
245
+ if (!isFileNotFoundError(e)) {
246
+ console.log(`could not process file ${file}`, e);
247
+ }
248
+ errors.add(file);
249
+ }
250
+ files.delete(file);
251
+ success.add(file);
252
+ }
253
+ else {
254
+ console.error(`can't process file ${file}: file deleted or contents keep changing`);
255
+ errors.add(file);
256
+ }
257
+ }
258
+ };
259
+ const watcher = watch(callback, callback, doneCallback, rest);
260
+ return {
261
+ ...watcher,
262
+ addFile,
263
+ };
264
+ };
265
+ export const md5File = async (path) => {
266
+ return new Promise((resolve, reject) => {
267
+ const output = createHash("md5");
268
+ const input = createReadStream(path);
269
+ input.on("error", (err) => {
270
+ reject(err);
271
+ });
272
+ output.once("readable", () => {
273
+ resolve(output.read().toString("hex"));
274
+ });
275
+ input.pipe(output);
276
+ });
277
+ };
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@allurereport/directory-watcher",
3
+ "version": "3.0.0-beta.10",
4
+ "description": "File system watcher for directories",
5
+ "keywords": [
6
+ "allure",
7
+ "testing",
8
+ "fs",
9
+ "watcher",
10
+ "fsevents"
11
+ ],
12
+ "repository": "https://github.com/allure-framework/allure3",
13
+ "license": "Apache-2.0",
14
+ "author": "Qameta Software",
15
+ "type": "module",
16
+ "exports": {
17
+ ".": "./dist/index.js"
18
+ },
19
+ "module": "dist/index.js",
20
+ "types": "dist/index.d.ts",
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "scripts": {
25
+ "build": "run clean && tsc --project ./tsconfig.json",
26
+ "clean": "rimraf ./dist",
27
+ "eslint": "eslint ./src/**/*.{js,jsx,ts,tsx}",
28
+ "eslint:format": "eslint --fix ./src/**/*.{js,jsx,ts,tsx}",
29
+ "pretest": "rimraf ./out",
30
+ "test": "rimraf ./out && vitest run"
31
+ },
32
+ "dependencies": {
33
+ "chokidar": "^4.0.1"
34
+ },
35
+ "devDependencies": {
36
+ "@stylistic/eslint-plugin": "^2.6.1",
37
+ "@types/eslint": "^8.56.11",
38
+ "@types/node": "^20.17.9",
39
+ "@typescript-eslint/eslint-plugin": "^8.0.0",
40
+ "@typescript-eslint/parser": "^8.0.0",
41
+ "@vitest/runner": "^2.1.8",
42
+ "allure-vitest": "^3.0.9",
43
+ "eslint": "^8.57.0",
44
+ "eslint-config-prettier": "^9.1.0",
45
+ "eslint-plugin-import": "^2.29.1",
46
+ "eslint-plugin-jsdoc": "^50.0.0",
47
+ "eslint-plugin-n": "^17.10.1",
48
+ "eslint-plugin-no-null": "^1.0.2",
49
+ "eslint-plugin-prefer-arrow": "^1.2.3",
50
+ "rimraf": "^6.0.1",
51
+ "ts-node": "^10.9.2",
52
+ "typescript": "^5.6.3",
53
+ "vitest": "^2.1.8"
54
+ }
55
+ }