dreamteamer 0.8.0 → 0.9.1
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/package.json +1 -1
- package/src/compile.js +7 -1
- package/src/schema-ops.js +179 -24
- package/src/store.js +19 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dreamteamer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "A workspace compiler for coding agents \u2014 schema-validated records as plain files over git, compiled into every harness",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Gilad Khen <giladkhen@gmail.com>",
|
package/src/compile.js
CHANGED
|
@@ -840,7 +840,13 @@ export function compile({ root, pkg }) {
|
|
|
840
840
|
// clear each kind's folder, plus `system/` — a runtime compiled by a pre-flatten engine has the
|
|
841
841
|
// whole tree under there, and leaving it would keep stale descriptors on disk beside the fresh
|
|
842
842
|
// ones. Never `rm -rf` the runtime root itself: it also holds the write lock.
|
|
843
|
-
|
|
843
|
+
// ⚠ DERIVED_KINDS too, not just KINDS. `modules/` is projected rather than staged, so it was not
|
|
844
|
+
// in this loop and never got cleared — a module that was RENAMED or REMOVED left its old record
|
|
845
|
+
// behind forever, listing collections that no longer exist. `check` reads those records like any
|
|
846
|
+
// other, so it surfaced as a dangling reference in a file nobody had touched, twice in one day
|
|
847
|
+
// (`hq3-workspace` after the workspace-module rename, and again after `crm` was folded in). The
|
|
848
|
+
// runtime is build output; stale build output is the compiler's problem, not the reader's.
|
|
849
|
+
for (const kind of [...KINDS, ...DERIVED_KINDS]) fs.rmSync(path.join(RUNTIME, kind), { recursive: true, force: true });
|
|
844
850
|
fs.rmSync(path.join(RUNTIME, 'system'), { recursive: true, force: true });
|
|
845
851
|
fs.rmSync(path.join(RUNTIME, 'ui'), { recursive: true, force: true });
|
|
846
852
|
for (const [rt, e] of entries) {
|
package/src/schema-ops.js
CHANGED
|
@@ -64,6 +64,81 @@ export function workspaceSystemDir(ws, kind) {
|
|
|
64
64
|
return kindDir(wm ? path.join(ws.root, 'modules', wm) : ws.root, kind);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* WHERE A COLLECTION'S DESCRIPTOR ACTUALLY LIVES — asked of the manifest, not assumed.
|
|
69
|
+
*
|
|
70
|
+
* `renameCollection` used to derive this from `workspaceSystemDir`, which silently meant "only the
|
|
71
|
+
* workspace module's own collections can be renamed". That is the wrong line. The guard exists to
|
|
72
|
+
* stop a write that will be ERASED, and the thing that erases writes is `npm install` — so the test
|
|
73
|
+
* is `node_modules/`, not "which module". A module whose sources are inline in the workspace repo is
|
|
74
|
+
* under the same git history as everything else and is perfectly safe to rewrite; refusing it made
|
|
75
|
+
* `collections rename` unusable for exactly the migration it was built for, because a workspace's
|
|
76
|
+
* domain collections almost always live in a module.
|
|
77
|
+
*
|
|
78
|
+
* Returns `{ dir, sources }` — the kind dir to write into (the SAME module the descriptor came from,
|
|
79
|
+
* so a rename never teleports a collection into the workspace module), and every descriptor source
|
|
80
|
+
* that contributed, so the caller can refuse the cases this cannot honestly do.
|
|
81
|
+
*/
|
|
82
|
+
function descriptorSourceDir(ws, name) {
|
|
83
|
+
const entry = readManifest(ws.root)?.entries?.[`collections/${name}.collection.yaml`];
|
|
84
|
+
// `sources` mixes the descriptor with any collection-templates it merged, so match on the shape
|
|
85
|
+
// of a descriptor path for THIS collection. A namespaced name is nested, hence the full suffix.
|
|
86
|
+
const suffix = `collections/${name}.collection.yaml`;
|
|
87
|
+
const sources = (entry?.sources ?? [])
|
|
88
|
+
.map((s) => s.path)
|
|
89
|
+
.filter((p) => p.endsWith(suffix));
|
|
90
|
+
if (!sources.length) return { dir: null, sources };
|
|
91
|
+
// The BASE descriptor is the one to move. With an overlay present there are two, and the overlay's
|
|
92
|
+
// `extends` names the base by its old qualified id — rewriting that is a second, different
|
|
93
|
+
// migration, so the caller refuses rather than half-doing it.
|
|
94
|
+
const moduleRoot = path.join(ws.root, sources[0].slice(0, sources[0].length - suffix.length));
|
|
95
|
+
return { dir: kindDir(moduleRoot, 'collections'), sources };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Set one scalar in a YAML document TEXTUALLY, so comments and key order survive.
|
|
100
|
+
*
|
|
101
|
+
* This exists because `load` → mutate → `dump` is lossy in the one way that matters here: it drops
|
|
102
|
+
* every comment. That is fine for a generated artifact and wrong for a module SOURCE, which is where
|
|
103
|
+
* this project writes down why a collection exists. Only `renameCollection` uses it, and only for the
|
|
104
|
+
* three scalars a rename changes; anything more ambitious belongs in a real round-trip YAML library,
|
|
105
|
+
* not in a regex.
|
|
106
|
+
*
|
|
107
|
+
* Handles both spellings the descriptors actually use — a top-level key, a nested block mapping, and
|
|
108
|
+
* the inline `storage: { path: x, suffix: y }` flow form. Callers MUST re-parse and assert, because a
|
|
109
|
+
* shape not covered here fails by changing nothing rather than by throwing.
|
|
110
|
+
*/
|
|
111
|
+
function setScalar(text, keyPath, value) {
|
|
112
|
+
const [head, child] = keyPath;
|
|
113
|
+
if (!child) return text.replace(new RegExp(`^${head}:.*$`, 'm'), `${head}: ${value}`);
|
|
114
|
+
|
|
115
|
+
// inline flow mapping: `storage: { path: data/x, suffix: y }`
|
|
116
|
+
const flow = new RegExp(`^${head}:\\s*\\{([^}]*)\\}\\s*$`, 'm').exec(text);
|
|
117
|
+
if (flow) {
|
|
118
|
+
let body = flow[1];
|
|
119
|
+
body = new RegExp(`\\b${child}:\\s*[^,}]+`).test(body)
|
|
120
|
+
? body.replace(new RegExp(`(\\b${child}:\\s*)[^,}]+`), `$1${value}`)
|
|
121
|
+
: `${body.trimEnd()}, ${child}: ${value}`;
|
|
122
|
+
return text.slice(0, flow.index) + `${head}: {${body}}` + text.slice(flow.index + flow[0].length);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// block mapping: `storage:\n path: data/x`
|
|
126
|
+
const block = new RegExp(`^${head}:\\n(?:[ \\t]+.*\\n)*?[ \\t]+${child}:.*$`, 'm').exec(text);
|
|
127
|
+
if (block) {
|
|
128
|
+
return text.slice(0, block.index)
|
|
129
|
+
+ block[0].replace(new RegExp(`([ \\t]+${child}:).*$`, 'm'), `$1 ${value}`)
|
|
130
|
+
+ text.slice(block.index + block[0].length);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// the key is absent under an existing block — insert it directly after the parent
|
|
134
|
+
const parent = new RegExp(`^${head}:\\s*$`, 'm').exec(text);
|
|
135
|
+
if (parent) {
|
|
136
|
+
const at = parent.index + parent[0].length + 1;
|
|
137
|
+
return text.slice(0, at) + ` ${child}: ${value}\n` + text.slice(at);
|
|
138
|
+
}
|
|
139
|
+
return text;
|
|
140
|
+
}
|
|
141
|
+
|
|
67
142
|
// ---- ops ------------------------------------------------------------------------
|
|
68
143
|
|
|
69
144
|
export function createCollection(ws, store, { name, template, namespace }) {
|
|
@@ -143,6 +218,15 @@ export function removeCollection(ws, store, name, { force = false } = {}) {
|
|
|
143
218
|
* already scopes prose to `[[wikilinks]]` (decision 7) — a fresh `oldName/` pattern would have to
|
|
144
219
|
* relearn both, and would corrupt `data/tasks/` in a path or a URL on its first outing. N passes over
|
|
145
220
|
* the record files is the price, and at human scale it is worth paying for reusing the correct code.
|
|
221
|
+
*
|
|
222
|
+
* ⚠ MEASURED 2026-08-17, so the cost is a number rather than a hope: a 2,291-record collection in a
|
|
223
|
+
* 3,391-file workspace — gk-brain's `finance-transactions` — takes **3 minutes**, of which 142s is
|
|
224
|
+
* system time. That is 7.7M file reads to rewrite ZERO references, because the pass runs per id
|
|
225
|
+
* whether or not anything points at the collection. Tolerable for a one-time migration and left
|
|
226
|
+
* alone on that basis; it is O(records x files), so a workspace 3x larger pays 27 minutes. The fix
|
|
227
|
+
* when it is needed is a batch entry point on the store that reads each file ONCE and loops the ref
|
|
228
|
+
* set in memory, with `text.includes(oldName + '/')` as a cheap NEGATIVE filter only — never as the
|
|
229
|
+
* matcher, for the reason above.
|
|
146
230
|
*/
|
|
147
231
|
export function renameCollection(ws, store, oldName, newName) {
|
|
148
232
|
const d = store.descriptor(oldName); // throws with the known-collection list if absent
|
|
@@ -156,10 +240,21 @@ export function renameCollection(ws, store, oldName, newName) {
|
|
|
156
240
|
if (store.descriptors.has(newName)) throw new Error(`collection "${newName}" already exists`);
|
|
157
241
|
if (d.storage.base === 'runtime') throw new Error(`"${oldName}" is a compiled source, not a data collection — it cannot be renamed`);
|
|
158
242
|
|
|
159
|
-
|
|
160
|
-
|
|
243
|
+
// The descriptor is renamed IN THE MODULE THAT SHIPS IT — see `descriptorSourceDir`. Two cases
|
|
244
|
+
// this refuses, both because doing them halfway is worse than not doing them:
|
|
245
|
+
const { dir: sourceDir, sources } = descriptorSourceDir(ws, oldName);
|
|
246
|
+
if (sources.length > 1) {
|
|
247
|
+
throw new Error(`"${oldName}" is overlaid — ${sources.length} modules contribute a descriptor (${sources.join(', ')}).\n the overlay's \`extends\` names the base by its current id, so renaming the base alone would break it. merge or remove the overlay first.`);
|
|
248
|
+
}
|
|
249
|
+
if (sources.some((p) => p.split(path.sep).includes('node_modules'))) {
|
|
250
|
+
throw new Error(`"${oldName}" ships from node_modules (${sources[0]}) — a write there is erased by the next \`npm install\`. rename it in its own repo and release, or overlay it with \`extends\`.`);
|
|
251
|
+
}
|
|
252
|
+
const src = sourceDir
|
|
253
|
+
? path.join(sourceDir, `${oldName}.collection.yaml`)
|
|
254
|
+
: path.join(workspaceSystemDir(ws, 'collections'), `${oldName}.collection.yaml`);
|
|
255
|
+
const dest = path.join(sourceDir ?? workspaceSystemDir(ws, 'collections'), `${newName}.collection.yaml`);
|
|
161
256
|
if (!fs.existsSync(src)) {
|
|
162
|
-
throw new Error(`"${oldName}"
|
|
257
|
+
throw new Error(`"${oldName}" has no writable descriptor source — the manifest names none under a module in this workspace. it may be contributed by the engine itself; overlay it with \`extends\` instead.`);
|
|
163
258
|
}
|
|
164
259
|
|
|
165
260
|
const doc = load(fs.readFileSync(src, 'utf8'));
|
|
@@ -212,21 +307,66 @@ export function renameCollection(ws, store, oldName, newName) {
|
|
|
212
307
|
};
|
|
213
308
|
for (const id of ids) captureRefs(`${oldName}/${id}`);
|
|
214
309
|
captureRefs(`collections/${oldName}`);
|
|
215
|
-
const restoreRefs = () => {
|
|
310
|
+
const restoreRefs = () => {
|
|
311
|
+
for (const [f, bytes] of refFiles) {
|
|
312
|
+
fs.mkdirSync(path.dirname(f), { recursive: true }); // pruneEmpty may have taken the parent
|
|
313
|
+
fs.writeFileSync(f, bytes);
|
|
314
|
+
}
|
|
315
|
+
};
|
|
216
316
|
|
|
217
317
|
const touched = new Set();
|
|
218
318
|
let rewrites = 0;
|
|
219
319
|
try {
|
|
220
|
-
// 1. the descriptor source, at its new path
|
|
320
|
+
// 1. the descriptor source, at its new path — EDITED TEXTUALLY, never re-dumped.
|
|
321
|
+
//
|
|
322
|
+
// ⚠ `fs.writeFileSync(dest, dump(doc))` destroyed every comment in the descriptor, and a
|
|
323
|
+
// descriptor's comments are where this project keeps its reasoning: 194 lines across 24
|
|
324
|
+
// files in one real migration, including 22-line headers stating what belongs in a
|
|
325
|
+
// collection and which failure mode it guards against. The record survived; the thinking
|
|
326
|
+
// did not, and nothing said so.
|
|
327
|
+
//
|
|
328
|
+
// A rename changes exactly three scalars. Rewriting those three in place keeps the
|
|
329
|
+
// comments, the key order and the author's formatting — and the parse afterwards proves
|
|
330
|
+
// the edit landed rather than trusting the regex.
|
|
331
|
+
const edited = setScalar(setScalar(setScalar(srcBytes.toString('utf8'),
|
|
332
|
+
['name'], newName),
|
|
333
|
+
['storage', 'path'], newPath),
|
|
334
|
+
['storage', 'suffix'], newSuffix);
|
|
335
|
+
const parsed = load(edited);
|
|
336
|
+
if (parsed?.name !== newName || parsed?.storage?.path !== newPath || parsed?.storage?.suffix !== newSuffix) {
|
|
337
|
+
throw new Error(`could not rewrite ${path.relative(ws.root, src)} in place — name/storage.path/storage.suffix did not take. nothing was changed.`);
|
|
338
|
+
}
|
|
221
339
|
doc.name = newName;
|
|
222
340
|
doc.storage = { ...doc.storage, path: newPath, suffix: newSuffix };
|
|
223
341
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
224
|
-
fs.writeFileSync(dest,
|
|
342
|
+
fs.writeFileSync(dest, edited);
|
|
225
343
|
if (dest !== src) fs.rmSync(src);
|
|
226
344
|
touched.add(src);
|
|
227
345
|
touched.add(dest);
|
|
228
346
|
|
|
229
|
-
// 2.
|
|
347
|
+
// 2. INBOUND REFERENCES FIRST, while the records are still where the store thinks they are.
|
|
348
|
+
//
|
|
349
|
+
// ⚠ This used to run AFTER the folder move and it silently missed every SELF-reference.
|
|
350
|
+
// `store.rewriteRefs` walks `recordFiles()`, which resolves each collection's directory
|
|
351
|
+
// from the descriptor loaded when the Store was built — i.e. the OLD `storage.path`. Move
|
|
352
|
+
// the records first and that walk finds an empty directory, so a record pointing at its
|
|
353
|
+
// own collection is never rewritten and dangles the moment compile catches up.
|
|
354
|
+
//
|
|
355
|
+
// It is not a corner case: it hit `finance/accounts`, where every card and loan carries
|
|
356
|
+
// `settled_by: <the account that settles it>` — 5 dangling refs out of 11 records, found
|
|
357
|
+
// only because `check` ran afterwards. Doing the rewrite first needs no descriptor reload
|
|
358
|
+
// and no second code path: the files are still at the old path, which is exactly what the
|
|
359
|
+
// old refs say.
|
|
360
|
+
for (const id of ids) {
|
|
361
|
+
const out = store.rewriteRefs(`${oldName}/${id}`, `${newName}/${id}`);
|
|
362
|
+
rewrites += out.rewrites;
|
|
363
|
+
for (const f of out.touched) touched.add(f);
|
|
364
|
+
}
|
|
365
|
+
const collOut = store.rewriteRefs(`collections/${oldName}`, `collections/${newName}`);
|
|
366
|
+
rewrites += collOut.rewrites;
|
|
367
|
+
for (const f of collOut.touched) touched.add(f);
|
|
368
|
+
|
|
369
|
+
// 3. the record folder, then the per-file suffix if it was derived
|
|
230
370
|
if (newDir !== oldDir && fs.existsSync(oldDir)) {
|
|
231
371
|
fs.mkdirSync(path.dirname(newDir), { recursive: true });
|
|
232
372
|
fs.renameSync(oldDir, newDir);
|
|
@@ -244,33 +384,48 @@ export function renameCollection(ws, store, oldName, newName) {
|
|
|
244
384
|
}
|
|
245
385
|
if (movedData) { touched.add(oldDir); touched.add(newDir); }
|
|
246
386
|
|
|
247
|
-
// 3. inbound references: per record id, plus the collection's own id in `collections`
|
|
248
|
-
// (which is what ui-views and command-bindings point at).
|
|
249
|
-
for (const id of ids) {
|
|
250
|
-
const out = store.rewriteRefs(`${oldName}/${id}`, `${newName}/${id}`);
|
|
251
|
-
rewrites += out.rewrites;
|
|
252
|
-
for (const f of out.touched) touched.add(f);
|
|
253
|
-
}
|
|
254
|
-
const collOut = store.rewriteRefs(`collections/${oldName}`, `collections/${newName}`);
|
|
255
|
-
rewrites += collOut.rewrites;
|
|
256
|
-
for (const f of collOut.touched) touched.add(f);
|
|
257
|
-
|
|
258
387
|
// 4. bare `x-reference: <oldName>` in every descriptor SOURCE. Not a `<collection>/<id>`
|
|
259
|
-
// ref, so step
|
|
388
|
+
// ref, so step 2 cannot see it — and leaving it makes compile fail on an unknown target.
|
|
389
|
+
//
|
|
390
|
+
// ⚠ TEXTUAL, for the same reason step 1 is. This used to `load` → mutate → `dump`, which
|
|
391
|
+
// meant that ANY descriptor needing a retarget lost every comment in it — including the
|
|
392
|
+
// renamed one itself when it self-references, which is how step 1's careful preservation
|
|
393
|
+
// was undone one step later. 17 of the 24 descriptors stripped in the migration that
|
|
394
|
+
// found this were stripped HERE, not there.
|
|
395
|
+
//
|
|
396
|
+
// `retargetRefs` still decides WHETHER a file is affected — it walks the parsed schema and
|
|
397
|
+
// knows about nested properties and `items` — but the write is a line edit, and the parse
|
|
398
|
+
// afterwards proves it landed.
|
|
260
399
|
for (const f of descriptorSources(ws, store)) {
|
|
261
400
|
const before = fs.readFileSync(f, 'utf8');
|
|
262
|
-
const
|
|
263
|
-
if (!
|
|
401
|
+
const probe = load(before);
|
|
402
|
+
if (!probe || !retargetRefs(probe.schema, oldName, newName)) continue;
|
|
403
|
+
// ⚠ the boundary must cover BOTH spellings. A descriptor may write the block form
|
|
404
|
+
// (`x-reference: accounts` to end of line) or the inline flow form
|
|
405
|
+
// (`{ type: string, x-reference: accounts }`), where the value ends at `,` or `}`.
|
|
406
|
+
// Anchoring on `$` alone silently matched nothing in the flow form — and the assert
|
|
407
|
+
// below turned that silence into a refusal, which is how it was found.
|
|
408
|
+
const after = before.replace(
|
|
409
|
+
new RegExp(`(x-reference:\\s*)(['"]?)${oldName.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&')}\\2(?=\\s*(?:[,}]|#|$))`, 'gm'),
|
|
410
|
+
(_m, lead) => `${lead}${newName.includes('/') ? `'${newName}'` : newName}`);
|
|
411
|
+
const reparsed = load(after);
|
|
412
|
+
if (!reparsed || retargetRefs(reparsed.schema, oldName, newName)) {
|
|
413
|
+
throw new Error(`could not retarget x-reference "${oldName}" in ${path.relative(ws.root, f)} without reformatting it — nothing was changed.`);
|
|
414
|
+
}
|
|
264
415
|
if (!refFiles.has(f)) refFiles.set(f, Buffer.from(before));
|
|
265
|
-
fs.writeFileSync(f,
|
|
416
|
+
fs.writeFileSync(f, after);
|
|
266
417
|
touched.add(f);
|
|
267
418
|
rewrites++;
|
|
268
419
|
}
|
|
269
420
|
|
|
270
421
|
compile(ws); // the gate: an uncompilable rename never reaches history
|
|
271
422
|
} catch (e) {
|
|
272
|
-
|
|
423
|
+
// ⚠ undo() FIRST. A captured file can be a SELF-reference — a record of the collection being
|
|
424
|
+
// renamed — so its path only exists again once undo() has moved the folder back. Restoring
|
|
425
|
+
// before that wrote into a directory that was no longer there, and the ENOENT masked the
|
|
426
|
+
// error actually being rolled back from.
|
|
273
427
|
undo();
|
|
428
|
+
restoreRefs();
|
|
274
429
|
try { compile(ws); } catch { /* pre-rename sources were compilable */ }
|
|
275
430
|
throw e;
|
|
276
431
|
}
|
|
@@ -287,8 +442,8 @@ export function renameCollection(ws, store, oldName, newName) {
|
|
|
287
442
|
execFileSync('git', ['commit', '--quiet', '-m', `dreamteamer: collections rename ${oldName} → ${newName}`, '--', ...rels], { cwd: ws.root, stdio: GIT_QUIET });
|
|
288
443
|
} catch (e) {
|
|
289
444
|
try { execFileSync('git', ['reset', '--quiet', '--', ...rels], { cwd: ws.root, stdio: GIT_QUIET }); } catch { /* nothing staged */ }
|
|
290
|
-
restoreRefs();
|
|
291
445
|
undo();
|
|
446
|
+
restoreRefs();
|
|
292
447
|
try { compile(ws); } catch { /* pre-rename sources were compilable */ }
|
|
293
448
|
throw new Error(`git commit failed — the rename was rolled back, nothing was changed. (${e.message.split('\n')[0]})`);
|
|
294
449
|
}
|
package/src/store.js
CHANGED
|
@@ -299,13 +299,31 @@ export class Store {
|
|
|
299
299
|
return new RegExp(`${ref.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\w/-])`, 'g');
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
+
// ⚠ EACH FILE EXACTLY ONCE. The `modules` collection's storage.path is `modules` and
|
|
303
|
+
// `sourceRoots()` includes the workspace root, so walking it RE-YIELDS every module source that
|
|
304
|
+
// its own kind's walk already produced — 173 files in one real vault, every module source
|
|
305
|
+
// among them.
|
|
306
|
+
//
|
|
307
|
+
// That was harmless while every rewrite was idempotent, and stopped being harmless the day
|
|
308
|
+
// namespaces arrived: replacing `draft-docs/x` with `rnd/draft-docs/x` is NOT idempotent,
|
|
309
|
+
// because the result still contains the pattern. A second pass wrote
|
|
310
|
+
// `data/rnd/rnd/draft-docs/x` into module-source comments during a real migration. Dedupe HERE
|
|
311
|
+
// rather than making each caller idempotent — `findInboundRefs` is a caller too, and its counts
|
|
312
|
+
// were quietly doubled by the same walk.
|
|
302
313
|
*recordFiles() {
|
|
314
|
+
const seen = new Set();
|
|
303
315
|
for (const d of this.descriptors.values()) {
|
|
304
316
|
// for runtime-based collections, inbound-ref surgery targets SOURCES, not the runtime
|
|
305
317
|
const roots = d.storage.base === 'runtime' ? this.sourceRoots() : [this.root];
|
|
306
318
|
for (const srcRoot of roots) {
|
|
307
319
|
const dir = path.join(srcRoot, d.storage.path);
|
|
308
|
-
if (fs.existsSync(dir))
|
|
320
|
+
if (!fs.existsSync(dir)) continue;
|
|
321
|
+
for (const f of walk(dir)) {
|
|
322
|
+
const key = path.resolve(f);
|
|
323
|
+
if (seen.has(key)) continue;
|
|
324
|
+
seen.add(key);
|
|
325
|
+
yield f;
|
|
326
|
+
}
|
|
309
327
|
}
|
|
310
328
|
}
|
|
311
329
|
}
|