Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 126 additions & 13 deletions ext/node/polyfills/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ import { core, primordials } from "ext:core/mod.js";

const {
ArrayBufferIsView,
ArrayIsArray,
BigInt,
DatePrototypeGetTime,
DateUTC,
Expand All @@ -175,6 +176,8 @@ const {
Promise,
PromisePrototypeThen,
PromiseResolve,
RegExpPrototype,
RegExpPrototypeTest,
SafeMap,
StringPrototypeToString,
SymbolAsyncIterator,
Expand Down Expand Up @@ -3129,10 +3132,107 @@ function asyncIterableToCallback<T>(
next();
}

// Mirrors Node's `validateIgnoreOption` /
// `createIgnoreMatcher` from `lib/internal/fs/watchers.js`.
// Accepts a string (minimatch glob), RegExp, function, or array of those.
// Returns a function `(filename) => boolean` (or `null` if `ignore` is nullish).
type IgnoreOption =
| string
| RegExp
| ((filename: string) => boolean)
| (string | RegExp | ((filename: string) => boolean))[]
| undefined
| null;

// deno-lint-ignore no-explicit-any
let _lazyMinimatch: any = null;
function getMinimatch() {
_lazyMinimatch ??= core.createLazyLoader("ext:deno_node/deps/minimatch.js");
return _lazyMinimatch();
}

function validateIgnoreOptionElement(value: unknown, name: string) {
if (typeof value === "string") {
if (value.length === 0) {
throw new ERR_INVALID_ARG_VALUE(
name,
value,
"must be a non-empty string",
);
}
return;
}
if (ObjectPrototypeIsPrototypeOf(RegExpPrototype, value)) return;
if (typeof value === "function") return;
throw new ERR_INVALID_ARG_TYPE(
name,
["string", "RegExp", "Function"],
value,
);
}

function validateIgnoreOption(value: unknown, name: string) {
if (value == null) return;
if (ArrayIsArray(value)) {
for (let i = 0; i < value.length; i++) {
validateIgnoreOptionElement(value[i], `${name}[${i}]`);
}
return;
}
validateIgnoreOptionElement(value, name);
}

function createIgnoreMatcher(
ignore: IgnoreOption,
): ((filename: string) => boolean) | null {
if (ignore == null) return null;
const matchers = ArrayIsArray(ignore) ? ignore : [ignore];
const compiled: Array<(filename: string) => boolean> = [];

for (let i = 0; i < matchers.length; i++) {
const matcher = matchers[i];
if (typeof matcher === "string") {
const { Minimatch } = getMinimatch().default;
const mm = new Minimatch(matcher, {
nocase: isMacOS || isWindows,
windowsPathsNoEscape: true,
nonegate: true,
nocomment: true,
optimizationLevel: 2,
platform: isWindows ? "win32" : "posix",
// Allow patterns without slashes to match the basename
// e.g. '*.log' matches 'subdir/file.log'.
matchBase: true,
});
ArrayPrototypePush(
compiled,
// deno-lint-ignore prefer-primordials
(filename: string) => mm.match(filename),
);
} else if (ObjectPrototypeIsPrototypeOf(RegExpPrototype, matcher)) {
ArrayPrototypePush(
compiled,
(filename: string) => RegExpPrototypeTest(matcher as RegExp, filename),
);
} else {
// Function
ArrayPrototypePush(compiled, matcher as (filename: string) => boolean);
}
}

return (filename: string) => {
for (let i = 0; i < compiled.length; i++) {
if (compiled[i](filename)) return true;
}
return false;
};
}

type watchOptions = {
persistent?: boolean;
recursive?: boolean;
encoding?: string;
ignore?: IgnoreOption;
};

type watchListener = (eventType: string, filename: string) => void;
Expand Down Expand Up @@ -3171,6 +3271,8 @@ function watch(
const watchPath = getValidatedPath(filename).toString();

const recursive = options?.recursive || false;
validateIgnoreOption(options?.ignore, "options.ignore");
const ignoreMatcher = createIgnoreMatcher(options?.ignore);
const iterator: Deno.FsWatcher = Deno.watchFs(watchPath, {
recursive,
});
Expand All @@ -3187,6 +3289,9 @@ function watch(
const filename = recursive
? relative(resolvedWatchPath, val.paths[0])
: basename(val.paths[0]);
if (ignoreMatcher !== null && ignoreMatcher(filename)) {
return;
}
fsWatcher.emit(
"change",
convertDenoFsEventToNodeFsEvent(val.kind),
Expand Down Expand Up @@ -3227,12 +3332,15 @@ function watchPromise(
recursive?: boolean;
encoding?: string;
signal?: AbortSignal;
ignore?: IgnoreOption;
},
): AsyncIterable<{ eventType: string; filename: string | Buffer | null }> {
// deno-lint-ignore prefer-primordials
const watchPath = getValidatedPath(filename).toString();

const recursive = options?.recursive ?? false;
validateIgnoreOption(options?.ignore, "options.ignore");
const ignoreMatcher = createIgnoreMatcher(options?.ignore);
const watcher = Deno.watchFs(watchPath, {
recursive,
});
Expand All @@ -3255,20 +3363,25 @@ function watchPromise(
async next(): Promise<
IteratorResult<{ eventType: string; filename: string | Buffer | null }>
> {
// deno-lint-ignore prefer-primordials
const iterResult = await fsIterable.next();
if (iterResult.done) return iterResult;
while (true) {
// deno-lint-ignore prefer-primordials
const iterResult = await fsIterable.next();
if (iterResult.done) return iterResult;

const eventType = convertDenoFsEventToNodeFsEvent(
iterResult.value.kind,
);
const fname = recursive
? relative(resolvedWatchPath, iterResult.value.paths[0])
: basename(iterResult.value.paths[0]);
return {
value: { eventType, filename: fname },
done: false,
};
const eventType = convertDenoFsEventToNodeFsEvent(
iterResult.value.kind,
);
const fname = recursive
? relative(resolvedWatchPath, iterResult.value.paths[0])
: basename(iterResult.value.paths[0]);
if (ignoreMatcher !== null && ignoreMatcher(fname)) {
continue;
}
return {
value: { eventType, filename: fname },
done: false,
};
}
},
// deno-lint-ignore no-explicit-any
return(value?: any): Promise<IteratorResult<any>> {
Expand Down
14 changes: 14 additions & 0 deletions tests/node_compat/config.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -1089,6 +1089,11 @@
"parallel/test-fs-promises-readfile-empty.js": {},
"parallel/test-fs-promises-readfile-with-fd.js": {},
"parallel/test-fs-promises-statfs-validate-path.js": {},
"parallel/test-fs-promises-watch-ignore-function.mjs": {},
Comment thread
nathanwhitbot marked this conversation as resolved.
"parallel/test-fs-promises-watch-ignore-glob.mjs": {},
"parallel/test-fs-promises-watch-ignore-invalid.mjs": {},
"parallel/test-fs-promises-watch-ignore-mixed.mjs": {},
"parallel/test-fs-promises-watch-ignore-regexp.mjs": {},
"parallel/test-fs-promises-watch-iterator.js": {},
"parallel/test-fs-promises-write-optional-params.js": {},
"parallel/test-fs-promises-writefile-typedarray.js": {},
Expand Down Expand Up @@ -1190,6 +1195,15 @@
"parallel/test-fs-utimes-y2K38.js": {},
"parallel/test-fs-utimes.js": {},
"parallel/test-fs-watch-file-enoent-after-deletion.js": {},
"parallel/test-fs-watch-ignore-function.js": {},
"parallel/test-fs-watch-ignore-glob.js": {},
"parallel/test-fs-watch-ignore-invalid.js": {},
"parallel/test-fs-watch-ignore-mixed.js": {},
"parallel/test-fs-watch-ignore-recursive-glob-subdirectories.js": {},
"parallel/test-fs-watch-ignore-recursive-glob.js": {},
"parallel/test-fs-watch-ignore-recursive-mixed.js": {},
"parallel/test-fs-watch-ignore-recursive-regexp.js": {},
"parallel/test-fs-watch-ignore-regexp.js": {},
"parallel/test-fs-watch-recursive-add-file-to-existing-subfolder.js": {},
"parallel/test-fs-watch-recursive-add-folder.js": {},
"parallel/test-fs-watch-recursive-delete.js": {},
Expand Down
Loading