|
1 | 1 | import * as fs from "fs";
|
2 | 2 | import { promises as fsp } from "fs";
|
3 | 3 | import { Minimatch } from "minimatch";
|
4 |
| -import { dirname, join } from "path"; |
| 4 | +import { dirname, join, relative } from "path"; |
5 | 5 |
|
6 | 6 | /**
|
7 | 7 | * Get the longest directory path common to all files.
|
@@ -139,41 +139,92 @@ export function copySync(src: string, dest: string): void {
|
139 | 139 | export function glob(
|
140 | 140 | pattern: string,
|
141 | 141 | root: string,
|
142 |
| - options?: { includeDirectories?: boolean } |
| 142 | + options: { includeDirectories?: boolean; followSymlinks?: boolean } = {} |
143 | 143 | ): string[] {
|
144 | 144 | const result: string[] = [];
|
145 | 145 | const mini = new Minimatch(normalizePath(pattern));
|
146 | 146 | const dirs: string[][] = [normalizePath(root).split("/")];
|
| 147 | + // cache of real paths to avoid infinite recursion |
| 148 | + const symlinkTargetsSeen: Set<string> = new Set(); |
| 149 | + // cache of fs.realpathSync results to avoid extra I/O |
| 150 | + const realpathCache: Map<string, string> = new Map(); |
| 151 | + const { includeDirectories = false, followSymlinks = false } = options; |
| 152 | + |
| 153 | + let dir = dirs.shift(); |
| 154 | + |
| 155 | + const handleFile = (path: string) => { |
| 156 | + const childPath = [...dir!, path].join("/"); |
| 157 | + if (mini.match(childPath)) { |
| 158 | + result.push(childPath); |
| 159 | + } |
| 160 | + }; |
| 161 | + |
| 162 | + const handleDirectory = (path: string) => { |
| 163 | + const childPath = [...dir!, path]; |
| 164 | + if ( |
| 165 | + mini.set.some((row) => |
| 166 | + mini.matchOne(childPath, row, /* partial */ true) |
| 167 | + ) |
| 168 | + ) { |
| 169 | + dirs.push(childPath); |
| 170 | + } |
| 171 | + }; |
| 172 | + |
| 173 | + const handleSymlink = (path: string) => { |
| 174 | + const childPath = [...dir!, path].join("/"); |
| 175 | + let realpath: string; |
| 176 | + try { |
| 177 | + realpath = |
| 178 | + realpathCache.get(childPath) ?? fs.realpathSync(childPath); |
| 179 | + realpathCache.set(childPath, realpath); |
| 180 | + } catch { |
| 181 | + return; |
| 182 | + } |
147 | 183 |
|
148 |
| - do { |
149 |
| - const dir = dirs.shift()!; |
| 184 | + if (symlinkTargetsSeen.has(realpath)) { |
| 185 | + return; |
| 186 | + } |
| 187 | + symlinkTargetsSeen.add(realpath); |
| 188 | + |
| 189 | + try { |
| 190 | + const stats = fs.statSync(realpath); |
| 191 | + if (stats.isDirectory()) { |
| 192 | + handleDirectory(path); |
| 193 | + } else if (stats.isFile()) { |
| 194 | + handleFile(path); |
| 195 | + } else if (stats.isSymbolicLink()) { |
| 196 | + const dirpath = dir!.join("/"); |
| 197 | + if (dirpath === realpath) { |
| 198 | + // special case: real path of symlink is the directory we're currently traversing |
| 199 | + return; |
| 200 | + } |
| 201 | + const targetPath = relative(dirpath, realpath); |
| 202 | + handleSymlink(targetPath); |
| 203 | + } // everything else should be ignored |
| 204 | + } catch (e) { |
| 205 | + // invalid symbolic link; ignore |
| 206 | + } |
| 207 | + }; |
150 | 208 |
|
151 |
| - if (options?.includeDirectories && mini.match(dir.join("/"))) { |
| 209 | + while (dir) { |
| 210 | + if (includeDirectories && mini.match(dir.join("/"))) { |
152 | 211 | result.push(dir.join("/"));
|
153 | 212 | }
|
154 | 213 |
|
155 | 214 | for (const child of fs.readdirSync(dir.join("/"), {
|
156 | 215 | withFileTypes: true,
|
157 | 216 | })) {
|
158 | 217 | if (child.isFile()) {
|
159 |
| - const childPath = [...dir, child.name].join("/"); |
160 |
| - if (mini.match(childPath)) { |
161 |
| - result.push(childPath); |
162 |
| - } |
163 |
| - } |
164 |
| - |
165 |
| - if (child.isDirectory() && child.name !== "node_modules") { |
166 |
| - const childPath = dir.concat(child.name); |
167 |
| - if ( |
168 |
| - mini.set.some((row) => |
169 |
| - mini.matchOne(childPath, row, /* partial */ true) |
170 |
| - ) |
171 |
| - ) { |
172 |
| - dirs.push(childPath); |
173 |
| - } |
| 218 | + handleFile(child.name); |
| 219 | + } else if (child.isDirectory() && child.name !== "node_modules") { |
| 220 | + handleDirectory(child.name); |
| 221 | + } else if (followSymlinks && child.isSymbolicLink()) { |
| 222 | + handleSymlink(child.name); |
174 | 223 | }
|
175 | 224 | }
|
176 |
| - } while (dirs.length); |
| 225 | + |
| 226 | + dir = dirs.shift(); |
| 227 | + } |
177 | 228 |
|
178 | 229 | return result;
|
179 | 230 | }
|
0 commit comments