@dbx-tools/path 0.1.9
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/.projen/deps.json +44 -0
- package/.projen/files.json +11 -0
- package/.projen/tasks.json +121 -0
- package/README.md +119 -0
- package/index.ts +15 -0
- package/package.json +46 -0
- package/src/find.ts +128 -0
- package/src/ignore.ts +189 -0
- package/src/match.ts +69 -0
- package/src/pattern.ts +31 -0
- package/src/scan.ts +35 -0
- package/src/watch.ts +160 -0
- package/test/fixtures/sample-tree/.hidden/secret.ts +1 -0
- package/test/fixtures/sample-tree/example/skip.ts +1 -0
- package/test/fixtures/sample-tree/src/index.ts +1 -0
- package/test/fixtures/sample-tree/src/keep.ts +1 -0
- package/test/match-parity.test.ts +99 -0
- package/test/tsconfig.json +14 -0
- package/tsconfig.json +41 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"dependencies": [
|
|
3
|
+
{
|
|
4
|
+
"name": "@types/node",
|
|
5
|
+
"version": "catalog:",
|
|
6
|
+
"type": "build"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"name": "tsx",
|
|
10
|
+
"version": "^4.23.0",
|
|
11
|
+
"type": "build"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"name": "typescript",
|
|
15
|
+
"type": "build"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"name": "@dbx-tools/core",
|
|
19
|
+
"version": "workspace:*",
|
|
20
|
+
"type": "runtime"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "@dbx-tools/shared-core",
|
|
24
|
+
"version": "workspace:*",
|
|
25
|
+
"type": "runtime"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"name": "chokidar",
|
|
29
|
+
"version": "^4.0.3",
|
|
30
|
+
"type": "runtime"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "glob",
|
|
34
|
+
"version": "^10.5.0",
|
|
35
|
+
"type": "runtime"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"name": "minimatch",
|
|
39
|
+
"version": "^10.2.5",
|
|
40
|
+
"type": "runtime"
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
"//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"pnpm exec projen\"."
|
|
44
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifestVersion": 3,
|
|
3
|
+
"env": {
|
|
4
|
+
"PATH": "$(pnpm -c exec \"node --print process.env.PATH\")"
|
|
5
|
+
},
|
|
6
|
+
"tasks": {
|
|
7
|
+
"build": {
|
|
8
|
+
"name": "build",
|
|
9
|
+
"description": "Full release build",
|
|
10
|
+
"steps": [
|
|
11
|
+
{
|
|
12
|
+
"spawn": "pre-compile"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"spawn": "compile"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"spawn": "post-compile"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"spawn": "test"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"spawn": "package"
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
"compile": {
|
|
29
|
+
"name": "compile",
|
|
30
|
+
"description": "Only compile",
|
|
31
|
+
"steps": [
|
|
32
|
+
{
|
|
33
|
+
"execArgs": [
|
|
34
|
+
"tsc",
|
|
35
|
+
"--build"
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
"default": {
|
|
41
|
+
"name": "default",
|
|
42
|
+
"description": "Synthesize project files",
|
|
43
|
+
"steps": [
|
|
44
|
+
{
|
|
45
|
+
"exec": "pnpm exec projen default",
|
|
46
|
+
"cwd": "../../.."
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
"install": {
|
|
51
|
+
"name": "install",
|
|
52
|
+
"description": "Install project dependencies and update lockfile (non-frozen)",
|
|
53
|
+
"steps": [
|
|
54
|
+
{
|
|
55
|
+
"exec": "pnpm i --no-frozen-lockfile"
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
},
|
|
59
|
+
"install:ci": {
|
|
60
|
+
"name": "install:ci",
|
|
61
|
+
"description": "Install project dependencies using frozen lockfile",
|
|
62
|
+
"steps": [
|
|
63
|
+
{
|
|
64
|
+
"exec": "pnpm i --frozen-lockfile"
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
},
|
|
68
|
+
"package": {
|
|
69
|
+
"name": "package",
|
|
70
|
+
"description": "Creates the distribution package",
|
|
71
|
+
"steps": [
|
|
72
|
+
{
|
|
73
|
+
"execArgs": [
|
|
74
|
+
"mkdir",
|
|
75
|
+
"-p",
|
|
76
|
+
"dist/js"
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"execArgs": [
|
|
81
|
+
"pnpm",
|
|
82
|
+
"pack",
|
|
83
|
+
"--pack-destination",
|
|
84
|
+
"dist/js"
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
},
|
|
89
|
+
"post-compile": {
|
|
90
|
+
"name": "post-compile",
|
|
91
|
+
"description": "Runs after successful compilation"
|
|
92
|
+
},
|
|
93
|
+
"pre-compile": {
|
|
94
|
+
"name": "pre-compile",
|
|
95
|
+
"description": "Prepare the project for compilation"
|
|
96
|
+
},
|
|
97
|
+
"test": {
|
|
98
|
+
"name": "test",
|
|
99
|
+
"description": "Run tests",
|
|
100
|
+
"steps": [
|
|
101
|
+
{
|
|
102
|
+
"exec": "tsx --test 'test/**/*.test.ts'"
|
|
103
|
+
}
|
|
104
|
+
]
|
|
105
|
+
},
|
|
106
|
+
"watch": {
|
|
107
|
+
"name": "watch",
|
|
108
|
+
"description": "Watch & compile in the background",
|
|
109
|
+
"steps": [
|
|
110
|
+
{
|
|
111
|
+
"execArgs": [
|
|
112
|
+
"tsc",
|
|
113
|
+
"--build",
|
|
114
|
+
"-w"
|
|
115
|
+
]
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
"//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"pnpm exec projen\"."
|
|
121
|
+
}
|
package/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# @dbx-tools/node-path
|
|
2
|
+
|
|
3
|
+
Node filesystem path toolkit for discovery, matching, ignoring, scanning, and
|
|
4
|
+
watching.
|
|
5
|
+
|
|
6
|
+
Import this package when Node code needs consistent glob behavior across CLI,
|
|
7
|
+
projen, barrel generation, OpenAPI generation, or docs tooling.
|
|
8
|
+
|
|
9
|
+
Key features:
|
|
10
|
+
|
|
11
|
+
- Lazy glob-backed file discovery that composes with shared-core sequences.
|
|
12
|
+
- Glob-to-predicate matchers for include/exclude logic.
|
|
13
|
+
- Centralized ignore rules for generated files, dependencies, package-manager
|
|
14
|
+
output, VCS folders, and build artifacts.
|
|
15
|
+
- Workspace scan option types shared by synthesis and docs tooling.
|
|
16
|
+
- Chokidar wrapper for watch loops that should follow the same ignore behavior.
|
|
17
|
+
- Escaped glob pattern builders for generated matcher fragments.
|
|
18
|
+
|
|
19
|
+
## Find Files
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { find, ignore } from "@dbx-tools/node-path";
|
|
23
|
+
|
|
24
|
+
const files = find
|
|
25
|
+
.findFiles(["workspaces/**/src/**/*.ts"], {
|
|
26
|
+
ignore: ignore.ignorePatterns(),
|
|
27
|
+
})
|
|
28
|
+
.toArray();
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`find.findFiles()` returns a lazy `Sequence<string>` from
|
|
32
|
+
[`@dbx-tools/shared-core`](../../shared/core), so callers can map/filter without
|
|
33
|
+
materializing immediately.
|
|
34
|
+
|
|
35
|
+
Use this package instead of direct `glob` or `chokidar` calls when the result
|
|
36
|
+
should agree with package discovery, barrel generation, or cleanup logic.
|
|
37
|
+
|
|
38
|
+
## Compile Matchers
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { match } from "@dbx-tools/node-path";
|
|
42
|
+
|
|
43
|
+
const isTest = match.toPathMatcher("**/*.test.ts");
|
|
44
|
+
if (isTest("workspaces/shared/model/test/classify.test.ts")) {
|
|
45
|
+
skip();
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`match.toPathMatcher()` returns a composable shared-core predicate. Use
|
|
50
|
+
`match.pathMatchTests()` when you need to inspect the compiled include/exclude
|
|
51
|
+
tests.
|
|
52
|
+
|
|
53
|
+
## Reuse Ignore Rules
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { ignore } from "@dbx-tools/node-path";
|
|
57
|
+
|
|
58
|
+
const matcher = ignore.ignorePathMatcher({
|
|
59
|
+
generated: true,
|
|
60
|
+
dependencies: true,
|
|
61
|
+
vcs: true,
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The ignore helpers centralize repo-wide exclusions such as `node_modules`,
|
|
66
|
+
generated files, build output, VCS metadata, and package-manager output.
|
|
67
|
+
|
|
68
|
+
## Scan Workspace Packages
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { scan } from "@dbx-tools/node-path";
|
|
72
|
+
|
|
73
|
+
const options: scan.FileScanOptions = {
|
|
74
|
+
roots: ["workspaces", "example-workspaces"],
|
|
75
|
+
followSymlinks: scan.FOLLOW_SYMLINKS_DEFAULT,
|
|
76
|
+
};
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`scan` exports shared scan option types used by the projen engine. Use the same
|
|
80
|
+
options when implementing docs or analysis tools that should walk the workspace
|
|
81
|
+
like synthesis does.
|
|
82
|
+
|
|
83
|
+
## Watch Files
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { watch } from "@dbx-tools/node-path";
|
|
87
|
+
|
|
88
|
+
const watcher = watch.watchFiles(["workspaces/**/src/**/*.ts"], {
|
|
89
|
+
ignoreInitial: true,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
watcher.on("change", (file) => rebuild(file));
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`watch.watchFiles()` wraps chokidar with the same path and ignore expectations
|
|
96
|
+
used by the rest of the repo tooling.
|
|
97
|
+
|
|
98
|
+
## Build Patterns
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import { pattern } from "@dbx-tools/node-path";
|
|
102
|
+
|
|
103
|
+
const nodeModulesPattern = pattern.directoryNamePattern("node_modules");
|
|
104
|
+
const tsPattern = pattern.fileExtensionPattern("ts");
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Pattern helpers keep generated glob fragments escaped and consistent.
|
|
108
|
+
|
|
109
|
+
## Modules
|
|
110
|
+
|
|
111
|
+
- `find` - glob-backed lazy file finding.
|
|
112
|
+
- `match` - glob-to-predicate path matchers.
|
|
113
|
+
- `ignore` - standard ignore pattern and matcher construction.
|
|
114
|
+
- `scan` - workspace package scan option types and defaults.
|
|
115
|
+
- `watch` - chokidar wrapper for file watching.
|
|
116
|
+
- `pattern` - escaped directory-name and extension glob fragments.
|
|
117
|
+
|
|
118
|
+
The projen engine uses this package in
|
|
119
|
+
[`@dbx-tools/projen`](../projen).
|
package/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// GENERATED by projen watch - DO NOT EDIT.
|
|
2
|
+
// Regenerated from the exporting modules in ./src.
|
|
3
|
+
// Hand edits are overwritten on the next watch; this file is read-only.
|
|
4
|
+
|
|
5
|
+
export * as find from "./src/find";
|
|
6
|
+
export * as ignore from "./src/ignore";
|
|
7
|
+
export * as match from "./src/match";
|
|
8
|
+
export * as pattern from "./src/pattern";
|
|
9
|
+
export * as scan from "./src/scan";
|
|
10
|
+
export * as watch from "./src/watch";
|
|
11
|
+
export type { FileFindOptions } from "./src/find";
|
|
12
|
+
export type { IgnorePatternOptions } from "./src/ignore";
|
|
13
|
+
export type { PathMatchPredicate, PathMatcher, PathMatchInput } from "./src/match";
|
|
14
|
+
export type { FileScanOptions, FileScanIgnoreOptions } from "./src/scan";
|
|
15
|
+
export type { FileWatchOptions } from "./src/watch";
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dbx-tools/path",
|
|
3
|
+
"repository": {
|
|
4
|
+
"type": "git",
|
|
5
|
+
"url": "git+https://github.com/reggie-db/dbx-tools.git",
|
|
6
|
+
"directory": "workspaces/node/path"
|
|
7
|
+
},
|
|
8
|
+
"devDependencies": {
|
|
9
|
+
"@types/node": "^24.6.0",
|
|
10
|
+
"tsx": "^4.23.0",
|
|
11
|
+
"typescript": "^5.9.3"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"chokidar": "^4.0.3",
|
|
15
|
+
"glob": "^10.5.0",
|
|
16
|
+
"minimatch": "^10.2.5",
|
|
17
|
+
"@dbx-tools/shared-core": "0.1.9",
|
|
18
|
+
"@dbx-tools/core": "0.1.9"
|
|
19
|
+
},
|
|
20
|
+
"main": "index.ts",
|
|
21
|
+
"license": "UNLICENSED",
|
|
22
|
+
"version": "0.1.9",
|
|
23
|
+
"types": "index.ts",
|
|
24
|
+
"type": "module",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": "./index.ts",
|
|
27
|
+
"./package.json": "./package.json"
|
|
28
|
+
},
|
|
29
|
+
"dbxToolsConfig": {
|
|
30
|
+
"tags": [
|
|
31
|
+
"node"
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
"//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"pnpm exec projen\".",
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "projen build",
|
|
37
|
+
"compile": "projen compile",
|
|
38
|
+
"default": "projen default",
|
|
39
|
+
"package": "projen package",
|
|
40
|
+
"post-compile": "projen post-compile",
|
|
41
|
+
"pre-compile": "projen pre-compile",
|
|
42
|
+
"test": "projen test",
|
|
43
|
+
"watch": "projen watch",
|
|
44
|
+
"projen": "projen"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/find.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { iterable, type Sequence } from "@dbx-tools/shared-core";
|
|
2
|
+
import { globIterateSync, IgnoreLike, type GlobOptionsWithFileTypesUnset } from "glob";
|
|
3
|
+
import { ignorePathMatcher } from "./ignore";
|
|
4
|
+
import { PathMatcher, PathMatchInput, pathMatchTests, toPathMatcher } from "./match";
|
|
5
|
+
import { FileScanIgnoreOptions, FileScanOptions, FOLLOW_SYMLINKS_DEFAULT } from "./scan";
|
|
6
|
+
|
|
7
|
+
type FileFindIgnore = PathMatchInput | readonly PathMatchInput[] | IgnoreLike;
|
|
8
|
+
|
|
9
|
+
export interface FileFindOptions
|
|
10
|
+
extends
|
|
11
|
+
Omit<
|
|
12
|
+
GlobOptionsWithFileTypesUnset,
|
|
13
|
+
"ignore" | "follow" | "dot" | "cwd" | "includeChildMatches"
|
|
14
|
+
>,
|
|
15
|
+
Omit<FileScanOptions, "ignore" | "cwd"> {
|
|
16
|
+
cwd?: string | URL;
|
|
17
|
+
ignore?: FileFindIgnore;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Recursively lists files matching `pattern`, ignoring the shared default groups
|
|
21
|
+
* plus any caller `ignore` patterns (unless `ignore` is a lone predicate function,
|
|
22
|
+
* in which case only that predicate applies). The ignore list mirrors what
|
|
23
|
+
* {@link watchFiles} feeds through its matchers.
|
|
24
|
+
*/
|
|
25
|
+
export function findFiles(pattern: string | string[], options?: FileFindOptions): Sequence<string> {
|
|
26
|
+
return iterable.sequence(globIterateSync(pattern, toGlobOptions(options)));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function toGlobOptions(options: FileFindOptions | undefined): GlobOptionsWithFileTypesUnset {
|
|
30
|
+
const { cwd, ignore, ignoreOptions, followSymlinks, ...restOptions } = options ?? {};
|
|
31
|
+
return {
|
|
32
|
+
...restOptions,
|
|
33
|
+
cwd: cwd ?? process.cwd(),
|
|
34
|
+
follow: followSymlinks ?? FOLLOW_SYMLINKS_DEFAULT,
|
|
35
|
+
ignore: normalizeIgnore(ignore, ignoreOptions),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const IGNORE_LIKE_FIELDS = [
|
|
40
|
+
"ignored",
|
|
41
|
+
"childrenIgnored",
|
|
42
|
+
"add",
|
|
43
|
+
] as const satisfies readonly (keyof IgnoreLike)[];
|
|
44
|
+
|
|
45
|
+
function isIgnoreLike(value: unknown): value is IgnoreLike {
|
|
46
|
+
if (Array.isArray(value)) return false;
|
|
47
|
+
if (typeof value === "object" && value !== null) {
|
|
48
|
+
for (const field of IGNORE_LIKE_FIELDS) {
|
|
49
|
+
if (field in value && typeof (value as any)[field] !== "function") {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Builds the {@link IgnoreLike} object {@link findFiles} hands to `glob`.
|
|
59
|
+
*
|
|
60
|
+
* A caller-supplied `IgnoreLike` (an object, not string/array) is consulted
|
|
61
|
+
* first - its `ignored`/`childrenIgnored` win - after which the shared
|
|
62
|
+
* {@link ignorePathMatcher} (the same matcher {@link watchFiles} uses) decides based on the
|
|
63
|
+
* package-relative path. `cacheDirectoryStats` is enabled because glob probes
|
|
64
|
+
* many paths per directory. Caller string/array patterns are merged into the
|
|
65
|
+
* matcher, and `add` forwards to both the caller object and the matcher.
|
|
66
|
+
*/
|
|
67
|
+
function normalizeIgnore(
|
|
68
|
+
ignore: FileFindIgnore | undefined,
|
|
69
|
+
ignoreOptions: FileScanIgnoreOptions | undefined,
|
|
70
|
+
): IgnoreLike {
|
|
71
|
+
const ignoreLike: IgnoreLike | undefined = isIgnoreLike(ignore) ? ignore : undefined;
|
|
72
|
+
const ignorePathMatcherInputs: PathMatchInput[] | undefined =
|
|
73
|
+
ignore === undefined || ignoreLike ? undefined : Array.isArray(ignore) ? [...ignore] : [ignore];
|
|
74
|
+
|
|
75
|
+
const predicateOnly =
|
|
76
|
+
ignorePathMatcherInputs?.length === 1 && typeof ignorePathMatcherInputs[0] === "function";
|
|
77
|
+
|
|
78
|
+
let ignoreMatcher: PathMatcher = predicateOnly
|
|
79
|
+
? toPathMatcher(ignorePathMatcherInputs[0])
|
|
80
|
+
: ignorePathMatcher(ignoreOptions);
|
|
81
|
+
if (ignorePathMatcherInputs && !predicateOnly) {
|
|
82
|
+
ignoreMatcher = ignoreMatcher.or(...pathMatchTests(...ignorePathMatcherInputs));
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
ignored(path) {
|
|
86
|
+
if (ignoreLike?.ignored?.(path)) return true;
|
|
87
|
+
const patheRelative = path.relative();
|
|
88
|
+
const ignore = ignoreMatcher(patheRelative);
|
|
89
|
+
return ignore;
|
|
90
|
+
},
|
|
91
|
+
childrenIgnored(path) {
|
|
92
|
+
if (ignoreLike?.childrenIgnored?.(path)) return true;
|
|
93
|
+
const patheRelative = path.relative();
|
|
94
|
+
if (patheRelative) {
|
|
95
|
+
if (ignoreMatcher(patheRelative)) {
|
|
96
|
+
return true;
|
|
97
|
+
} else if (ignoreMatcher(patheRelative + "/")) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
},
|
|
103
|
+
add(ignore) {
|
|
104
|
+
ignoreLike?.add?.(ignore);
|
|
105
|
+
ignoreMatcher = ignoreMatcher.and(...pathMatchTests(ignore));
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Manual demo: run this file directly (e.g. `tsx src/find.ts`) to print the
|
|
111
|
+
// files findFiles keeps for this package under the given ignore options.
|
|
112
|
+
if (import.meta.main) {
|
|
113
|
+
const startTime = performance.now();
|
|
114
|
+
const cwd = process.cwd();
|
|
115
|
+
console.log(`Scanning: ${cwd}`);
|
|
116
|
+
|
|
117
|
+
const files = findFiles("**/*.*", {
|
|
118
|
+
ignore: ["**/index.ts", "**/example/**", "**/*.md"],
|
|
119
|
+
ignoreOptions: {
|
|
120
|
+
test: true,
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
for (const file of files) {
|
|
124
|
+
console.log(file);
|
|
125
|
+
}
|
|
126
|
+
const elapsed = performance.now() - startTime;
|
|
127
|
+
console.log(`Elapsed: ${elapsed}ms`);
|
|
128
|
+
}
|
package/src/ignore.ts
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in ignore glob patterns for file scanning and watching.
|
|
3
|
+
*
|
|
4
|
+
* This module owns the pattern catalog and option toggles only. Each group is
|
|
5
|
+
* compiled into {@link PathMatchPredicate} instances via {@link toPathMatcher};
|
|
6
|
+
* composition (`and`/`or`/`negate`) lives in {@link ./match}.
|
|
7
|
+
*
|
|
8
|
+
* Consumers:
|
|
9
|
+
* - {@link ignorePatterns} - yields the enabled glob strings (for projen prettier/gitignore).
|
|
10
|
+
* - {@link ignorePathMatcher} - returns a single {@link PathMatcher} for {@link findFiles}
|
|
11
|
+
* and {@link watchFiles}.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { exec } from "@dbx-tools/core";
|
|
15
|
+
import { functionModule, iterable, type Sequence } from "@dbx-tools/shared-core";
|
|
16
|
+
import { PathMatcher, PathMatchPredicate, toPathMatcher } from "./match";
|
|
17
|
+
import { directoryNamePattern, fileExtensionPattern } from "./pattern";
|
|
18
|
+
|
|
19
|
+
/** Toggles for each built-in ignore group; omitted flags default to `true`. */
|
|
20
|
+
export type IgnorePatternOptions = {
|
|
21
|
+
/** Build artifacts, dependency dirs, caches, logs, and OS junk files. */
|
|
22
|
+
defaults?: boolean;
|
|
23
|
+
/** Hidden dot-files and dot-directories (`.git`, `.vscode`, `.gitignore`, etc.). */
|
|
24
|
+
dot?: boolean;
|
|
25
|
+
/** Temporary directories (`tmp`, `.tmp`, `scratch`, etc.). */
|
|
26
|
+
temp?: boolean;
|
|
27
|
+
/** Test directories and `*.test.*` / `*.spec.*` naming conventions. */
|
|
28
|
+
test?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Lockfile ignore group (`package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`,
|
|
31
|
+
* `bun.lockb`, etc.).
|
|
32
|
+
*
|
|
33
|
+
* - `true` - always ignore lockfiles.
|
|
34
|
+
* - `false` - never ignore lockfiles.
|
|
35
|
+
* - `"auto"` (default) - ignore when the active npm registry is not the public
|
|
36
|
+
* npm registry (`registry.npmjs.org`), e.g. a company-internal registry.
|
|
37
|
+
*/
|
|
38
|
+
lock?: boolean | "auto";
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Memoized probe for {@link IgnorePatternOptions.lock} `"auto"`.
|
|
43
|
+
*
|
|
44
|
+
* Runs `npm`/`pnpm`/`yarn config get registry` (whichever succeeds first) and
|
|
45
|
+
* returns `true` when the registry host is not `registry.npmjs.org`.
|
|
46
|
+
*/
|
|
47
|
+
const lockIgnoreMatchersAutoEnabled = functionModule.memoize(() => {
|
|
48
|
+
let registryUrl: URL | undefined;
|
|
49
|
+
for (const command of ["npm", "pnpm", "yarn"]) {
|
|
50
|
+
const result = exec.spawnSync(command, ["config", "get", "registry"], {
|
|
51
|
+
stdout: "capture",
|
|
52
|
+
stderr: "ignore",
|
|
53
|
+
stdin: "ignore",
|
|
54
|
+
});
|
|
55
|
+
if (result.exitCode !== 0) continue;
|
|
56
|
+
const output = result.stdout;
|
|
57
|
+
if (output && output.includes("://")) {
|
|
58
|
+
const url = new URL(output);
|
|
59
|
+
if (url.protocol && url.hostname) {
|
|
60
|
+
registryUrl = url;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (registryUrl && registryUrl.hostname !== "registry.npmjs.org") {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
/** Resolves whether the lockfile ignore group is enabled for the given options. */
|
|
72
|
+
function lockIgnoreMatchersEnabled(options?: IgnorePatternOptions): boolean {
|
|
73
|
+
const value = options?.lock ?? "auto";
|
|
74
|
+
if (value === "auto") {
|
|
75
|
+
return lockIgnoreMatchersAutoEnabled();
|
|
76
|
+
}
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Compiles glob strings into a `{ [glob]: PathMatchPredicate }` map.
|
|
82
|
+
*
|
|
83
|
+
* Keying by the source glob de-dupes and keeps each matcher beside its pattern.
|
|
84
|
+
*/
|
|
85
|
+
function compileMatchers(patterns: readonly string[]): Record<string, PathMatchPredicate> {
|
|
86
|
+
return Object.fromEntries(patterns.map((glob) => [glob, toPathMatcher(glob)]));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Build artifacts, dependency directories, caches, logs, and OS files. */
|
|
90
|
+
const defaultIgnoreMatchers = compileMatchers([
|
|
91
|
+
"**/.DS_Store",
|
|
92
|
+
"**/Thumbs.db",
|
|
93
|
+
"**/Desktop.ini",
|
|
94
|
+
...["log", "tsbuildinfo"].map((ext) => fileExtensionPattern(ext)),
|
|
95
|
+
"**/*-debug.log*",
|
|
96
|
+
"**/yarn-error.log*",
|
|
97
|
+
...[
|
|
98
|
+
"node_modules",
|
|
99
|
+
"bower_components",
|
|
100
|
+
"jspm_packages",
|
|
101
|
+
"dist",
|
|
102
|
+
"lib",
|
|
103
|
+
"build",
|
|
104
|
+
"out",
|
|
105
|
+
"coverage",
|
|
106
|
+
".cache",
|
|
107
|
+
".parcel-cache",
|
|
108
|
+
".turbo",
|
|
109
|
+
".next",
|
|
110
|
+
".nuxt",
|
|
111
|
+
".svelte-kit",
|
|
112
|
+
".pnpm-store",
|
|
113
|
+
".nyc_output",
|
|
114
|
+
].map((name) => directoryNamePattern(name)),
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
/** Hidden dot-files and dot-directories such as `.git`, `.vscode`, `.idea`, etc. */
|
|
118
|
+
const dotIgnoreMatchers = compileMatchers(
|
|
119
|
+
[".*"].flatMap((glob) => [`**/${glob}`, directoryNamePattern(glob, false)]),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
/** Temporary directories, including hidden variants (`.tmp`, etc.). */
|
|
123
|
+
const tempIgnoreMatchers = compileMatchers(
|
|
124
|
+
["temp", "tmp", "temps", "scratch"]
|
|
125
|
+
.flatMap((name) => [name, `.${name}`])
|
|
126
|
+
.map((name) => directoryNamePattern(name)),
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
/** Test directories and common test-file naming conventions. */
|
|
130
|
+
const testIgnoreMatchers = compileMatchers([
|
|
131
|
+
...["test", "tests", "__tests__", "__snapshots__"]
|
|
132
|
+
.flatMap((name) => [name, `.${name}`])
|
|
133
|
+
.map((name) => directoryNamePattern(name)),
|
|
134
|
+
"**/*.test.*",
|
|
135
|
+
"**/*.spec.*",
|
|
136
|
+
"**/test.*",
|
|
137
|
+
"**/spec.*",
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
/** Lockfiles (`package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`, `bun.lockb`, …). */
|
|
141
|
+
const lockIgnoreMatchers = compileMatchers([
|
|
142
|
+
"**/bun.lockb",
|
|
143
|
+
...["json", "yaml", "yml"].map((ext) => `**/*-lock.${ext}`),
|
|
144
|
+
...["lock"].map((ext) => fileExtensionPattern(ext)),
|
|
145
|
+
]);
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Collects the pre-compiled matcher maps for each enabled built-in group.
|
|
149
|
+
*
|
|
150
|
+
* Group toggles come from `options`; the lock group also respects
|
|
151
|
+
* {@link lockIgnoreMatchersEnabled} (`lock: "auto"` probes the npm registry).
|
|
152
|
+
*/
|
|
153
|
+
function ignoreMatchPredicates(
|
|
154
|
+
options?: IgnorePatternOptions,
|
|
155
|
+
): Record<string, PathMatchPredicate>[] {
|
|
156
|
+
const ignoreMatchRecords: Record<string, PathMatchPredicate>[] = [];
|
|
157
|
+
if (options?.defaults ?? true) ignoreMatchRecords.push(defaultIgnoreMatchers);
|
|
158
|
+
if (options?.temp ?? true) ignoreMatchRecords.push(tempIgnoreMatchers);
|
|
159
|
+
if (options?.test ?? true) ignoreMatchRecords.push(testIgnoreMatchers);
|
|
160
|
+
if (options?.dot ?? true) ignoreMatchRecords.push(dotIgnoreMatchers);
|
|
161
|
+
if (lockIgnoreMatchersEnabled(options)) ignoreMatchRecords.push(lockIgnoreMatchers);
|
|
162
|
+
return ignoreMatchRecords;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Yields the glob strings for each enabled built-in ignore group.
|
|
167
|
+
*
|
|
168
|
+
* Useful when a consumer needs literal patterns (e.g. projen `.gitignore` /
|
|
169
|
+
* `.prettierignore`) rather than a compiled {@link PathMatcher}.
|
|
170
|
+
*
|
|
171
|
+
* @param options - Group toggles; omitted flags default to enabled.
|
|
172
|
+
*/
|
|
173
|
+
export function ignorePatterns(options?: IgnorePatternOptions): Sequence<string> {
|
|
174
|
+
return iterable.sequence(ignoreMatchPredicates(options)).flatMap(Object.keys).distinct();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Returns a {@link PathMatcher} for paths ignored by the enabled built-in groups.
|
|
179
|
+
*
|
|
180
|
+
* Matchers for each group are compiled once at module load; `options` only
|
|
181
|
+
* selects which groups participate. The lock group is included when
|
|
182
|
+
* {@link lockIgnoreMatchersEnabled} returns `true`.
|
|
183
|
+
*
|
|
184
|
+
* @param options - Group toggles; omitted flags default to enabled.
|
|
185
|
+
*/
|
|
186
|
+
export function ignorePathMatcher(options?: IgnorePatternOptions): PathMatcher {
|
|
187
|
+
const predicates = ignoreMatchPredicates(options).flatMap(Object.values);
|
|
188
|
+
return toPathMatcher(...predicates);
|
|
189
|
+
}
|
package/src/match.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path matching and predicate composition for file scanning and watching.
|
|
3
|
+
*
|
|
4
|
+
* {@link toPathMatcher} compiles globs into {@link Predicate} values from
|
|
5
|
+
* shared-core (`and` / `or` / `negate`) for use by {@link findFiles} and
|
|
6
|
+
* {@link watchFiles}.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
iterable,
|
|
11
|
+
predicate,
|
|
12
|
+
type Predicate,
|
|
13
|
+
type PredicateFunction,
|
|
14
|
+
} from "@dbx-tools/shared-core";
|
|
15
|
+
import { Minimatch } from "minimatch";
|
|
16
|
+
import { ignorePathMatcher } from "./ignore";
|
|
17
|
+
|
|
18
|
+
export type PathMatchPredicate = PredicateFunction<string>;
|
|
19
|
+
|
|
20
|
+
/** A composable path test (`true` == match). */
|
|
21
|
+
export type PathMatcher = Predicate<string>;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Anything {@link toPathMatcher} accepts: a glob string (compiled with
|
|
25
|
+
* `dot: true`), or a custom `(path) => boolean`.
|
|
26
|
+
*/
|
|
27
|
+
export type PathMatchInput = PathMatchPredicate | string;
|
|
28
|
+
|
|
29
|
+
/** Normalizes a {@link PathMatchInput} to a plain path test. */
|
|
30
|
+
function toTest(pattern: PathMatchInput): PathMatchPredicate {
|
|
31
|
+
if (typeof pattern === "string") {
|
|
32
|
+
const matcher = new Minimatch(pattern, { dot: true });
|
|
33
|
+
return (path) => matcher.match(path);
|
|
34
|
+
}
|
|
35
|
+
return pattern;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Compiles path match inputs to predicate functions for `.and()` / `.or()`. */
|
|
39
|
+
export function pathMatchTests(
|
|
40
|
+
...inputs: readonly (PathMatchInput | null | undefined)[]
|
|
41
|
+
): readonly PathMatchPredicate[] {
|
|
42
|
+
return iterable.sequence(inputs).nonNull().map(toTest).toArray();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Combines one or more patterns into a single {@link PathMatcher} (OR'd).
|
|
47
|
+
* Strings are compiled with `dot: true`; predicates are used as-is;
|
|
48
|
+
* `null`/`undefined` entries are skipped. With no matching patterns the result
|
|
49
|
+
* always returns `false`.
|
|
50
|
+
*
|
|
51
|
+
* @param patterns - Globs and/or predicates to OR together.
|
|
52
|
+
*/
|
|
53
|
+
export function toPathMatcher(
|
|
54
|
+
...inputs: readonly (PathMatchInput | null | undefined)[]
|
|
55
|
+
): PathMatcher {
|
|
56
|
+
const tests = pathMatchTests(...inputs);
|
|
57
|
+
if (tests.length === 0) return predicate.create(() => false);
|
|
58
|
+
return tests
|
|
59
|
+
.slice(1)
|
|
60
|
+
.reduce<PathMatcher>((acc, test) => acc.or(test), predicate.create(tests[0]!));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (import.meta.main) {
|
|
64
|
+
const matcher = ignorePathMatcher({ test: true })
|
|
65
|
+
.negate()
|
|
66
|
+
.and(...pathMatchTests("**/cool.ts", "**/wow.ts"));
|
|
67
|
+
console.log(matcher("workspaces/node/file-scan/src/cool.ts"));
|
|
68
|
+
console.log(matcher("workspaces/node/file-scan/.src/cool.ts"));
|
|
69
|
+
}
|
package/src/pattern.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Glob pattern builders shared by ignore groups and matchers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Glob metacharacters; escaped so a directory or file name is matched literally. */
|
|
6
|
+
const ESCAPE_GLOB_REGEXP = /([*?[\]{}()!+@\\])/g;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Returns a glob matching all descendants of directories named `name`.
|
|
10
|
+
*
|
|
11
|
+
* By default, glob metacharacters in `name` are escaped so it is treated
|
|
12
|
+
* literally. Pass `false` for `escape` when `name` intentionally contains glob
|
|
13
|
+
* syntax.
|
|
14
|
+
*
|
|
15
|
+
* @param name - Directory base name (e.g. `node_modules`).
|
|
16
|
+
* @param escape - When `true` (default), escape glob metacharacters in `name`.
|
|
17
|
+
*/
|
|
18
|
+
export function directoryNamePattern(name: string, escape = true): string {
|
|
19
|
+
return `**/${escape ? name.replace(ESCAPE_GLOB_REGEXP, "\\$1") : name}/**`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Returns a glob matching files with extension `extension` at any depth.
|
|
24
|
+
*
|
|
25
|
+
* @param extension - Extension with or without a leading dot (e.g. `log` or `.log`).
|
|
26
|
+
* @param escape - When `true` (default), escape glob metacharacters in `extension`.
|
|
27
|
+
*/
|
|
28
|
+
export function fileExtensionPattern(extension: string, escape = true): string {
|
|
29
|
+
const bare = extension.startsWith(".") ? extension.slice(1) : extension;
|
|
30
|
+
return `**/*.${escape ? bare.replace(ESCAPE_GLOB_REGEXP, "\\$1") : bare}`;
|
|
31
|
+
}
|
package/src/scan.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { IgnorePatternOptions } from "./ignore";
|
|
2
|
+
import { PathMatchInput } from "./match";
|
|
3
|
+
|
|
4
|
+
export const FOLLOW_SYMLINKS_DEFAULT = false;
|
|
5
|
+
|
|
6
|
+
export interface FileScanOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Base directory used to resolve relative paths.
|
|
9
|
+
*/
|
|
10
|
+
cwd?: string;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Ignore files or directories matching these patterns or predicates.
|
|
14
|
+
*/
|
|
15
|
+
ignore?: PathMatchInput | readonly PathMatchInput[];
|
|
16
|
+
ignoreOptions?: FileScanIgnoreOptions;
|
|
17
|
+
/**
|
|
18
|
+
* Follow symbolic links.
|
|
19
|
+
*
|
|
20
|
+
* Maps to:
|
|
21
|
+
* glob: follow
|
|
22
|
+
* chokidar: followSymlinks
|
|
23
|
+
*/
|
|
24
|
+
followSymlinks?: boolean;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Abort an in-progress operation.
|
|
28
|
+
*
|
|
29
|
+
* Ignored by chokidar after the watcher has been created.
|
|
30
|
+
*/
|
|
31
|
+
signal?: AbortSignal;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Options controlling the generated ignore pattern list. */
|
|
35
|
+
export type FileScanIgnoreOptions = IgnorePatternOptions;
|
package/src/watch.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { Stats } from "fs";
|
|
2
|
+
import { isAbsolute, relative, resolve } from "path";
|
|
3
|
+
import { ChokidarOptions, MatchFunction, watch } from "chokidar";
|
|
4
|
+
import { hasMagic } from "glob";
|
|
5
|
+
import { findFiles, type FileFindOptions } from "./find";
|
|
6
|
+
import { ignorePathMatcher } from "./ignore";
|
|
7
|
+
import { pathMatchTests } from "./match";
|
|
8
|
+
import { FileScanIgnoreOptions, FileScanOptions, FOLLOW_SYMLINKS_DEFAULT } from "./scan";
|
|
9
|
+
|
|
10
|
+
export interface FileWatchOptions
|
|
11
|
+
extends Omit<ChokidarOptions, "ignored">, Omit<FileScanOptions, "ignore"> {
|
|
12
|
+
ignore?: string | string[] | MatchFunction | MatchFunction[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface NormalizedFileWatchOptions extends Omit<FileWatchOptions, "ignore" | "ignoreOptions"> {
|
|
16
|
+
ignored: MatchFunction;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Watches files and directories for real-time changes, applying the shared
|
|
21
|
+
* ignore groups so it prunes the same paths {@link findFiles} skips for the
|
|
22
|
+
* same patterns.
|
|
23
|
+
*/
|
|
24
|
+
export function watchFiles(paths: string | string[], options?: FileWatchOptions) {
|
|
25
|
+
const normalized = toNormalizedFileWatchOptions(options);
|
|
26
|
+
const { ignored, cwd, followSymlinks, ...chokidarOptions } = normalized;
|
|
27
|
+
const { paths: literals, globs } = partitionPaths(paths);
|
|
28
|
+
|
|
29
|
+
let watchPaths: string[];
|
|
30
|
+
if (globs.length > 0) {
|
|
31
|
+
const distinctWatchPaths = new Set(literals);
|
|
32
|
+
for (const path of findFiles(globs, toFindFilesOptions(normalized))) {
|
|
33
|
+
distinctWatchPaths.add(path);
|
|
34
|
+
}
|
|
35
|
+
watchPaths = [...distinctWatchPaths];
|
|
36
|
+
} else {
|
|
37
|
+
watchPaths = literals;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return watch(watchPaths, {
|
|
41
|
+
...chokidarOptions,
|
|
42
|
+
cwd,
|
|
43
|
+
followSymlinks,
|
|
44
|
+
ignored,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function partitionPaths(paths: string | string[]): { paths: string[]; globs: string[] } {
|
|
49
|
+
const globs: string[] = [];
|
|
50
|
+
const literals: string[] = [];
|
|
51
|
+
for (const path of Array.isArray(paths) ? paths : [paths]) {
|
|
52
|
+
(hasMagic(path) ? globs : literals).push(path);
|
|
53
|
+
}
|
|
54
|
+
return { paths: literals, globs };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Builds {@link NormalizedFileWatchOptions} first: all caller ignore inputs and
|
|
59
|
+
* built-in groups are consolidated into one chokidar {@link MatchFunction}. When
|
|
60
|
+
* `stats` marks a directory, the matcher also tests `path + "/"` for pruning.
|
|
61
|
+
*/
|
|
62
|
+
function toNormalizedFileWatchOptions(options?: FileWatchOptions): NormalizedFileWatchOptions & {
|
|
63
|
+
cwd: string;
|
|
64
|
+
followSymlinks: boolean;
|
|
65
|
+
} {
|
|
66
|
+
const { cwd, ignore, followSymlinks, ignoreOptions, ...chokidarOptions } = options ?? {};
|
|
67
|
+
return {
|
|
68
|
+
...chokidarOptions,
|
|
69
|
+
cwd: cwd ?? process.cwd(),
|
|
70
|
+
followSymlinks: followSymlinks ?? FOLLOW_SYMLINKS_DEFAULT,
|
|
71
|
+
ignored: toWatchMatchFunction(ignore, ignoreOptions, cwd ?? process.cwd()),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Glob expansion reuses the normalized ignore matcher as a cwd-relative path predicate. */
|
|
76
|
+
function toFindFilesOptions(
|
|
77
|
+
normalized: NormalizedFileWatchOptions & { cwd: string; followSymlinks: boolean },
|
|
78
|
+
): FileFindOptions {
|
|
79
|
+
const { ignored, cwd, followSymlinks } = normalized;
|
|
80
|
+
return {
|
|
81
|
+
cwd,
|
|
82
|
+
followSymlinks,
|
|
83
|
+
ignore: (path) => ignored(path, undefined),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Consolidates built-in ignore groups, caller glob patterns, and caller
|
|
89
|
+
* {@link MatchFunction}s into one ignore test. Directory pruning uses `stats`
|
|
90
|
+
* when chokidar supplies it; glob expansion passes `undefined`.
|
|
91
|
+
*/
|
|
92
|
+
function toWatchMatchFunction(
|
|
93
|
+
ignore: FileWatchOptions["ignore"] | undefined,
|
|
94
|
+
ignoreOptions: FileScanIgnoreOptions | undefined,
|
|
95
|
+
cwd: string,
|
|
96
|
+
): MatchFunction {
|
|
97
|
+
const patterns: string[] = [];
|
|
98
|
+
const functions: MatchFunction[] = [];
|
|
99
|
+
|
|
100
|
+
if (ignore !== undefined) {
|
|
101
|
+
for (const item of Array.isArray(ignore) ? ignore : [ignore]) {
|
|
102
|
+
if (typeof item === "string") patterns.push(item);
|
|
103
|
+
else functions.push(item);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let matcher = ignorePathMatcher(ignoreOptions);
|
|
108
|
+
if (patterns.length > 0) matcher = matcher.or(...pathMatchTests(...patterns));
|
|
109
|
+
|
|
110
|
+
const matchPath = (path: string, stats?: Stats): boolean => {
|
|
111
|
+
const rel = toCwdRelativePath(path, cwd);
|
|
112
|
+
if (matcher(rel)) return true;
|
|
113
|
+
for (const fn of functions) {
|
|
114
|
+
if (fn(rel, stats)) return true;
|
|
115
|
+
}
|
|
116
|
+
if (stats?.isDirectory()) {
|
|
117
|
+
const directoryPath = rel + "/";
|
|
118
|
+
if (matcher(directoryPath)) return true;
|
|
119
|
+
for (const fn of functions) {
|
|
120
|
+
if (fn(directoryPath, stats)) return true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
return matchPath;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Aligns chokidar paths with the cwd-relative form {@link findFiles} uses. */
|
|
130
|
+
function toCwdRelativePath(path: string, cwd: string): string {
|
|
131
|
+
if (!isAbsolute(path)) return path;
|
|
132
|
+
const rel = relative(cwd, path);
|
|
133
|
+
return rel === "" ? "." : rel;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Manual demo: run this file directly (e.g. `tsx src/watch.ts`) to watch this
|
|
137
|
+
// package and log every file event as it happens.
|
|
138
|
+
if (import.meta.main) {
|
|
139
|
+
const dir = import.meta.dirname;
|
|
140
|
+
console.log("--- Starting File Watcher ---");
|
|
141
|
+
let cwd = resolve(dir, "..");
|
|
142
|
+
cwd = "/Users/reggie.pierce/Projects/github-reggie-db/dbx-tools";
|
|
143
|
+
console.log(`Working Directory: ${cwd}`);
|
|
144
|
+
|
|
145
|
+
const watcher = watchFiles("**/src/**", {
|
|
146
|
+
cwd,
|
|
147
|
+
ignoreInitial: false,
|
|
148
|
+
ignore: ["**/index.ts", "**/core/**"],
|
|
149
|
+
ignoreOptions: {
|
|
150
|
+
temp: false,
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
watcher.on("all", (event: string, path: string) => {
|
|
154
|
+
console.log(`[${event.toUpperCase()}] ${path}`);
|
|
155
|
+
});
|
|
156
|
+
watcher.on("error", (error) => {
|
|
157
|
+
console.error("Watcher error:", error);
|
|
158
|
+
});
|
|
159
|
+
console.log("Watcher started");
|
|
160
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const secret = true;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const skip = true;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const keep = true;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parity tests: the same ignore patterns must make {@link findFiles} and
|
|
3
|
+
* {@link watchFiles} keep the same set of files. Both run against a temp copy of
|
|
4
|
+
* the fixture tree (see the note below the imports for why a temp root is used)
|
|
5
|
+
* and their results are asserted equal, then checked against a concrete list.
|
|
6
|
+
*/
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { cpSync, mkdtempSync, rmSync } from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { join, relative, sep, isAbsolute } from "node:path";
|
|
11
|
+
import { after, before, describe, it } from "node:test";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { findFiles } from "../src/find";
|
|
14
|
+
import { watchFiles } from "../src/watch";
|
|
15
|
+
|
|
16
|
+
const FIXTURE_SOURCE = fileURLToPath(new URL("fixtures/sample-tree", import.meta.url));
|
|
17
|
+
|
|
18
|
+
// glob matches ignore patterns against cwd-relative paths, while chokidar
|
|
19
|
+
// matches them against absolute paths. The real fixture lives under a `test/`
|
|
20
|
+
// directory, so the default `**/test/**` group would match in watch (absolute)
|
|
21
|
+
// but not in scan (relative) - a mismatch unrelated to the shared patterns.
|
|
22
|
+
// Copying the tree to a neutral temp root removes that ancestor so the two are
|
|
23
|
+
// compared purely on pattern parity.
|
|
24
|
+
let root: string;
|
|
25
|
+
|
|
26
|
+
before(() => {
|
|
27
|
+
root = mkdtempSync(join(tmpdir(), "file-scan-parity-"));
|
|
28
|
+
cpSync(FIXTURE_SOURCE, root, { recursive: true });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
after(() => {
|
|
32
|
+
rmSync(root, { recursive: true, force: true });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const toPosix = (path: string): string => path.split(sep).join("/");
|
|
36
|
+
|
|
37
|
+
/** Files fileScan keeps for the fixture (relative, posix) given `ignore`. */
|
|
38
|
+
function scannedFiles(ignore: string | string[] | undefined): Set<string> {
|
|
39
|
+
return new Set([...findFiles("**", { cwd: root, nodir: true, ignore })].map(toPosix));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Files fileWatch reports for the (static) fixture (relative, posix), then closes. */
|
|
43
|
+
async function watchedFiles(ignore: string | string[] | undefined): Promise<Set<string>> {
|
|
44
|
+
const files = new Set<string>();
|
|
45
|
+
const watcher = watchFiles(root, {
|
|
46
|
+
cwd: root,
|
|
47
|
+
ignore,
|
|
48
|
+
persistent: false,
|
|
49
|
+
ignoreInitial: false,
|
|
50
|
+
});
|
|
51
|
+
watcher.on("add", (path) => {
|
|
52
|
+
const rel = isAbsolute(path) ? relative(root, path) : path;
|
|
53
|
+
files.add(toPosix(rel === "" ? "." : rel));
|
|
54
|
+
});
|
|
55
|
+
try {
|
|
56
|
+
await new Promise<void>((resolve, reject) => {
|
|
57
|
+
watcher.once("ready", () => resolve());
|
|
58
|
+
watcher.once("error", reject);
|
|
59
|
+
});
|
|
60
|
+
} finally {
|
|
61
|
+
await watcher.close();
|
|
62
|
+
}
|
|
63
|
+
return files;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The same ignore patterns must make fileScan and fileWatch keep the same files.
|
|
68
|
+
* Asserts parity, then returns the (shared) sorted result for concrete checks.
|
|
69
|
+
*/
|
|
70
|
+
async function keptByBoth(patterns: string | string[] | undefined): Promise<string[]> {
|
|
71
|
+
const scanned = [...scannedFiles(patterns)].sort();
|
|
72
|
+
const watched = [...(await watchedFiles(patterns))].sort();
|
|
73
|
+
assert.deepEqual(watched, scanned);
|
|
74
|
+
return scanned;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
describe("scan/watch matching parity", () => {
|
|
78
|
+
// `.hidden` (dot group) and any node_modules/dist (default groups) are dropped
|
|
79
|
+
// by both; only the plain source files survive the default ignore list.
|
|
80
|
+
it("keeps the same files with only the default ignore groups", async () => {
|
|
81
|
+
assert.deepEqual(await keptByBoth(undefined), [
|
|
82
|
+
"example/skip.ts",
|
|
83
|
+
"src/index.ts",
|
|
84
|
+
"src/keep.ts",
|
|
85
|
+
]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("keeps the same files ignoring a file pattern", async () => {
|
|
89
|
+
assert.deepEqual(await keptByBoth("**/index.ts"), ["example/skip.ts", "src/keep.ts"]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("keeps the same files ignoring a directory pattern", async () => {
|
|
93
|
+
assert.deepEqual(await keptByBoth(["**/example/**"]), ["src/index.ts", "src/keep.ts"]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("keeps the same files ignoring multiple patterns", async () => {
|
|
97
|
+
assert.deepEqual(await keptByBoth(["**/index.ts", "**/example/**"]), ["src/keep.ts"]);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// ~~ Generated by projen. To modify, edit .projenrc.js and run "pnpm exec projen".
|
|
2
|
+
{
|
|
3
|
+
"extends": "../tsconfig.json",
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"noEmit": true,
|
|
6
|
+
"rootDir": ".."
|
|
7
|
+
},
|
|
8
|
+
"include": [
|
|
9
|
+
"**/*.ts"
|
|
10
|
+
],
|
|
11
|
+
"exclude": [
|
|
12
|
+
"node_modules"
|
|
13
|
+
]
|
|
14
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// ~~ Generated by projen. To modify, edit .projenrc.js and run "pnpm exec projen".
|
|
2
|
+
{
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": "src",
|
|
5
|
+
"outDir": "lib",
|
|
6
|
+
"alwaysStrict": true,
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"experimentalDecorators": true,
|
|
10
|
+
"inlineSourceMap": true,
|
|
11
|
+
"inlineSources": true,
|
|
12
|
+
"lib": [
|
|
13
|
+
"ES2022"
|
|
14
|
+
],
|
|
15
|
+
"module": "ESNext",
|
|
16
|
+
"noEmitOnError": false,
|
|
17
|
+
"noFallthroughCasesInSwitch": true,
|
|
18
|
+
"noImplicitAny": true,
|
|
19
|
+
"noImplicitReturns": true,
|
|
20
|
+
"noImplicitThis": true,
|
|
21
|
+
"noUnusedLocals": true,
|
|
22
|
+
"noUnusedParameters": true,
|
|
23
|
+
"resolveJsonModule": true,
|
|
24
|
+
"strict": true,
|
|
25
|
+
"strictNullChecks": true,
|
|
26
|
+
"strictPropertyInitialization": true,
|
|
27
|
+
"stripInternal": true,
|
|
28
|
+
"target": "ES2022",
|
|
29
|
+
"types": [
|
|
30
|
+
"node"
|
|
31
|
+
],
|
|
32
|
+
"moduleResolution": "bundler",
|
|
33
|
+
"skipLibCheck": true
|
|
34
|
+
},
|
|
35
|
+
"include": [
|
|
36
|
+
"src/**/*.ts"
|
|
37
|
+
],
|
|
38
|
+
"exclude": [
|
|
39
|
+
"node_modules"
|
|
40
|
+
]
|
|
41
|
+
}
|