|
| 1 | +/** |
| 2 | + * @file createParseConfigHost |
| 3 | + * @module tsconfig-utils/lib/createParseConfigHost |
| 4 | + */ |
| 5 | + |
| 6 | +import dfs from '#internal/fs' |
| 7 | +import createModuleResolutionHost from '#lib/create-module-resolution-host' |
| 8 | +import type { ModuleId } from '@flex-development/mlly' |
| 9 | +import pathe from '@flex-development/pathe' |
| 10 | +import type { |
| 11 | + FileSystem, |
| 12 | + ModuleResolutionHost, |
| 13 | + ParseConfigHost |
| 14 | +} from '@flex-development/tsconfig-utils' |
| 15 | +import { alphabetize, fork, identity } from '@flex-development/tutils' |
| 16 | +import { ok } from 'devlop' |
| 17 | + |
| 18 | +export default createParseConfigHost |
| 19 | + |
| 20 | +/** |
| 21 | + * Union of path types. |
| 22 | + * |
| 23 | + * @internal |
| 24 | + */ |
| 25 | +type PathType = 'directory' | 'file' |
| 26 | + |
| 27 | +/** |
| 28 | + * Create a parse config host. |
| 29 | + * |
| 30 | + * @see {@linkcode FileSystem} |
| 31 | + * @see {@linkcode ParseConfigHost} |
| 32 | + * |
| 33 | + * @this {void} |
| 34 | + * |
| 35 | + * @param {FileSystem | null | undefined} [fs] |
| 36 | + * File system API |
| 37 | + * @return {ParseConfigHost} |
| 38 | + * Parse config host object |
| 39 | + */ |
| 40 | +function createParseConfigHost( |
| 41 | + this: void, |
| 42 | + fs?: FileSystem | null | undefined |
| 43 | +): ParseConfigHost { |
| 44 | + fs ??= dfs |
| 45 | + |
| 46 | + /** |
| 47 | + * Module resolution host object. |
| 48 | + * |
| 49 | + * @const {ModuleResolutionHost} host |
| 50 | + */ |
| 51 | + const host: ModuleResolutionHost = createModuleResolutionHost(fs) |
| 52 | + |
| 53 | + return { |
| 54 | + directoryExists: host.directoryExists, |
| 55 | + fileExists: host.fileExists, |
| 56 | + getCurrentDirectory: pathe.cwd, |
| 57 | + getDirectories: host.getDirectories, |
| 58 | + readDirectory, |
| 59 | + readFile: host.readFile, |
| 60 | + realpath: host.realpath, |
| 61 | + useCaseSensitiveFileNames: false |
| 62 | + } |
| 63 | + |
| 64 | + /** |
| 65 | + * Get a list of files in a directory. |
| 66 | + * |
| 67 | + * @this {void} |
| 68 | + * |
| 69 | + * @param {ModuleId} id |
| 70 | + * The directory path or URL to read |
| 71 | + * @param {Set<string> | ReadonlyArray<string> | undefined} [extensions] |
| 72 | + * List of file extensions to filter for |
| 73 | + * @param {Set<string> | ReadonlyArray<string> | undefined} [exclude] |
| 74 | + * List of of glob patterns matching files to exclude |
| 75 | + * @param {Set<string> | ReadonlyArray<string> | undefined} [include] |
| 76 | + * List of of glob patterns matching files to include |
| 77 | + * @param {number | null | undefined} [depth] |
| 78 | + * Maximum search depth (inclusive) |
| 79 | + * @return {ReadonlyArray<string>} |
| 80 | + * List of files under directory at `id` |
| 81 | + */ |
| 82 | + function readDirectory( |
| 83 | + this: void, |
| 84 | + id: ModuleId, |
| 85 | + extensions: Set<string> | readonly string[] | undefined, |
| 86 | + exclude: Set<string> | readonly string[] | undefined, |
| 87 | + include: Set<string> | readonly string[] | undefined, |
| 88 | + depth?: number | null | undefined |
| 89 | + ): readonly string[] { |
| 90 | + id = host.realpath(id) |
| 91 | + |
| 92 | + /** |
| 93 | + * Record of glob patterns matching directories and files to exclude. |
| 94 | + * |
| 95 | + * @const {Record<PathType, Set<string>>} ignore |
| 96 | + */ |
| 97 | + const ignore: Record<PathType, Set<string>> = { |
| 98 | + directory: new Set<string>(), |
| 99 | + file: new Set<string>() |
| 100 | + } |
| 101 | + |
| 102 | + /** |
| 103 | + * List of matched files. |
| 104 | + * |
| 105 | + * @const {string[]} matched |
| 106 | + */ |
| 107 | + const matched: string[] = [] |
| 108 | + |
| 109 | + /** |
| 110 | + * Record of glob patterns matching directories and files to include. |
| 111 | + * |
| 112 | + * @const {Record<PathType, Set<string>>} matchers |
| 113 | + */ |
| 114 | + const matchers: Record<PathType, Set<string>> = { |
| 115 | + directory: new Set<string>(), |
| 116 | + file: new Set<string>() |
| 117 | + } |
| 118 | + |
| 119 | + /** |
| 120 | + * Visited paths. |
| 121 | + * |
| 122 | + * @const {Set<string>} visited |
| 123 | + */ |
| 124 | + const visited: Set<string> = new Set<string>() |
| 125 | + |
| 126 | + pstore(ignore, exclude) |
| 127 | + pstore(matchers, include) |
| 128 | + |
| 129 | + if (!matchers.directory.size) matchers.directory.add('**/*') |
| 130 | + |
| 131 | + matchers.directory.add('bower_components') |
| 132 | + matchers.directory.add('node_modules') |
| 133 | + matchers.directory.add('jspm_packages') |
| 134 | + |
| 135 | + visit(id, '', depth) |
| 136 | + |
| 137 | + return Object.freeze(alphabetize(matched, identity)) |
| 138 | + |
| 139 | + /** |
| 140 | + * Check if `x` is a matched directory or file. |
| 141 | + * |
| 142 | + * @this {void} |
| 143 | + * |
| 144 | + * @param {string} x |
| 145 | + * The path to match |
| 146 | + * @param {PathType} type |
| 147 | + * Path type |
| 148 | + * @return {boolean} |
| 149 | + * `true` if `x` matches include pattern and is not excluded |
| 150 | + */ |
| 151 | + function filter(this: void, x: string, type: PathType): boolean { |
| 152 | + return pathe.matchesGlob(x, [...matchers[type]], { |
| 153 | + dot: type === 'file', |
| 154 | + ignore: [...ignore[type]] |
| 155 | + }) |
| 156 | + } |
| 157 | + |
| 158 | + /** |
| 159 | + * Store glob paterns in `record`. |
| 160 | + * |
| 161 | + * If the last path segment in a pattern does not contain a file extension |
| 162 | + * or wildcard character (`'*'`), it is treated as a directory pattern. |
| 163 | + * |
| 164 | + * @this {void} |
| 165 | + * |
| 166 | + * @param {Record<PathType, Set<string>>} record |
| 167 | + * Glob pattern record |
| 168 | + * @param {Set<string> | ReadonlyArray<string> | undefined} patterns |
| 169 | + * List of user glob patterns |
| 170 | + * @return {undefined} |
| 171 | + */ |
| 172 | + function pstore( |
| 173 | + this: void, |
| 174 | + record: Record<PathType, Set<string>>, |
| 175 | + patterns: Set<string> | readonly string[] | undefined = [] |
| 176 | + ): undefined { |
| 177 | + for (const pattern of patterns) { |
| 178 | + /** |
| 179 | + * Last segment of {@linkcode pattern}. |
| 180 | + * |
| 181 | + * @const {string} segment |
| 182 | + */ |
| 183 | + const segment: string = pathe.basename(pattern) |
| 184 | + |
| 185 | + if (!pathe.extname(segment) && !segment.includes('*')) { |
| 186 | + record.directory.add(pattern) |
| 187 | + } else { |
| 188 | + record.file.add(pattern) |
| 189 | + } |
| 190 | + } |
| 191 | + |
| 192 | + return void this |
| 193 | + } |
| 194 | + |
| 195 | + /** |
| 196 | + * Visit the directory at `path` and store matched files. |
| 197 | + * |
| 198 | + * @this {void} |
| 199 | + * |
| 200 | + * @param {string} path |
| 201 | + * Absolute directory path |
| 202 | + * @param {string} relpath |
| 203 | + * Relative directory path |
| 204 | + * @param {number | null | undefined} depth |
| 205 | + * Maximum search depth (inclusive) |
| 206 | + * @return {undefined} |
| 207 | + */ |
| 208 | + function visit( |
| 209 | + this: void, |
| 210 | + path: string, |
| 211 | + relpath: string, |
| 212 | + depth: number | null | undefined |
| 213 | + ): undefined { |
| 214 | + ok(fs, 'expected `fs`') |
| 215 | + |
| 216 | + if (host.directoryExists(path) && !visited.has(path)) { |
| 217 | + visited.add(path) |
| 218 | + |
| 219 | + /** |
| 220 | + * List where the first item is a list of files under {@linkcode path}, |
| 221 | + * and the last item a list of subdirectories. |
| 222 | + * |
| 223 | + * @const {[string[], string[]]} pack |
| 224 | + */ |
| 225 | + const pack: [string[], string[]] = fork( |
| 226 | + fs.readdir(path), |
| 227 | + x => host.fileExists(pathe.join(path, x)) |
| 228 | + ) |
| 229 | + |
| 230 | + for (const file of pack[0]) { |
| 231 | + /** |
| 232 | + * File matched? |
| 233 | + * |
| 234 | + * @var {boolean} match |
| 235 | + */ |
| 236 | + let match: boolean = filter(pathe.join(relpath, file), 'file') |
| 237 | + |
| 238 | + if (extensions) { |
| 239 | + extensions = [...extensions].map(pathe.formatExt) |
| 240 | + match = match && extensions.includes(pathe.extname(file)) |
| 241 | + } |
| 242 | + |
| 243 | + if (match) matched.push(pathe.join(path, file)) |
| 244 | + } |
| 245 | + |
| 246 | + if (depth) { |
| 247 | + depth-- |
| 248 | + if (!depth) return void depth |
| 249 | + } |
| 250 | + |
| 251 | + for (const subdirectory of pack[1]) { |
| 252 | + /** |
| 253 | + * Relative subdirectory path. |
| 254 | + * |
| 255 | + * @const {string} subdir |
| 256 | + */ |
| 257 | + const subdir: string = pathe.join(relpath, subdirectory) |
| 258 | + |
| 259 | + if (filter(subdir, 'directory')) { |
| 260 | + visit(pathe.join(path, subdirectory), subdir, depth) |
| 261 | + } |
| 262 | + } |
| 263 | + } |
| 264 | + |
| 265 | + return void path |
| 266 | + } |
| 267 | + } |
| 268 | +} |
0 commit comments