@alchemy.run/node-utils 2.0.0-beta.76 → 2.0.0-beta.78
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/THIRD_PARTY_LICENSES.md +11 -11
- package/lib/dependency-watcher.d.ts +28 -0
- package/lib/dependency-watcher.d.ts.map +1 -0
- package/lib/dependency-watcher.js +114 -0
- package/lib/dependency-watcher.js.map +1 -0
- package/lib/register-oxc.d.ts +61 -0
- package/lib/register-oxc.d.ts.map +1 -0
- package/lib/register-oxc.js +220 -0
- package/lib/register-oxc.js.map +1 -0
- package/lib/resolve-specifier.d.ts +46 -0
- package/lib/resolve-specifier.d.ts.map +1 -0
- package/lib/resolve-specifier.js +123 -0
- package/lib/resolve-specifier.js.map +1 -0
- package/lib/transform-cache.d.ts +63 -0
- package/lib/transform-cache.d.ts.map +1 -0
- package/lib/transform-cache.js +193 -0
- package/lib/transform-cache.js.map +1 -0
- package/lib/transform-source.d.ts +19 -0
- package/lib/transform-source.d.ts.map +1 -0
- package/lib/transform-source.js +213 -0
- package/lib/transform-source.js.map +1 -0
- package/lib/watch-import-bun.d.ts +30 -0
- package/lib/watch-import-bun.d.ts.map +1 -0
- package/lib/watch-import-bun.js +73 -0
- package/lib/watch-import-bun.js.map +1 -0
- package/lib/watch-import.d.ts +27 -0
- package/lib/watch-import.d.ts.map +1 -0
- package/lib/watch-import.js +79 -0
- package/lib/watch-import.js.map +1 -0
- package/package.json +21 -2
- package/src/dependency-watcher.ts +130 -0
- package/src/register-oxc.ts +337 -0
- package/src/resolve-specifier.ts +157 -0
- package/src/transform-cache.ts +226 -0
- package/src/transform-source.ts +254 -0
- package/src/watch-import-bun.ts +99 -0
- package/src/watch-import.ts +106 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type DependencyChangeListener, type DependencyWatcherOptions } from "./dependency-watcher.ts";
|
|
2
|
+
export interface BunImportTrackerOptions extends DependencyWatcherOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Directory whose modules belong to the tracked graph. Files outside it and
|
|
5
|
+
* anything under a `node_modules` directory load untouched.
|
|
6
|
+
*/
|
|
7
|
+
readonly root: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Records every project-local module Bun loads after registration and watches
|
|
11
|
+
* those files for changes.
|
|
12
|
+
*
|
|
13
|
+
* Bun has no loader hooks that can evict or re-namespace an evaluated module,
|
|
14
|
+
* so unlike Node's {@link ImportWatcher} this cannot import a fresh
|
|
15
|
+
* generation in-process. A runtime `Bun.plugin` `onLoad` hook is used purely
|
|
16
|
+
* as a dependency probe: it hands the source back unchanged with the loader
|
|
17
|
+
* Bun would have picked itself. Callers react to a change by exiting so a
|
|
18
|
+
* supervisor can start a fresh process.
|
|
19
|
+
*/
|
|
20
|
+
export declare class BunImportTracker {
|
|
21
|
+
#private;
|
|
22
|
+
constructor(options: BunImportTrackerOptions);
|
|
23
|
+
get dependencies(): ReadonlySet<string>;
|
|
24
|
+
subscribe(listener: DependencyChangeListener): () => void;
|
|
25
|
+
/** Stops watching. The load hook stays registered but only echoes sources. */
|
|
26
|
+
close(): Promise<void>;
|
|
27
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
export declare const trackBunImports: (options: BunImportTrackerOptions) => BunImportTracker;
|
|
30
|
+
//# sourceMappingURL=watch-import-bun.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watch-import-bun.d.ts","sourceRoot":"","sources":["../src/watch-import-bun.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,wBAAwB,EAC7B,KAAK,wBAAwB,EAC9B,MAAM,yBAAyB,CAAC;AAEjC,MAAM,WAAW,uBAAwB,SAAQ,wBAAwB;IACvE;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAgBD;;;;;;;;;;GAUG;AACH,qBAAa,gBAAgB;;IAI3B,YAAY,OAAO,EAAE,uBAAuB,EAgC3C;IAED,IAAI,YAAY,IAAI,WAAW,CAAC,MAAM,CAAC,CAEtC;IAED,SAAS,CAAC,QAAQ,EAAE,wBAAwB,GAAG,MAAM,IAAI,CAExD;IAED,8EAA8E;IAC9E,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAErB;IAEK,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAE3C;CACF;AAED,eAAO,MAAM,eAAe,YAAa,uBAAuB,qBACjC,CAAC"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { DependencyWatcher, } from "./dependency-watcher.js";
|
|
4
|
+
const loaders = {
|
|
5
|
+
".js": "js",
|
|
6
|
+
".mjs": "js",
|
|
7
|
+
".cjs": "js",
|
|
8
|
+
".jsx": "jsx",
|
|
9
|
+
".ts": "ts",
|
|
10
|
+
".mts": "ts",
|
|
11
|
+
".cts": "ts",
|
|
12
|
+
".tsx": "tsx",
|
|
13
|
+
};
|
|
14
|
+
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
15
|
+
/**
|
|
16
|
+
* Records every project-local module Bun loads after registration and watches
|
|
17
|
+
* those files for changes.
|
|
18
|
+
*
|
|
19
|
+
* Bun has no loader hooks that can evict or re-namespace an evaluated module,
|
|
20
|
+
* so unlike Node's {@link ImportWatcher} this cannot import a fresh
|
|
21
|
+
* generation in-process. A runtime `Bun.plugin` `onLoad` hook is used purely
|
|
22
|
+
* as a dependency probe: it hands the source back unchanged with the loader
|
|
23
|
+
* Bun would have picked itself. Callers react to a change by exiting so a
|
|
24
|
+
* supervisor can start a fresh process.
|
|
25
|
+
*/
|
|
26
|
+
export class BunImportTracker {
|
|
27
|
+
#watcher;
|
|
28
|
+
#dependencies = new Set();
|
|
29
|
+
constructor(options) {
|
|
30
|
+
if (process.versions.bun === undefined) {
|
|
31
|
+
throw new Error("BunImportTracker requires Bun; Node callers should use watchImport.");
|
|
32
|
+
}
|
|
33
|
+
this.#watcher = new DependencyWatcher(options);
|
|
34
|
+
// Bun reports real paths (`/private/tmp/...` for `/tmp/...` on macOS);
|
|
35
|
+
// match them against the root's real path too.
|
|
36
|
+
const root = realpathSync.native(path.resolve(options.root)) + path.sep;
|
|
37
|
+
const nodeModules = `node_modules${path.sep}`;
|
|
38
|
+
// `root` already ends in a separator, so the segment right after it has no
|
|
39
|
+
// leading one: a `.*${sep}node_modules${sep}` lookahead alone would miss
|
|
40
|
+
// `<root>node_modules/...` and intercept every dependency. Bun cannot
|
|
41
|
+
// return CommonJS source from `onLoad` (oven-sh/bun#19279), so a CJS
|
|
42
|
+
// dependency that reaches this probe loses its exports.
|
|
43
|
+
const filter = new RegExp(`^${escapeRegExp(root)}(?!(?:.*${escapeRegExp(path.sep)})?${escapeRegExp(nodeModules)}).*\\.[cm]?[jt]sx?$`);
|
|
44
|
+
Bun.plugin({
|
|
45
|
+
name: "@alchemy.run/node-utils/watch-import-bun",
|
|
46
|
+
setup: (build) => {
|
|
47
|
+
build.onLoad({ filter }, async (args) => {
|
|
48
|
+
this.#dependencies.add(args.path);
|
|
49
|
+
this.#watcher.set(new Set(this.#dependencies));
|
|
50
|
+
return {
|
|
51
|
+
contents: await Bun.file(args.path).text(),
|
|
52
|
+
loader: loaders[path.extname(args.path)] ?? "js",
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
get dependencies() {
|
|
59
|
+
return this.#watcher.dependencies;
|
|
60
|
+
}
|
|
61
|
+
subscribe(listener) {
|
|
62
|
+
return this.#watcher.subscribe(listener);
|
|
63
|
+
}
|
|
64
|
+
/** Stops watching. The load hook stays registered but only echoes sources. */
|
|
65
|
+
close() {
|
|
66
|
+
return this.#watcher.close();
|
|
67
|
+
}
|
|
68
|
+
async [Symbol.asyncDispose]() {
|
|
69
|
+
await this.close();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export const trackBunImports = (options) => new BunImportTracker(options);
|
|
73
|
+
//# sourceMappingURL=watch-import-bun.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watch-import-bun.js","sourceRoot":"","sources":["../src/watch-import-bun.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type DependencyChangeListener, type DependencyWatcherOptions } from "./dependency-watcher.ts";
|
|
2
|
+
import type { OxcLoaderOptions } from "./register-oxc.ts";
|
|
3
|
+
export interface ImportGeneration<T> {
|
|
4
|
+
readonly value: T;
|
|
5
|
+
readonly namespace: string;
|
|
6
|
+
readonly dependencies: ReadonlySet<string>;
|
|
7
|
+
}
|
|
8
|
+
export interface ImportWatcherOptions extends OxcLoaderOptions, DependencyWatcherOptions {
|
|
9
|
+
readonly parentURL: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Imports fresh Node module generations and watches the exact files loaded by
|
|
13
|
+
* the current generation. Bun callers should use `BunImportTracker` from
|
|
14
|
+
* `./watch-import-bun.ts`: Bun cannot evict evaluated modules, so a change
|
|
15
|
+
* there restarts the process instead of importing a new generation.
|
|
16
|
+
*/
|
|
17
|
+
export declare class ImportWatcher<T = unknown> {
|
|
18
|
+
#private;
|
|
19
|
+
constructor(specifier: string, options: ImportWatcherOptions);
|
|
20
|
+
get dependencies(): ReadonlySet<string>;
|
|
21
|
+
subscribe(listener: DependencyChangeListener): () => void;
|
|
22
|
+
import(): Promise<ImportGeneration<T>>;
|
|
23
|
+
close(): Promise<void>;
|
|
24
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
export declare const watchImport: <T = unknown>(specifier: string, options: ImportWatcherOptions) => ImportWatcher<T>;
|
|
27
|
+
//# sourceMappingURL=watch-import.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watch-import.d.ts","sourceRoot":"","sources":["../src/watch-import.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,wBAAwB,EAC7B,KAAK,wBAAwB,EAC9B,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAa,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAErE,MAAM,WAAW,gBAAgB,CAAC,CAAC;IACjC,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;IAClB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAC5C;AAED,MAAM,WAAW,oBACf,SAAQ,gBAAgB,EAAE,wBAAwB;IAClD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;GAKG;AACH,qBAAa,aAAa,CAAC,CAAC,GAAG,OAAO;;IAQpC,YAAY,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,oBAAoB,EAI3D;IAED,IAAI,YAAY,IAAI,WAAW,CAAC,MAAM,CAAC,CAEtC;IAED,SAAS,CAAC,QAAQ,EAAE,wBAAwB,GAAG,MAAM,IAAI,CAExD;IAEK,MAAM,IAAI,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAwC3C;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAK3B;IAEK,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAE3C;CACF;AAED,eAAO,MAAM,WAAW,GAAI,CAAC,GAAG,OAAO,aAC1B,MAAM,WACR,oBAAoB,qBACc,CAAC"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { DependencyWatcher, } from "./dependency-watcher.js";
|
|
4
|
+
/**
|
|
5
|
+
* Imports fresh Node module generations and watches the exact files loaded by
|
|
6
|
+
* the current generation. Bun callers should use `BunImportTracker` from
|
|
7
|
+
* `./watch-import-bun.ts`: Bun cannot evict evaluated modules, so a change
|
|
8
|
+
* there restarts the process instead of importing a new generation.
|
|
9
|
+
*/
|
|
10
|
+
export class ImportWatcher {
|
|
11
|
+
#specifier;
|
|
12
|
+
#options;
|
|
13
|
+
#watcher;
|
|
14
|
+
#registration;
|
|
15
|
+
#dependencies = new Set();
|
|
16
|
+
#closed = false;
|
|
17
|
+
constructor(specifier, options) {
|
|
18
|
+
this.#specifier = specifier;
|
|
19
|
+
this.#options = options;
|
|
20
|
+
this.#watcher = new DependencyWatcher(options);
|
|
21
|
+
}
|
|
22
|
+
get dependencies() {
|
|
23
|
+
return this.#dependencies;
|
|
24
|
+
}
|
|
25
|
+
subscribe(listener) {
|
|
26
|
+
return this.#watcher.subscribe(listener);
|
|
27
|
+
}
|
|
28
|
+
async import() {
|
|
29
|
+
if (this.#closed)
|
|
30
|
+
throw new Error("ImportWatcher is closed");
|
|
31
|
+
const namespace = randomUUID();
|
|
32
|
+
const dependencies = new Set();
|
|
33
|
+
const { debounceMs: _, parentURL, watch: _watch, ...registerOptions } = this.#options;
|
|
34
|
+
// Loaded here, not at module scope: the exec child imports this file on
|
|
35
|
+
// both runtimes, and the loader's Node hooks do not exist under Bun.
|
|
36
|
+
const { registerOxc } = await import("./register-oxc.js");
|
|
37
|
+
const registration = registerOxc({
|
|
38
|
+
...registerOptions,
|
|
39
|
+
namespace,
|
|
40
|
+
onImport: (url) => {
|
|
41
|
+
if (!url.startsWith("file:"))
|
|
42
|
+
return;
|
|
43
|
+
dependencies.add(fileURLToPath(url));
|
|
44
|
+
// A lazy import evaluated after this generation became current
|
|
45
|
+
// extends the watched set immediately.
|
|
46
|
+
if (this.#dependencies === dependencies)
|
|
47
|
+
this.#watcher.set(dependencies);
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
try {
|
|
51
|
+
const value = await registration.import(this.#specifier, parentURL);
|
|
52
|
+
await this.#registration?.unregister();
|
|
53
|
+
this.#registration = registration;
|
|
54
|
+
this.#dependencies = dependencies;
|
|
55
|
+
this.#watcher.set(dependencies);
|
|
56
|
+
return { value, namespace, dependencies };
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
await registration.unregister();
|
|
60
|
+
// Keep watching everything the failed import touched so the next save
|
|
61
|
+
// of any of those files retries.
|
|
62
|
+
this.#dependencies = new Set([...this.#dependencies, ...dependencies]);
|
|
63
|
+
this.#watcher.set(this.#dependencies);
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async close() {
|
|
68
|
+
if (this.#closed)
|
|
69
|
+
return;
|
|
70
|
+
this.#closed = true;
|
|
71
|
+
await this.#registration?.unregister();
|
|
72
|
+
await this.#watcher.close();
|
|
73
|
+
}
|
|
74
|
+
async [Symbol.asyncDispose]() {
|
|
75
|
+
await this.close();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export const watchImport = (specifier, options) => new ImportWatcher(specifier, options);
|
|
79
|
+
//# sourceMappingURL=watch-import.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watch-import.js","sourceRoot":"","sources":["../src/watch-import.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alchemy.run/node-utils",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.0.0-beta.78",
|
|
4
|
+
"description": "Runtime utilities for Alchemy packages.",
|
|
5
5
|
"homepage": "https://alchemy.run",
|
|
6
6
|
"author": "Sam Goodwin <sam@alchemy.run>",
|
|
7
7
|
"keywords": [
|
|
@@ -33,8 +33,27 @@
|
|
|
33
33
|
"types": "./lib/ignore.d.ts",
|
|
34
34
|
"bun": "./src/ignore.ts",
|
|
35
35
|
"import": "./lib/ignore.js"
|
|
36
|
+
},
|
|
37
|
+
"./register-oxc": {
|
|
38
|
+
"types": "./lib/register-oxc.d.ts",
|
|
39
|
+
"bun": "./src/register-oxc.ts",
|
|
40
|
+
"import": "./lib/register-oxc.js"
|
|
41
|
+
},
|
|
42
|
+
"./watch-import": {
|
|
43
|
+
"types": "./lib/watch-import.d.ts",
|
|
44
|
+
"bun": "./src/watch-import.ts",
|
|
45
|
+
"import": "./lib/watch-import.js"
|
|
46
|
+
},
|
|
47
|
+
"./watch-import-bun": {
|
|
48
|
+
"types": "./lib/watch-import-bun.d.ts",
|
|
49
|
+
"bun": "./src/watch-import-bun.ts",
|
|
50
|
+
"import": "./lib/watch-import-bun.js"
|
|
36
51
|
}
|
|
37
52
|
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"chokidar": "^5.0.0",
|
|
55
|
+
"rolldown": "1.2.5"
|
|
56
|
+
},
|
|
38
57
|
"publishConfig": {
|
|
39
58
|
"access": "public"
|
|
40
59
|
},
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { watch, type ChokidarOptions, type FSWatcher } from "chokidar";
|
|
5
|
+
|
|
6
|
+
export interface DependencyChange {
|
|
7
|
+
readonly paths: ReadonlySet<string>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type DependencyChangeListener = (change: DependencyChange) => void;
|
|
11
|
+
|
|
12
|
+
export interface DependencyWatcherOptions {
|
|
13
|
+
/** Chokidar options passed to the file watcher. */
|
|
14
|
+
readonly watch?: ChokidarOptions | undefined;
|
|
15
|
+
readonly debounceMs?: number | undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Watches an explicit set of absolute file paths and delivers debounced
|
|
20
|
+
* batches only when their contents change. Chokidar follows editor atomic-save
|
|
21
|
+
* replacements without polling by default. The set is replaced wholesale
|
|
22
|
+
* with {@link set}, so the watcher always mirrors exactly the files the caller
|
|
23
|
+
* currently depends on.
|
|
24
|
+
*/
|
|
25
|
+
export class DependencyWatcher {
|
|
26
|
+
readonly #options: DependencyWatcherOptions;
|
|
27
|
+
readonly #listeners = new Set<DependencyChangeListener>();
|
|
28
|
+
readonly #watcher: FSWatcher;
|
|
29
|
+
#dependencies: ReadonlySet<string> = new Set();
|
|
30
|
+
#fingerprints = new Map<string, string | undefined>();
|
|
31
|
+
#pending = new Set<string>();
|
|
32
|
+
#timer: NodeJS.Timeout | undefined;
|
|
33
|
+
#closed = false;
|
|
34
|
+
|
|
35
|
+
constructor(options: DependencyWatcherOptions = {}) {
|
|
36
|
+
this.#options = options;
|
|
37
|
+
this.#watcher = watch([], {
|
|
38
|
+
...options.watch,
|
|
39
|
+
ignoreInitial: true,
|
|
40
|
+
});
|
|
41
|
+
this.#watcher.on("all", (_event, changed) => {
|
|
42
|
+
const absolute = path.resolve(this.#cwd(), changed);
|
|
43
|
+
if (this.#dependencies.has(absolute)) this.#queue(absolute);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get dependencies(): ReadonlySet<string> {
|
|
48
|
+
return this.#dependencies;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
subscribe(listener: DependencyChangeListener): () => void {
|
|
52
|
+
this.#listeners.add(listener);
|
|
53
|
+
return () => this.#listeners.delete(listener);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Replace the watched set with `dependencies` (absolute paths). */
|
|
57
|
+
set(dependencies: ReadonlySet<string>): void {
|
|
58
|
+
if (this.#closed) return;
|
|
59
|
+
const previous = this.#dependencies;
|
|
60
|
+
this.#dependencies = dependencies;
|
|
61
|
+
const removed = [...previous].filter(
|
|
62
|
+
(dependency) => !dependencies.has(dependency),
|
|
63
|
+
);
|
|
64
|
+
const added = [...dependencies].filter(
|
|
65
|
+
(dependency) => !previous.has(dependency),
|
|
66
|
+
);
|
|
67
|
+
if (removed.length > 0) {
|
|
68
|
+
for (const dependency of removed) {
|
|
69
|
+
this.#fingerprints.delete(dependency);
|
|
70
|
+
this.#pending.delete(dependency);
|
|
71
|
+
}
|
|
72
|
+
void this.#watcher.unwatch(removed);
|
|
73
|
+
}
|
|
74
|
+
if (added.length > 0) {
|
|
75
|
+
for (const dependency of added) {
|
|
76
|
+
this.#fingerprints.set(dependency, this.#fingerprint(dependency));
|
|
77
|
+
}
|
|
78
|
+
this.#watcher.add(added);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async close(): Promise<void> {
|
|
83
|
+
if (this.#closed) return;
|
|
84
|
+
this.#closed = true;
|
|
85
|
+
if (this.#timer !== undefined) clearTimeout(this.#timer);
|
|
86
|
+
await this.#watcher.close();
|
|
87
|
+
this.#listeners.clear();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async [Symbol.asyncDispose](): Promise<void> {
|
|
91
|
+
await this.close();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#cwd(): string {
|
|
95
|
+
return this.#options.watch?.cwd ?? process.cwd();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#queue(changed: string): void {
|
|
99
|
+
this.#pending.add(changed);
|
|
100
|
+
if (this.#timer !== undefined) clearTimeout(this.#timer);
|
|
101
|
+
this.#timer = setTimeout(() => {
|
|
102
|
+
this.#timer = undefined;
|
|
103
|
+
const pending = this.#pending;
|
|
104
|
+
this.#pending = new Set();
|
|
105
|
+
const paths = new Set<string>();
|
|
106
|
+
for (const dependency of pending) {
|
|
107
|
+
const previous = this.#fingerprints.get(dependency);
|
|
108
|
+
const current = this.#fingerprint(dependency);
|
|
109
|
+
if (current === previous) continue;
|
|
110
|
+
this.#fingerprints.set(dependency, current);
|
|
111
|
+
paths.add(dependency);
|
|
112
|
+
}
|
|
113
|
+
if (paths.size === 0) return;
|
|
114
|
+
for (const listener of this.#listeners) listener({ paths });
|
|
115
|
+
}, this.#options.debounceMs ?? 50);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#fingerprint(file: string): string | undefined {
|
|
119
|
+
try {
|
|
120
|
+
return createHash("sha256")
|
|
121
|
+
.update(readFileSync(file))
|
|
122
|
+
.digest("base64url");
|
|
123
|
+
} catch {
|
|
124
|
+
// Missing and unreadable are both materially different from the last
|
|
125
|
+
// successfully read contents, while repeated missing-file events fold
|
|
126
|
+
// onto the same value.
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|