Skip to content

Commit 7704399

Browse files
committed
feat(lib): createParseConfigHost
Signed-off-by: Lexus Drumgold <[email protected]>
1 parent 585921b commit 7704399

File tree

9 files changed

+352
-10
lines changed

9 files changed

+352
-10
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ import {
8282
This package exports the following identifiers:
8383

8484
- [`createModuleResolutionHost`](./src/lib/create-module-resolution-host.mts)
85+
- [`createParseConfigHost`](./src/lib/create-parse-config-host.mts)
8586
- [`isResolvedTsconfig`](./src/lib/is-resolved-tsconfig.mts)
8687
- [`isTsconfigHost`](./src/lib/is-tsconfig-host.mts)
8788
- [`loadTsconfig`](./src/lib/load-tsconfig.mts)

src/__snapshots__/index.e2e.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
exports[`e2e:tsconfig-utils > should expose public api 1`] = `
44
[
55
"createModuleResolutionHost",
6+
"createParseConfigHost",
67
"isResolvedTsconfig",
78
"isTsconfigHost",
89
"loadTsconfig",

src/interfaces/options-load-tsconfig.mts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,7 @@ interface LoadTsconfigOptions extends ReadTsconfigOptions {
2323
* 'compilerOptions.paths.*',
2424
* 'compilerOptions.paths.*.*',
2525
* 'compilerOptions.rootDir',
26-
* 'exclude.*',
27-
* 'files.*',
28-
* 'include.*'
26+
* 'files.*'
2927
* ])
3028
*/
3129
relativePaths?: Set<string> | readonly string[] | null | undefined

src/lib/__snapshots__/load-tsconfig.snap

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,9 @@ exports[`unit:lib/loadTsconfig > should return resolved tsconfig object 1`] = `
5050
"verbatimModuleSyntax": true,
5151
},
5252
"exclude": [
53-
"./coverage",
54-
"./dist",
55-
"./node_modules",
53+
"coverage",
54+
"dist",
55+
"node_modules",
5656
],
5757
"include": [
5858
"**/**.json",
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* @file Unit Tests - createParseConfigHost
3+
* @module tsconfig-utils/lib/tests/unit/createParseConfigHost
4+
*/
5+
6+
import fs from '#internal/fs'
7+
import testSubject from '#lib/create-parse-config-host'
8+
import * as mlly from '@flex-development/mlly'
9+
import type { ParseConfigHost } from '@flex-development/tsconfig-utils'
10+
import { alphabetize, identity } from '@flex-development/tutils'
11+
import ts from 'typescript'
12+
import tsconfig from '../../../tsconfig.json' with { type: 'json' }
13+
14+
describe('unit:lib/createParseConfigHost', () => {
15+
let subject: ParseConfigHost
16+
17+
beforeAll(() => {
18+
subject = testSubject()
19+
})
20+
21+
describe('host', () => {
22+
describe('readDirectory', () => {
23+
it.each<Parameters<ParseConfigHost['readDirectory']>>([
24+
[
25+
new URL('src', mlly.cwd()),
26+
['.cjs', '.cts', '.js', '.json', '.mjs', '.mts', '.ts'],
27+
tsconfig.exclude,
28+
tsconfig.include,
29+
0
30+
],
31+
[
32+
mlly.cwd(),
33+
['.cjs', '.cts', '.js', '.json', '.mjs', '.mts', '.ts'],
34+
tsconfig.exclude,
35+
tsconfig.include
36+
],
37+
[
38+
mlly.cwd(),
39+
['.mjs', '.mts', '.ts'],
40+
tsconfig.exclude,
41+
tsconfig.include,
42+
1
43+
]
44+
])('should return list of files under directory at `id` (%#)', (
45+
id,
46+
extensions,
47+
exclude,
48+
include,
49+
depth
50+
) => {
51+
// Arrange
52+
const expected: readonly string[] = alphabetize(ts.sys.readDirectory(
53+
fs.realpath(id),
54+
extensions ? [...extensions] : undefined,
55+
exclude ? [...exclude] : undefined,
56+
include ? [...include] : undefined,
57+
depth ?? undefined
58+
), identity)
59+
60+
// Act
61+
const result = subject.readDirectory(
62+
id,
63+
extensions,
64+
exclude,
65+
include,
66+
depth
67+
)
68+
69+
// Expect
70+
expect(result).to.be.an('array').and.be.frozen.and.eql(expected)
71+
})
72+
})
73+
})
74+
})

src/lib/create-module-resolution-host.mts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,8 @@ function createModuleResolutionHost(
115115
id = pathe.toPath(id)
116116

117117
names = fs.readdir(id).filter(x => {
118-
return directoryExists(pathe.join(id as string, x))
118+
ok(typeof id === 'string', 'expected `id` to be a string')
119+
return directoryExists(pathe.join(id, x))
119120
})
120121
}
121122

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
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

Comments
 (0)