asljs-locator 0.1.2 → 0.1.3
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 +5 -0
- package/dist/git-ignore.d.ts +18 -5
- package/dist/git-ignore.js +52 -46
- package/dist/location.d.ts +11 -1
- package/dist/location.js +80 -45
- package/package.json +16 -8
package/README.md
CHANGED
|
@@ -59,6 +59,11 @@ const files = await resolver.resolve(process.cwd(), {
|
|
|
59
59
|
`resolve` returns absolute paths, deduplicated and sorted. Give it one location
|
|
60
60
|
or several; several are merged.
|
|
61
61
|
|
|
62
|
+
The excludes and the filters apply while the tree is walked, not to what it
|
|
63
|
+
found: a folder that `.gitignore` ignores, or whose every file an exclude such
|
|
64
|
+
as `**/node_modules/**` takes away, is not entered at all. That is what keeps
|
|
65
|
+
locating cheap in a tree full of `node_modules` and build output.
|
|
66
|
+
|
|
62
67
|
`check` answers the same question for a single path without walking the tree,
|
|
63
68
|
which is what makes it usable per file:
|
|
64
69
|
|
package/dist/git-ignore.d.ts
CHANGED
|
@@ -2,16 +2,29 @@ import { Logger } from 'asljs-logging';
|
|
|
2
2
|
/**
|
|
3
3
|
* Whether a path is ignored by a `.gitignore`.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* A path is tested against the `.gitignore` of every folder above it, because
|
|
6
|
+
* a nested `.gitignore` applies to its own subtree and git reads them all.
|
|
7
|
+
* Each folder's file is read once, and a path is tested only against the
|
|
8
|
+
* folders it is in, however many other folders were read.
|
|
9
|
+
*
|
|
10
|
+
* A path ending with the separator is a folder, tested as `name/`, as git
|
|
11
|
+
* matches a pattern such as `build/`; `LocationResolver` asks so before it
|
|
12
|
+
* walks into a folder, and skips the folder when it is ignored, since git
|
|
13
|
+
* ignores everything inside an ignored folder.
|
|
8
14
|
*/
|
|
9
15
|
export declare class GitIgnore {
|
|
10
16
|
private logger;
|
|
17
|
+
/**
|
|
18
|
+
* The matcher of each folder read, `null` for a folder without a
|
|
19
|
+
* `.gitignore`.
|
|
20
|
+
*/
|
|
11
21
|
private matchers;
|
|
12
|
-
private visitedPaths;
|
|
13
22
|
constructor(logger: Logger);
|
|
14
23
|
isIgnored(targetPath: string): boolean;
|
|
15
24
|
filter(paths: string[]): string[];
|
|
16
|
-
|
|
25
|
+
/**
|
|
26
|
+
* The matcher of the `.gitignore` of a folder, read the first time it is
|
|
27
|
+
* asked for; `null` when the folder has none.
|
|
28
|
+
*/
|
|
29
|
+
private matcherOf;
|
|
17
30
|
}
|
package/dist/git-ignore.js
CHANGED
|
@@ -1,81 +1,87 @@
|
|
|
1
1
|
import ignore from 'ignore';
|
|
2
|
-
import { readFileSync
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { toPosixPath } from './paths.js';
|
|
5
5
|
/**
|
|
6
6
|
* Whether a path is ignored by a `.gitignore`.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* A path is tested against the `.gitignore` of every folder above it, because
|
|
9
|
+
* a nested `.gitignore` applies to its own subtree and git reads them all.
|
|
10
|
+
* Each folder's file is read once, and a path is tested only against the
|
|
11
|
+
* folders it is in, however many other folders were read.
|
|
12
|
+
*
|
|
13
|
+
* A path ending with the separator is a folder, tested as `name/`, as git
|
|
14
|
+
* matches a pattern such as `build/`; `LocationResolver` asks so before it
|
|
15
|
+
* walks into a folder, and skips the folder when it is ignored, since git
|
|
16
|
+
* ignores everything inside an ignored folder.
|
|
11
17
|
*/
|
|
12
18
|
export class GitIgnore {
|
|
13
19
|
logger;
|
|
20
|
+
/**
|
|
21
|
+
* The matcher of each folder read, `null` for a folder without a
|
|
22
|
+
* `.gitignore`.
|
|
23
|
+
*/
|
|
14
24
|
matchers;
|
|
15
|
-
visitedPaths;
|
|
16
25
|
constructor(logger) {
|
|
17
26
|
this.logger = logger;
|
|
18
|
-
this.matchers =
|
|
19
|
-
this.visitedPaths = new Set();
|
|
27
|
+
this.matchers = new Map();
|
|
20
28
|
}
|
|
21
29
|
isIgnored(targetPath) {
|
|
22
30
|
if (!path.isAbsolute(targetPath)) {
|
|
23
31
|
throw new Error(`Path must be absolute: ${targetPath}`);
|
|
24
32
|
}
|
|
25
33
|
const isDirectory = targetPath.endsWith(path.sep);
|
|
26
|
-
const normalisedPath = path.normalize(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
this.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const testRelativePath = isDirectory
|
|
40
|
-
? relativePath + '/'
|
|
41
|
-
: relativePath;
|
|
42
|
-
if (matcher.matcher.ignores(testRelativePath)) {
|
|
43
|
-
return true;
|
|
34
|
+
const normalisedPath = path.normalize(isDirectory
|
|
35
|
+
? targetPath.slice(0, -path.sep.length)
|
|
36
|
+
: targetPath);
|
|
37
|
+
let folder = path.dirname(normalisedPath);
|
|
38
|
+
while (folder !== null) {
|
|
39
|
+
const matcher = this.matcherOf(folder);
|
|
40
|
+
if (matcher !== null) {
|
|
41
|
+
const relativePath = toPosixPath(path.relative(folder, normalisedPath));
|
|
42
|
+
if (matcher.ignores(isDirectory
|
|
43
|
+
? relativePath + '/'
|
|
44
|
+
: relativePath)) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
44
47
|
}
|
|
48
|
+
const parent = path.dirname(folder);
|
|
49
|
+
folder =
|
|
50
|
+
parent === folder
|
|
51
|
+
? null
|
|
52
|
+
: parent;
|
|
45
53
|
}
|
|
46
54
|
return false;
|
|
47
55
|
}
|
|
48
56
|
filter(paths) {
|
|
49
57
|
return paths.filter(filePath => !this.isIgnored(filePath));
|
|
50
58
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
59
|
+
/**
|
|
60
|
+
* The matcher of the `.gitignore` of a folder, read the first time it is
|
|
61
|
+
* asked for; `null` when the folder has none.
|
|
62
|
+
*/
|
|
63
|
+
matcherOf(folder) {
|
|
64
|
+
const known = this.matchers.get(folder);
|
|
65
|
+
if (known !== undefined) {
|
|
66
|
+
return known;
|
|
54
67
|
}
|
|
55
|
-
|
|
56
|
-
.add(directoryPath);
|
|
57
|
-
const gitIgnorePath = path.join(directoryPath, '.gitignore');
|
|
58
|
-
let gitIgnoreExists = false;
|
|
68
|
+
let content = null;
|
|
59
69
|
try {
|
|
60
|
-
|
|
61
|
-
|
|
70
|
+
content =
|
|
71
|
+
readFileSync(path.join(folder, '.gitignore'), 'utf8');
|
|
62
72
|
}
|
|
63
73
|
catch (err) {
|
|
64
74
|
const code = err.code;
|
|
65
|
-
|
|
75
|
+
// A folder without one, or a `.gitignore` that is a folder.
|
|
76
|
+
if (code !== 'ENOENT'
|
|
77
|
+
&& code !== 'EISDIR') {
|
|
66
78
|
throw err;
|
|
67
79
|
}
|
|
68
80
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
matcher });
|
|
75
|
-
}
|
|
76
|
-
const parentPath = path.dirname(directoryPath);
|
|
77
|
-
if (parentPath !== directoryPath) {
|
|
78
|
-
this.loadMatchers(parentPath);
|
|
79
|
-
}
|
|
81
|
+
const matcher = content === null
|
|
82
|
+
? null
|
|
83
|
+
: ignore().add(content);
|
|
84
|
+
this.matchers.set(folder, matcher);
|
|
85
|
+
return matcher;
|
|
80
86
|
}
|
|
81
87
|
}
|
package/dist/location.d.ts
CHANGED
|
@@ -44,6 +44,17 @@ export declare class LocationResolver {
|
|
|
44
44
|
check(targetPath: string, basePath: string, location: Location | readonly Location[]): Promise<boolean>;
|
|
45
45
|
private resolveOne;
|
|
46
46
|
private checkOne;
|
|
47
|
+
/**
|
|
48
|
+
* Whether `filters` asks for `GitIgnore`, the only filter there is; an
|
|
49
|
+
* unknown name throws.
|
|
50
|
+
*/
|
|
51
|
+
private usesGitIgnore;
|
|
52
|
+
/**
|
|
53
|
+
* Whether an absolute path matches a pattern, resolved as `kindOf` says:
|
|
54
|
+
* where it is, from the root, or from the base path. `resolve` and `check`
|
|
55
|
+
* both match with it, so they agree.
|
|
56
|
+
*/
|
|
57
|
+
private patternMatcher;
|
|
47
58
|
/**
|
|
48
59
|
* How a pattern resolves: `absolute`, a path of the file system, such as
|
|
49
60
|
* `/tmp/notes/*.md` or `C:/notes/*.md`; `root`, from the root, such as
|
|
@@ -67,5 +78,4 @@ export declare class LocationResolver {
|
|
|
67
78
|
* belonging to a location that never finds it.
|
|
68
79
|
*/
|
|
69
80
|
private expand;
|
|
70
|
-
private applyFilters;
|
|
71
81
|
}
|
package/dist/location.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { glob } from 'glob';
|
|
2
|
-
import {
|
|
2
|
+
import { Minimatch } from 'minimatch';
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { GitIgnore } from './git-ignore.js';
|
|
@@ -49,7 +49,11 @@ export class LocationResolver {
|
|
|
49
49
|
const results = [];
|
|
50
50
|
for (const one of toLocations(location)) {
|
|
51
51
|
const resolved = await this.resolveOne(basePath, one);
|
|
52
|
-
|
|
52
|
+
// One at a time: spreading tens of thousands of paths into `push`
|
|
53
|
+
// exceeds the call stack.
|
|
54
|
+
for (const match of resolved) {
|
|
55
|
+
results.push(match);
|
|
56
|
+
}
|
|
53
57
|
}
|
|
54
58
|
const unique = [...new Set(results)];
|
|
55
59
|
unique.sort((a, b) => a.localeCompare(b));
|
|
@@ -80,11 +84,31 @@ export class LocationResolver {
|
|
|
80
84
|
const normalisedBasePath = path.normalize(path.resolve(basePath));
|
|
81
85
|
assertFilePatterns([...patterns,
|
|
82
86
|
...exclude]);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
87
|
+
// Excludes and the filters are applied as the tree is walked, not to what
|
|
88
|
+
// it found: a folder whose every file is excluded, as by
|
|
89
|
+
// `**/node_modules/**`, or which `.gitignore` ignores, is not entered.
|
|
90
|
+
const excluded = exclude.map(pattern => this.patternMatcher(pattern, normalisedBasePath));
|
|
91
|
+
// `<folder>/**` excludes everything in the folders `<folder>` matches.
|
|
92
|
+
const excludedFolders = exclude
|
|
93
|
+
.filter(pattern => pattern.endsWith('/**'))
|
|
94
|
+
.map(pattern => pattern.slice(0, -'/**'.length))
|
|
95
|
+
.filter(folder => folder !== ''
|
|
96
|
+
&& folder !== '/')
|
|
97
|
+
.map(folder => this.patternMatcher(folder, normalisedBasePath));
|
|
98
|
+
const gitIgnore = this.usesGitIgnore(filters);
|
|
99
|
+
const ignore = { ignored: (walked) => {
|
|
100
|
+
const fullPath = walked.fullpath();
|
|
101
|
+
return excluded.some(matches => matches(fullPath))
|
|
102
|
+
|| gitIgnore
|
|
103
|
+
&& this.gitIgnore.isIgnored(fullPath);
|
|
104
|
+
},
|
|
105
|
+
childrenIgnored: (walked) => {
|
|
106
|
+
const fullPath = walked.fullpath();
|
|
107
|
+
return excludedFolders.some(matches => matches(fullPath))
|
|
108
|
+
|| gitIgnore
|
|
109
|
+
&& this.gitIgnore.isIgnored(fullPath + path.sep);
|
|
110
|
+
} };
|
|
111
|
+
const result = [...new Set(await this.expand(patterns, normalisedBasePath, ignore))];
|
|
88
112
|
result.sort((a, b) => a.localeCompare(b));
|
|
89
113
|
this.logger.trace('LocationResolver.resolve() => %d matches', result.length);
|
|
90
114
|
return result;
|
|
@@ -99,26 +123,42 @@ export class LocationResolver {
|
|
|
99
123
|
...exclude]);
|
|
100
124
|
const normalisedTargetPath = path.normalize(path.resolve(targetPath));
|
|
101
125
|
const normalisedBasePath = path.normalize(path.resolve(basePath));
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
126
|
+
const matches = (pattern) => this.patternMatcher(pattern, normalisedBasePath)(normalisedTargetPath);
|
|
127
|
+
return patterns.some(matches)
|
|
128
|
+
&& !exclude.some(matches)
|
|
129
|
+
&& !(this.usesGitIgnore(filters)
|
|
130
|
+
&& this.gitIgnore.isIgnored(normalisedTargetPath));
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Whether `filters` asks for `GitIgnore`, the only filter there is; an
|
|
134
|
+
* unknown name throws.
|
|
135
|
+
*/
|
|
136
|
+
usesGitIgnore(filters) {
|
|
137
|
+
for (const filter of filters) {
|
|
138
|
+
if (filter.name !== 'GitIgnore') {
|
|
139
|
+
throw new Error(`Unknown filter: ${filter.name}`);
|
|
106
140
|
}
|
|
107
|
-
const relativePath = path.relative(kind === 'root'
|
|
108
|
-
? this.rootPath
|
|
109
|
-
: normalisedBasePath, normalisedTargetPath);
|
|
110
|
-
return minimatch(relativePath, kind === 'root'
|
|
111
|
-
? pattern.slice(1)
|
|
112
|
-
: pattern, { dot: true });
|
|
113
|
-
};
|
|
114
|
-
if (!patterns.some(matchesPattern)) {
|
|
115
|
-
return false;
|
|
116
141
|
}
|
|
117
|
-
|
|
118
|
-
|
|
142
|
+
return filters.length > 0;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Whether an absolute path matches a pattern, resolved as `kindOf` says:
|
|
146
|
+
* where it is, from the root, or from the base path. `resolve` and `check`
|
|
147
|
+
* both match with it, so they agree.
|
|
148
|
+
*/
|
|
149
|
+
patternMatcher(pattern, basePath) {
|
|
150
|
+
const kind = this.kindOf(pattern);
|
|
151
|
+
if (kind === 'absolute') {
|
|
152
|
+
const absolute = new Minimatch(toPosixPath(pattern), { dot: true });
|
|
153
|
+
return targetPath => absolute.match(toPosixPath(targetPath));
|
|
119
154
|
}
|
|
120
|
-
const
|
|
121
|
-
|
|
155
|
+
const from = kind === 'root'
|
|
156
|
+
? this.rootPath
|
|
157
|
+
: basePath;
|
|
158
|
+
const relative = new Minimatch(kind === 'root'
|
|
159
|
+
? pattern.slice(1)
|
|
160
|
+
: pattern, { dot: true });
|
|
161
|
+
return targetPath => relative.match(toPosixPath(path.relative(from, targetPath)));
|
|
122
162
|
}
|
|
123
163
|
/**
|
|
124
164
|
* How a pattern resolves: `absolute`, a path of the file system, such as
|
|
@@ -157,49 +197,44 @@ export class LocationResolver {
|
|
|
157
197
|
* agree: without it a path such as `.remarkrc.mjs` would be reported as
|
|
158
198
|
* belonging to a location that never finds it.
|
|
159
199
|
*/
|
|
160
|
-
async expand(patterns, basePath) {
|
|
200
|
+
async expand(patterns, basePath, ignore) {
|
|
161
201
|
const matches = [];
|
|
162
202
|
const absolutePatterns = patterns
|
|
163
203
|
.filter(pattern => this.kindOf(pattern)
|
|
164
204
|
=== 'absolute')
|
|
165
205
|
.map(pattern => toPosixPath(pattern));
|
|
166
206
|
if (absolutePatterns.length > 0) {
|
|
167
|
-
|
|
207
|
+
for (const match of await glob(absolutePatterns, { absolute: true,
|
|
168
208
|
dot: true,
|
|
169
|
-
nodir: true
|
|
209
|
+
nodir: true,
|
|
210
|
+
ignore })) {
|
|
211
|
+
matches.push(match);
|
|
212
|
+
}
|
|
170
213
|
}
|
|
171
214
|
const rootPatterns = patterns
|
|
172
215
|
.filter(pattern => this.kindOf(pattern)
|
|
173
216
|
=== 'root')
|
|
174
217
|
.map(pattern => pattern.slice(1));
|
|
175
218
|
if (rootPatterns.length > 0) {
|
|
176
|
-
|
|
219
|
+
for (const match of await glob(rootPatterns, { cwd: this.rootPath,
|
|
177
220
|
absolute: true,
|
|
178
221
|
dot: true,
|
|
179
|
-
nodir: true
|
|
222
|
+
nodir: true,
|
|
223
|
+
ignore })) {
|
|
224
|
+
matches.push(match);
|
|
225
|
+
}
|
|
180
226
|
}
|
|
181
227
|
const basePatterns = patterns.filter(pattern => this.kindOf(pattern)
|
|
182
228
|
=== 'base');
|
|
183
229
|
if (basePatterns.length > 0) {
|
|
184
|
-
|
|
230
|
+
for (const match of await glob(basePatterns, { cwd: basePath,
|
|
185
231
|
absolute: true,
|
|
186
232
|
dot: true,
|
|
187
|
-
nodir: true
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
applyFilters(paths, filters) {
|
|
192
|
-
let result = paths;
|
|
193
|
-
for (const filter of filters) {
|
|
194
|
-
switch (filter.name) {
|
|
195
|
-
case 'GitIgnore':
|
|
196
|
-
result =
|
|
197
|
-
this.gitIgnore.filter(result);
|
|
198
|
-
break;
|
|
199
|
-
default:
|
|
200
|
-
throw new Error(`Unknown filter: ${filter.name}`);
|
|
233
|
+
nodir: true,
|
|
234
|
+
ignore })) {
|
|
235
|
+
matches.push(match);
|
|
201
236
|
}
|
|
202
237
|
}
|
|
203
|
-
return
|
|
238
|
+
return matches;
|
|
204
239
|
}
|
|
205
240
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "asljs-locator",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Locates files by glob patterns, exclusions and filters such as .gitignore.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"asljs": {
|
|
@@ -18,23 +18,31 @@
|
|
|
18
18
|
"dist/**",
|
|
19
19
|
"README.md",
|
|
20
20
|
"LICENSE.md",
|
|
21
|
-
"docs/**"
|
|
21
|
+
"docs/**",
|
|
22
|
+
"!dist/*.tsbuildinfo"
|
|
22
23
|
],
|
|
23
24
|
"scripts": {
|
|
24
25
|
"flint": "toolkit flint",
|
|
25
|
-
"clean": "
|
|
26
|
-
"
|
|
27
|
-
"build
|
|
26
|
+
"clean:build": "node ../../scripts/cleanBuildOutDir.js",
|
|
27
|
+
"clean:dist": "node ../../scripts/cleanDistOutDir.js",
|
|
28
|
+
"compile:build": "npm run codegen --if-present && tsc -p tsconfig.build.json",
|
|
29
|
+
"compile:dist": "npm run codegen --if-present && tsc -p tsconfig.dist.json",
|
|
30
|
+
"build:dist": "node ../../scripts/buildDistDeps.js && npm run compile:dist",
|
|
28
31
|
"typecheck": "npm run codegen --if-present && tsc -p tsconfig.build.json --noEmit",
|
|
29
32
|
"guard:clean-git": "toolkit ensure-clean-working-directory",
|
|
30
|
-
"prepack": "npm run clean && npm run build:dist && npm run build && npm run test && npm run guard:clean-git",
|
|
33
|
+
"prepack": "npm run clean:build && npm run clean:dist && npm run build:dist && npm run compile:build && npm run test && npm run guard:clean-git",
|
|
31
34
|
"postpublish": "toolkit tag-release-revision",
|
|
32
35
|
"test": "node --test --enable-source-maps \"build/**/*.test.js\"",
|
|
33
36
|
"test:watch": "node --watch --test \"build/**/*.test.js\"",
|
|
34
|
-
"coverage": "npm run build && node --test --experimental-test-coverage --enable-source-maps --test-coverage-exclude=\"**/*.test.js\" \"build/**/*.test.js\"",
|
|
37
|
+
"coverage": "npm run compile:build && node --test --experimental-test-coverage --enable-source-maps --test-coverage-exclude=\"**/*.test.js\" \"build/**/*.test.js\"",
|
|
35
38
|
"release:patch": "toolkit release-patch",
|
|
36
|
-
"
|
|
39
|
+
"build": "npm run clean:build && npm run flint && npm run typecheck && npm run compile:build && npm run test"
|
|
37
40
|
},
|
|
41
|
+
"distDependencies": [
|
|
42
|
+
"asljs-logging",
|
|
43
|
+
"asljs-testing",
|
|
44
|
+
"asljs-tmpdir"
|
|
45
|
+
],
|
|
38
46
|
"keywords": [
|
|
39
47
|
"glob",
|
|
40
48
|
"gitignore",
|