@carlesandres/house 0.4.7 → 0.4.9
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/CHANGELOG.md +43 -4
- package/README.md +64 -53
- package/package.json +2 -1
- package/src/Browser.tsx +235 -160
- package/src/CommandPalette.tsx +78 -14
- package/src/Footer.tsx +44 -37
- package/src/Header.tsx +4 -1
- package/src/StatusPopover.tsx +189 -0
- package/src/cli/argv.ts +15 -12
- package/src/commands/buildCommands.ts +22 -33
- package/src/commands/score.ts +1 -1
- package/src/discovery/filter.ts +83 -23
- package/src/discovery/walk.ts +24 -15
- package/src/index.tsx +120 -170
- package/src/keymap/browser.ts +14 -20
- package/src/keymap/keymap.ts +4 -4
- package/src/layout/sidebarEmptyState.ts +7 -0
- package/src/layout/sidebarRow.ts +1 -1
- package/src/markdown/frontmatter.ts +62 -0
- package/src/theme/resolve.ts +1 -1
- package/src/theme/themes/aura.json +1 -1
- package/src/theme/themes/carbonfox.json +1 -1
- package/src/theme/themes/lucent-orng.json +4 -4
- package/src/theme/themes/nightowl.json +2 -2
- package/src/theme/themes/orng.json +2 -2
- package/src/theme/themes/solarized.json +6 -2
- package/src/theme/themes/vesper.json +2 -2
- package/src/tips.ts +0 -5
- package/src/update/check.ts +1 -1
- package/src/update/runtime.ts +1 -1
- package/src/HelpOverlay.tsx +0 -148
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
* Derive palette commands from `browserBindings` plus the annotation map.
|
|
3
3
|
*
|
|
4
4
|
* The annotation map is the *only* hand-written list keyed by binding id:
|
|
5
|
-
* it carries
|
|
6
|
-
*
|
|
7
|
-
* entry rather than a palette command. Every other binding is exposed
|
|
5
|
+
* it carries title rewrites and metadata for bindings whose raw
|
|
6
|
+
* `description` reads awkwardly as a palette command. Every enabled binding is exposed
|
|
8
7
|
* verbatim — its `description` becomes the palette `title`, its first key
|
|
9
8
|
* becomes the `shortcut`.
|
|
10
9
|
*
|
|
@@ -30,49 +29,40 @@ interface Annotation {
|
|
|
30
29
|
}
|
|
31
30
|
|
|
32
31
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* hidden in v1, reconsider if users ask. Title rewrites convert
|
|
37
|
-
* help-overlay phrasing ("Toggle sidebar visibility") into imperative
|
|
38
|
-
* palette phrasing ("Toggle sidebar"). See #70 design log §6.
|
|
32
|
+
* Title rewrites convert keymap phrasing into imperative palette phrasing.
|
|
33
|
+
* With the help overlay removed (#139), the palette is the only in-app full
|
|
34
|
+
* action index, so all currently-enabled actions stay discoverable here.
|
|
39
35
|
*/
|
|
40
36
|
const annotations: Record<string, Annotation> = {
|
|
41
37
|
// --- Keep, with title rewrites where the binding description reads awkwardly as a command ---
|
|
42
38
|
quit: { category: "App" },
|
|
43
39
|
"focus.toggle": { title: "Toggle focus", category: "View" },
|
|
44
40
|
"sidebar.toggle": { title: "Toggle sidebar", category: "View" },
|
|
45
|
-
"
|
|
41
|
+
"sidebar.down": { category: "Navigation" },
|
|
42
|
+
"sidebar.up": { category: "Navigation" },
|
|
43
|
+
"sidebar.jumpDown": { category: "Navigation" },
|
|
44
|
+
"sidebar.jumpUp": { category: "Navigation" },
|
|
45
|
+
"sidebar.pageDown": { category: "Navigation" },
|
|
46
|
+
"sidebar.pageUp": { category: "Navigation" },
|
|
47
|
+
"sidebar.top": { category: "Navigation" },
|
|
48
|
+
"sidebar.bottom": { category: "Navigation" },
|
|
49
|
+
"sidebar.open": { title: "Open file", category: "Navigation" },
|
|
46
50
|
"filter.open": { title: "Filter files…", category: "Navigation" },
|
|
51
|
+
"filter.clearOrOpen": { category: "Navigation" },
|
|
47
52
|
"discovery.toggleAll": {
|
|
48
53
|
title: "Toggle hidden / gitignored files",
|
|
49
54
|
category: "Navigation",
|
|
50
55
|
keywords: ["hidden", "gitignore", "dotfiles", "all"],
|
|
51
56
|
},
|
|
57
|
+
"reader.back": { title: "Back to sidebar", category: "Navigation" },
|
|
58
|
+
"reader.prevFile": { title: "Previous file", category: "Navigation" },
|
|
59
|
+
"reader.nextFile": { title: "Next file", category: "Navigation" },
|
|
52
60
|
"serve.current": { title: "Open in browser", category: "File" },
|
|
53
61
|
"file.edit": { title: "Open in editor", category: "File", keywords: ["editor", "vim", "vscode"] },
|
|
54
62
|
"theme.next": { category: "Appearance" },
|
|
55
63
|
"theme.prev": { category: "Appearance" },
|
|
56
64
|
"theme.toneToggle": { title: "Toggle dark/light tone", category: "Appearance" },
|
|
57
65
|
|
|
58
|
-
// --- Hide: pure keystroke navigation (j/k/space/b/g/G…) ---
|
|
59
|
-
"sidebar.down": { hidden: true },
|
|
60
|
-
"sidebar.up": { hidden: true },
|
|
61
|
-
"sidebar.jumpDown": { hidden: true },
|
|
62
|
-
"sidebar.jumpUp": { hidden: true },
|
|
63
|
-
"sidebar.pageDown": { hidden: true },
|
|
64
|
-
"sidebar.pageUp": { hidden: true },
|
|
65
|
-
"sidebar.top": { hidden: true },
|
|
66
|
-
"sidebar.bottom": { hidden: true },
|
|
67
|
-
|
|
68
|
-
// --- Hide: borderline reader nav. `[`/`]` and Return-to-open feel command-shaped
|
|
69
|
-
// but are pure keystroke navigation under the hood. #70 Q6a — reconsider
|
|
70
|
-
// if user feedback expects them in the palette.
|
|
71
|
-
"sidebar.open": { hidden: true },
|
|
72
|
-
"reader.back": { hidden: true },
|
|
73
|
-
"reader.prevFile": { hidden: true },
|
|
74
|
-
"reader.nextFile": { hidden: true },
|
|
75
|
-
|
|
76
66
|
// --- Hide: the palette opener itself shouldn't appear in the palette ---
|
|
77
67
|
"palette.open": { hidden: true },
|
|
78
68
|
}
|
|
@@ -80,8 +70,8 @@ const annotations: Record<string, Annotation> = {
|
|
|
80
70
|
/**
|
|
81
71
|
* Build the AppCommand list for a given render. Iterates `browserBindings`
|
|
82
72
|
* in array order (the empty-query palette renders in this order, by design
|
|
83
|
-
* — see #70 design log §empty-state ordering), drops hidden entries
|
|
84
|
-
* those whose `when` predicate currently returns false, and resolves
|
|
73
|
+
* — see #70 design log §empty-state ordering), drops explicitly-hidden entries
|
|
74
|
+
* and those whose `when` predicate currently returns false, and resolves
|
|
85
75
|
* annotations to populate title / category / keywords.
|
|
86
76
|
*/
|
|
87
77
|
export const buildCommands = (ctx: BrowserCtx): readonly AppCommand[] => {
|
|
@@ -89,9 +79,8 @@ export const buildCommands = (ctx: BrowserCtx): readonly AppCommand[] => {
|
|
|
89
79
|
for (const binding of browserBindings) {
|
|
90
80
|
const ann = annotations[binding.id]
|
|
91
81
|
if (ann?.hidden) continue
|
|
92
|
-
// Same gating the keymap dispatcher uses. Disabled bindings
|
|
93
|
-
//
|
|
94
|
-
// reason follow-up after the atom-driven migration).
|
|
82
|
+
// Same gating the keymap dispatcher uses. Disabled bindings stay out of
|
|
83
|
+
// the palette; #96 tracks a future show-with-reason mode.
|
|
95
84
|
if (binding.when && !binding.when(ctx)) continue
|
|
96
85
|
const cmd: AppCommand = {
|
|
97
86
|
id: binding.id,
|
package/src/commands/score.ts
CHANGED
|
@@ -34,7 +34,7 @@ const fuzzyIncludes = (text: string, query: string): boolean => {
|
|
|
34
34
|
|
|
35
35
|
const searchText = (command: AppCommand): string =>
|
|
36
36
|
normalize(
|
|
37
|
-
[command.title, command.category,
|
|
37
|
+
[command.title, command.category, ...(command.keywords ?? [])]
|
|
38
38
|
.filter((s): s is string => Boolean(s))
|
|
39
39
|
.join(" "),
|
|
40
40
|
)
|
package/src/discovery/filter.ts
CHANGED
|
@@ -3,12 +3,26 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Matching stays intentionally small and pure: case-insensitive subsequence on
|
|
5
5
|
* the filename and full relative path. Ranking prefers what users usually mean
|
|
6
|
-
* in a sidebar:
|
|
7
|
-
* - filename matches above folder-only matches
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
6
|
+
* in a sidebar (no configuration, no user knobs):
|
|
7
|
+
* - filename matches above folder-only matches (zf/fzf-inspired strong bias)
|
|
8
|
+
* - shallower / current-folder paths win over deep nested ones
|
|
9
|
+
* - incidental matches purely from deep directory names are heavily demoted
|
|
10
|
+
* so "nested folders bubbling up" does not happen for typical queries
|
|
10
11
|
*
|
|
11
12
|
* Empty query preserves discovery/tree order.
|
|
13
|
+
*
|
|
14
|
+
* The low-level fuzzyScore uses a "best alignment" scan (try every possible
|
|
15
|
+
* start position for the first query char, complete greedily, take the highest
|
|
16
|
+
* scoring match). This avoids the "early spurious char traps" a pure left-to-right
|
|
17
|
+
* greedy can hit on paths (e.g. "src/r.../readme" for query "readme" must prefer
|
|
18
|
+
* the boundary "r" in the filename, not the "r" in "src"). This draws from fzf's
|
|
19
|
+
* v1 "find occurrence then look for better" idea and the general principle that
|
|
20
|
+
* boundary+consecutive bonuses should win when they exist later in the string.
|
|
21
|
+
* We deliberately do not depend on fuzzysort/fuse/fzf here: the ranking *policy*
|
|
22
|
+
* (heavy filename bias, exact/stem bonuses, depth soft penalty) is application
|
|
23
|
+
* specific to "what makes a sidebar nice" and must be owned so end users get the
|
|
24
|
+
* good experience with zero configuration or thought. See DESIGN.md §7.4 and the
|
|
25
|
+
* filter tests.
|
|
12
26
|
*/
|
|
13
27
|
|
|
14
28
|
import type { FileEntry } from "./walk.ts"
|
|
@@ -17,19 +31,41 @@ export const fuzzyScore = (query: string, target: string): number | null => {
|
|
|
17
31
|
if (query.length === 0) return 0
|
|
18
32
|
const q = query.toLowerCase()
|
|
19
33
|
const t = target.toLowerCase()
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
let
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
if (q.length > t.length) return null
|
|
35
|
+
|
|
36
|
+
let best: number | null = null
|
|
37
|
+
|
|
38
|
+
// Best-alignment: try every viable start for q[0], then complete the rest
|
|
39
|
+
// greedily from there. Take the alignment that accumulates the most bonus
|
|
40
|
+
// points (word-start after /, consecutives). This is what gives good OOB
|
|
41
|
+
// results on tree paths without the caller doing extra work.
|
|
42
|
+
for (let start = 0; start < t.length; start++) {
|
|
43
|
+
if (t[start] !== q[0]) continue
|
|
44
|
+
|
|
45
|
+
let qi = 1
|
|
46
|
+
let score = 0
|
|
47
|
+
let lastMatch = start
|
|
48
|
+
|
|
49
|
+
// score first char
|
|
50
|
+
const isWordStart0 = start === 0 || t[start - 1] === "/"
|
|
51
|
+
score += isWordStart0 ? 10 : 1
|
|
52
|
+
|
|
53
|
+
let i = start + 1
|
|
54
|
+
for (; i < t.length && qi < q.length; i++) {
|
|
55
|
+
if (t[i] !== q[qi]) continue
|
|
56
|
+
const isWordStart = i === 0 || t[i - 1] === "/"
|
|
57
|
+
score += isWordStart ? 10 : 1
|
|
58
|
+
if (lastMatch === i - 1) score += 5
|
|
59
|
+
lastMatch = i
|
|
60
|
+
qi++
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (qi === q.length) {
|
|
64
|
+
if (best === null || score > best) best = score
|
|
65
|
+
}
|
|
30
66
|
}
|
|
31
|
-
|
|
32
|
-
return
|
|
67
|
+
|
|
68
|
+
return best
|
|
33
69
|
}
|
|
34
70
|
|
|
35
71
|
const splitPath = (relativePath: string): { fileName: string; depth: number } => {
|
|
@@ -55,15 +91,31 @@ const rankFile = (query: string, file: FileEntry): number | null => {
|
|
|
55
91
|
const stem = fileStem(fileName).toLowerCase()
|
|
56
92
|
const nameScore = fuzzyScore(query, fileName) ?? 0
|
|
57
93
|
|
|
58
|
-
let score = pathScore *
|
|
59
|
-
score += nameScore *
|
|
94
|
+
let score = pathScore * 4
|
|
95
|
+
score += nameScore * 150
|
|
60
96
|
|
|
97
|
+
// Filename (and stem) priority — the heart of good sidebar UX.
|
|
98
|
+
// Users almost always mean "the file whose name contains this", not
|
|
99
|
+
// "some directory component that happens to have these letters".
|
|
61
100
|
if (name === q || stem === q) score += 5_000
|
|
62
101
|
else if (name.startsWith(q) || stem.startsWith(q)) score += 2_000
|
|
63
102
|
else if (name.includes(q)) score += 1_000
|
|
64
103
|
|
|
65
|
-
|
|
66
|
-
|
|
104
|
+
// Strong depth bias so less-nested files appear near the top.
|
|
105
|
+
// We want shallow files to win over deep "nested folders" even when
|
|
106
|
+
// the deep file has a decent path match (common with short queries or
|
|
107
|
+
// dir names). The penalty is now large enough to matter vs. the
|
|
108
|
+
// match bonuses. Root gets a big kick. This is the main lever for
|
|
109
|
+
// "position in the tree structure" without any user config.
|
|
110
|
+
if (depth === 0) score += 800
|
|
111
|
+
score -= depth * 120
|
|
112
|
+
|
|
113
|
+
// Extra penalty when the match is weak or absent in the *basename*.
|
|
114
|
+
// This aggressively demotes results where the hit is only because the
|
|
115
|
+
// file lives under a matching deep directory ("nested folders bubbling up").
|
|
116
|
+
if (nameScore == null || nameScore < 8) {
|
|
117
|
+
score -= depth * 80
|
|
118
|
+
}
|
|
67
119
|
|
|
68
120
|
return score
|
|
69
121
|
}
|
|
@@ -73,16 +125,24 @@ const rankFile = (query: string, file: FileEntry): number | null => {
|
|
|
73
125
|
* unchanged (preserves the discovery sort order). Non-empty query keeps
|
|
74
126
|
* matches only, sorted by score desc; ties fall back to the input order so
|
|
75
127
|
* the discovery sort still leaks through.
|
|
128
|
+
*
|
|
129
|
+
* All the "make this feel right for users exploring a tree" logic lives here
|
|
130
|
+
* (and in rankFile) so callers (Browser) and end users never have to think
|
|
131
|
+
* about or configure ranking.
|
|
76
132
|
*/
|
|
77
133
|
export const filterFiles = (files: readonly FileEntry[], query: string): readonly FileEntry[] => {
|
|
78
134
|
if (query.length === 0) return files
|
|
79
|
-
const scored: { file: FileEntry; score: number; index: number }[] = []
|
|
135
|
+
const scored: { file: FileEntry; score: number; depth: number; index: number }[] = []
|
|
80
136
|
for (let i = 0; i < files.length; i++) {
|
|
81
137
|
const file = files[i]!
|
|
82
138
|
const score = rankFile(query, file)
|
|
83
139
|
if (score === null) continue
|
|
84
|
-
|
|
140
|
+
const depth = (file.relativePath.match(/\//g) || []).length
|
|
141
|
+
scored.push({ file, score, depth, index: i })
|
|
85
142
|
}
|
|
86
|
-
|
|
143
|
+
// Primary: match quality (fuzzy + name bonuses - depth penalties already in score)
|
|
144
|
+
// Secondary: shallower depth wins (less nested near top)
|
|
145
|
+
// Tertiary: stable original discovery order
|
|
146
|
+
scored.sort((a, b) => b.score - a.score || a.depth - b.depth || a.index - b.index)
|
|
87
147
|
return scored.map((s) => s.file)
|
|
88
148
|
}
|
package/src/discovery/walk.ts
CHANGED
|
@@ -13,7 +13,10 @@ export interface FileEntry {
|
|
|
13
13
|
readonly name: string
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export
|
|
16
|
+
export interface DiscoveryWarning {
|
|
17
|
+
readonly path: string
|
|
18
|
+
readonly cause: unknown
|
|
19
|
+
}
|
|
17
20
|
|
|
18
21
|
export interface WalkOptions {
|
|
19
22
|
/** Categories of normally-skipped entries to opt into. Empty (the
|
|
@@ -21,10 +24,10 @@ export interface WalkOptions {
|
|
|
21
24
|
* entries. Order is irrelevant — semantics are set membership. Hard
|
|
22
25
|
* skips (`node_modules`, `.git`, `.venv`) always apply. */
|
|
23
26
|
readonly show?: Iterable<ShowCategory>
|
|
24
|
-
/** Group order within each directory. Default `dirs-first`. */
|
|
25
|
-
readonly sort?: SortOrder
|
|
26
27
|
/** Include `.mdx` files alongside `.md`/`.markdown`. Default `true`. */
|
|
27
28
|
readonly mdx?: boolean
|
|
29
|
+
/** Non-fatal subtree read errors. Root-level failures still error the walk. */
|
|
30
|
+
readonly onWarning?: ((warning: DiscoveryWarning) => void) | null
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
export class DiscoveryError extends Data.TaggedError("DiscoveryError")<{
|
|
@@ -66,14 +69,12 @@ const tryLoadGitignore = async (dir: string): Promise<Ignore | null> => {
|
|
|
66
69
|
|
|
67
70
|
const sortEntries = <T extends { name: string; isDirectory: () => boolean }>(
|
|
68
71
|
entries: readonly T[],
|
|
69
|
-
order: SortOrder,
|
|
70
72
|
): T[] =>
|
|
71
73
|
[...entries].sort((a, b) => {
|
|
72
74
|
const aDir = a.isDirectory()
|
|
73
75
|
const bDir = b.isDirectory()
|
|
74
76
|
if (aDir !== bDir) {
|
|
75
|
-
|
|
76
|
-
return aDir ? -1 : 1
|
|
77
|
+
return aDir ? 1 : -1
|
|
77
78
|
}
|
|
78
79
|
return a.name.localeCompare(b.name)
|
|
79
80
|
})
|
|
@@ -81,7 +82,7 @@ const sortEntries = <T extends { name: string; isDirectory: () => boolean }>(
|
|
|
81
82
|
/**
|
|
82
83
|
* DFS generator. Yields each markdown FileEntry as it is discovered, before
|
|
83
84
|
* descending further. Per-directory sort still happens before yielding so
|
|
84
|
-
* arrival order
|
|
85
|
+
* arrival order is files-first and alphabetical within each group.
|
|
85
86
|
*
|
|
86
87
|
* Cancellation: `signal.aborted` is checked between syscalls. Node's
|
|
87
88
|
* `readdir` doesn't accept an AbortSignal, so a single in-flight `readdir`
|
|
@@ -92,10 +93,12 @@ async function* walkDirGen(
|
|
|
92
93
|
dirPath: string,
|
|
93
94
|
rootPath: string,
|
|
94
95
|
parentLevels: readonly IgnoreLevel[],
|
|
95
|
-
opts: { showHidden: boolean; showGitignored: boolean;
|
|
96
|
+
opts: { showHidden: boolean; showGitignored: boolean; mdx: boolean },
|
|
97
|
+
onWarning: ((warning: DiscoveryWarning) => void) | null,
|
|
96
98
|
signal: AbortSignal,
|
|
97
99
|
): AsyncGenerator<FileEntry, void, void> {
|
|
98
100
|
if (signal.aborted) return
|
|
101
|
+
const isRoot = dirPath === rootPath
|
|
99
102
|
|
|
100
103
|
let levels = parentLevels
|
|
101
104
|
if (!opts.showGitignored) {
|
|
@@ -104,10 +107,17 @@ async function* walkDirGen(
|
|
|
104
107
|
if (ig) levels = [...parentLevels, { dir: dirPath, ig }]
|
|
105
108
|
}
|
|
106
109
|
|
|
107
|
-
|
|
110
|
+
let raw
|
|
111
|
+
try {
|
|
112
|
+
raw = await readdir(dirPath, { withFileTypes: true })
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (isRoot) throw error
|
|
115
|
+
onWarning?.({ path: dirPath, cause: error })
|
|
116
|
+
return
|
|
117
|
+
}
|
|
108
118
|
if (signal.aborted) return
|
|
109
119
|
|
|
110
|
-
for (const entry of sortEntries(raw
|
|
120
|
+
for (const entry of sortEntries(raw)) {
|
|
111
121
|
if (signal.aborted) return
|
|
112
122
|
|
|
113
123
|
// Never follow symlinks — cycle hazard, and a markdown reader doesn't
|
|
@@ -120,7 +130,7 @@ async function* walkDirGen(
|
|
|
120
130
|
if (HARD_SKIP_DIRS.has(entry.name)) continue
|
|
121
131
|
if (!opts.showHidden && entry.name.startsWith(".")) continue
|
|
122
132
|
if (!opts.showGitignored && isIgnored(entryPath, true, levels)) continue
|
|
123
|
-
yield* walkDirGen(entryPath, rootPath, levels, opts, signal)
|
|
133
|
+
yield* walkDirGen(entryPath, rootPath, levels, opts, onWarning, signal)
|
|
124
134
|
continue
|
|
125
135
|
}
|
|
126
136
|
|
|
@@ -150,8 +160,7 @@ async function* walkDirGen(
|
|
|
150
160
|
* - Hidden files/dirs (leading `.`) skipped unless `show` contains `"hidden"`.
|
|
151
161
|
* - `.gitignore` honored, including nested `.gitignore` files.
|
|
152
162
|
* - Symlinks not followed.
|
|
153
|
-
* - Sort: alphabetical within each group
|
|
154
|
-
* (`dirs-first`, default) or files before directories (`files-first`).
|
|
163
|
+
* - Sort: files before directories, alphabetical within each group.
|
|
155
164
|
*/
|
|
156
165
|
export const walk = (
|
|
157
166
|
root: string,
|
|
@@ -162,13 +171,13 @@ export const walk = (
|
|
|
162
171
|
const opts = {
|
|
163
172
|
showHidden: show.has("hidden"),
|
|
164
173
|
showGitignored: show.has("gitignored"),
|
|
165
|
-
sort: options.sort ?? ("dirs-first" as SortOrder),
|
|
166
174
|
mdx: options.mdx ?? true,
|
|
167
175
|
}
|
|
176
|
+
const onWarning = options.onWarning ?? null
|
|
168
177
|
const controller = new AbortController()
|
|
169
178
|
const iterable: AsyncIterable<FileEntry> = {
|
|
170
179
|
[Symbol.asyncIterator]() {
|
|
171
|
-
const gen = walkDirGen(absRoot, absRoot, [], opts, controller.signal)
|
|
180
|
+
const gen = walkDirGen(absRoot, absRoot, [], opts, onWarning, controller.signal)
|
|
172
181
|
return {
|
|
173
182
|
next: () => gen.next(),
|
|
174
183
|
return: async (value?: void) => {
|