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 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
 
@@ -2,16 +2,29 @@ import { Logger } from 'asljs-logging';
2
2
  /**
3
3
  * Whether a path is ignored by a `.gitignore`.
4
4
  *
5
- * Matchers are loaded from the directory of the path being tested and every
6
- * directory above it, because a nested `.gitignore` applies to its own subtree
7
- * and git reads them all. Each directory is read once.
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
- loadMatchers(directoryPath: string): void;
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
  }
@@ -1,81 +1,87 @@
1
1
  import ignore from 'ignore';
2
- import { readFileSync, statSync } from 'node:fs';
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
- * Matchers are loaded from the directory of the path being tested and every
9
- * directory above it, because a nested `.gitignore` applies to its own subtree
10
- * and git reads them all. Each directory is read once.
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(targetPath);
27
- if (isDirectory) {
28
- this.loadMatchers(normalisedPath);
29
- }
30
- else {
31
- this.loadMatchers(path.dirname(normalisedPath));
32
- }
33
- for (const matcher of this.matchers) {
34
- const relativePath = toPosixPath(path.relative(matcher.path, normalisedPath));
35
- if (relativePath === ''
36
- || relativePath.startsWith('../')) {
37
- continue;
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
- loadMatchers(directoryPath) {
52
- if (this.visitedPaths.has(directoryPath)) {
53
- return;
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
- this.visitedPaths
56
- .add(directoryPath);
57
- const gitIgnorePath = path.join(directoryPath, '.gitignore');
58
- let gitIgnoreExists = false;
68
+ let content = null;
59
69
  try {
60
- statSync(gitIgnorePath);
61
- gitIgnoreExists = true;
70
+ content =
71
+ readFileSync(path.join(folder, '.gitignore'), 'utf8');
62
72
  }
63
73
  catch (err) {
64
74
  const code = err.code;
65
- if (code !== 'ENOENT') {
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
- if (gitIgnoreExists) {
70
- const gitIgnoreContent = readFileSync(gitIgnorePath, 'utf8');
71
- const matcher = ignore()
72
- .add(gitIgnoreContent);
73
- this.matchers.push({ path: directoryPath,
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
  }
@@ -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 { minimatch } from 'minimatch';
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
- results.push(...resolved);
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
- const matches = new Set(await this.expand(patterns, normalisedBasePath));
84
- for (const excluded of await this.expand(exclude, normalisedBasePath)) {
85
- matches.delete(excluded);
86
- }
87
- const result = this.applyFilters([...matches], filters);
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 matchesPattern = (pattern) => {
103
- const kind = this.kindOf(pattern);
104
- if (kind === 'absolute') {
105
- return minimatch(toPosixPath(normalisedTargetPath), toPosixPath(pattern), { dot: true });
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
- if (exclude.some(matchesPattern)) {
118
- return false;
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 result = this.applyFilters([normalisedTargetPath], filters);
121
- return result.length > 0;
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
- matches.push(...await glob(absolutePatterns, { absolute: true,
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
- matches.push(...await glob(rootPatterns, { cwd: this.rootPath,
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
- matches.push(...await glob(basePatterns, { cwd: basePath,
230
+ for (const match of await glob(basePatterns, { cwd: basePath,
185
231
  absolute: true,
186
232
  dot: true,
187
- nodir: true }));
188
- }
189
- return matches;
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 result;
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.2",
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": "toolkit clean",
26
- "build": "npm run codegen --if-present && tsc -p tsconfig.build.json",
27
- "build:dist": "npm run codegen --if-present && tsc -p tsconfig.dist.json",
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
- "all": "npm run clean && npm run build:dist && npm run flint && npm run typecheck && npm run build && npm run test"
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",