mikser-io 9.79.0 → 9.81.0
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/app.js +8 -2
- package/docs/diagnostics.md +15 -3
- package/package.json +1 -1
- package/src/engine.js +38 -7
- package/src/instance.js +46 -2
- package/src/plugins/assets.js +83 -5
- package/src/references.js +26 -0
package/app.js
CHANGED
|
@@ -43,10 +43,16 @@ function locate(argv) {
|
|
|
43
43
|
: tool ? { type: 'report', tool, toolArgs: value('--tool-args'), json: has('--json') }
|
|
44
44
|
: explain ? { type: 'report', explain, json: has('--json') }
|
|
45
45
|
: has('--audit-output') ? { type: 'report', auditOutput: true, json: has('--json') }
|
|
46
|
-
: { type: 'build', clear: has('--clear') }
|
|
46
|
+
: { type: 'build', clear: has('--clear'), renderPresets: has('--render-presets') ? (value('--render-presets') ?? true) : undefined }
|
|
47
47
|
|
|
48
48
|
return {
|
|
49
49
|
longRunning,
|
|
50
|
+
// The flags this process was invoked with, forwarded so the INSTANCE
|
|
51
|
+
// can reject an unknown one. This pre-parser reads the few options it
|
|
52
|
+
// needs and passes the rest along, so commander never runs on a
|
|
53
|
+
// forwarded invocation — and a typo was accepted in silence, built,
|
|
54
|
+
// and reported success.
|
|
55
|
+
argv,
|
|
50
56
|
workingFolder: value('--working-folder', '-i') ?? '.',
|
|
51
57
|
config: value('--config', '-c') ?? 'mikser.config.js',
|
|
52
58
|
request,
|
|
@@ -74,7 +80,7 @@ async function main() {
|
|
|
74
80
|
const code = await forward({
|
|
75
81
|
workingFolder,
|
|
76
82
|
config: path.resolve(where.workingFolder, where.config),
|
|
77
|
-
request: where.request,
|
|
83
|
+
request: { ...where.request, argv: where.argv },
|
|
78
84
|
})
|
|
79
85
|
// null means nobody was listening — carry on exactly as before.
|
|
80
86
|
if (code !== null) process.exit(code)
|
package/docs/diagnostics.md
CHANGED
|
@@ -882,6 +882,15 @@ surfaces that turn silence into a statement:
|
|
|
882
882
|
- **A plugin that appears to do nothing** — `No plugins loaded` with a
|
|
883
883
|
config present is a warning naming the file. A config that fails to
|
|
884
884
|
load now exits non-zero rather than loading as empty.
|
|
885
|
+
- **A derivative that did not re-render.** A preset is re-evaluated when
|
|
886
|
+
its definition moves — its `revision`, its module, or its `match`
|
|
887
|
+
patterns, all of which the preset entity now carries. Nothing else
|
|
888
|
+
schedules it: an entity whose source and preset both stood still is
|
|
889
|
+
skipped, which is what keeps a no-op build from scanning the corpus.
|
|
890
|
+
When the cause is outside all of that — a preset edited without bumping
|
|
891
|
+
`revision`, an image library upgraded under the build, a marker deleted
|
|
892
|
+
by hand — `--render-presets [name]` re-derives regardless. It fails
|
|
893
|
+
loudly if no assets plugin is loaded to act on it.
|
|
885
894
|
- **A derivative for a source that is gone.** The assets folder sits at
|
|
886
895
|
the working-folder root, outside both `outputFolder` and the runtime
|
|
887
896
|
folder, and it is symlinked INTO the output — so an orphaned derivative
|
|
@@ -905,9 +914,12 @@ surfaces that turn silence into a statement:
|
|
|
905
914
|
evidence:
|
|
906
915
|
- **The emitted output**, read back and resolved the way a browser
|
|
907
916
|
would — `src`, `href`, `poster`, `srcset` and CSS `url()` across
|
|
908
|
-
html and css.
|
|
909
|
-
|
|
910
|
-
|
|
917
|
+
html and css. This one sees paths written by hand, not just helper
|
|
918
|
+
output, and it separates two problems that share a symptom:
|
|
919
|
+
`reference-wrong-base` when the file exists elsewhere in the output
|
|
920
|
+
(the url was built from the wrong root, and the report names where
|
|
921
|
+
the file actually is), `reference-broken` when nothing produced it
|
|
922
|
+
at all.
|
|
911
923
|
- **The render track**, which records every `asset()` / `resource()`
|
|
912
924
|
call and tests the destination it built. This catches a url that
|
|
913
925
|
never reaches an html file at all — one emitted into a feed or a
|
package/package.json
CHANGED
package/src/engine.js
CHANGED
|
@@ -116,15 +116,31 @@ async function reportBrokenReferences(logger) {
|
|
|
116
116
|
const named = (files) =>
|
|
117
117
|
files.slice(0, 3).join(', ') + (files.length > 3 ? ` and ${files.length - 3} more` : '')
|
|
118
118
|
|
|
119
|
-
for (const { url, target, files } of broken.slice(0, SHOWN)) {
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
for (const { url, target, files, elsewhere } of broken.slice(0, SHOWN)) {
|
|
120
|
+
// Two different problems wear the same symptom. A target whose file
|
|
121
|
+
// exists elsewhere in the output is a base that is wrong, not an asset
|
|
122
|
+
// that is missing — and saying which one saves the reader the search.
|
|
123
|
+
if (elsewhere?.length) {
|
|
124
|
+
logger.warn({ code: 'reference-wrong-base', url, target, files, elsewhere },
|
|
125
|
+
'Points at the wrong place: %s (from %s) — nothing at %s, but the file is at %s',
|
|
126
|
+
url, named(files), target, elsewhere.join(', '))
|
|
127
|
+
} else {
|
|
128
|
+
logger.warn({ code: 'reference-broken', url, target, files },
|
|
129
|
+
'Resolves to nothing, and nothing produced it: %s (from %s) — %s',
|
|
130
|
+
url, named(files), target)
|
|
131
|
+
}
|
|
122
132
|
}
|
|
123
133
|
if (broken.length) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
broken
|
|
134
|
+
const misplaced = broken.filter(b => b.elsewhere?.length).length
|
|
135
|
+
logger.warn({
|
|
136
|
+
code: 'reference-broken-summary',
|
|
137
|
+
broken: broken.length, wrongBase: misplaced, checked,
|
|
138
|
+
},
|
|
139
|
+
'%d of %d reference(s) in the output resolve to nothing%s. Wrong base (the file exists '
|
|
140
|
+
+ 'elsewhere): %d. Never produced: %d. A URL helper builds the path rather than looking '
|
|
141
|
+
+ 'it up, so neither kind can fail at the point it is written.',
|
|
142
|
+
broken.length, checked, broken.length > SHOWN ? `, ${SHOWN} shown` : '',
|
|
143
|
+
misplaced, broken.length - misplaced)
|
|
128
144
|
}
|
|
129
145
|
|
|
130
146
|
// Grouped by how FAR each climbed, because a site whose every over-deep url
|
|
@@ -408,6 +424,7 @@ export async function setup(options) {
|
|
|
408
424
|
.option('-c --config <file>', 'set mikser mikser.config.js location', './mikser.config.js')
|
|
409
425
|
.option('-m --mode <mode>', 'set mikser runtime mode', 'development')
|
|
410
426
|
.option('-r --clear', 'clear current state before execution', false)
|
|
427
|
+
.option('--render-presets [name]', 're-render preset derivatives whose sources and revisions are unchanged; with a name, only that preset')
|
|
411
428
|
.option('-o --output-folder <folder>', 'set mikser output folder relative to working folder', 'out')
|
|
412
429
|
.option('-w --watch', 'watch entities for changes', false)
|
|
413
430
|
.option('-f --force', 'rebuild everything; disable incremental dispatch', false)
|
|
@@ -1261,6 +1278,20 @@ export async function setup(options) {
|
|
|
1261
1278
|
// Checked at the end of the cycle because that is the first moment the
|
|
1262
1279
|
// answer is stable: derivatives are produced during the cycle, so
|
|
1263
1280
|
// asking any earlier would report files that were about to appear.
|
|
1281
|
+
// --render-presets with nothing to consume it.
|
|
1282
|
+
//
|
|
1283
|
+
// The flag is implemented by the assets plugin, so without that plugin
|
|
1284
|
+
// it reaches nobody: the build runs normally, nothing is re-derived,
|
|
1285
|
+
// and the operator is left to notice. Checked here rather than at
|
|
1286
|
+
// onLoaded because the engine's own onLoaded is registered first and
|
|
1287
|
+
// runs before any plugin has set itself up.
|
|
1288
|
+
if (runtime.options.renderPresets && !runtime.state?.assets?.renderPresetsHandled) {
|
|
1289
|
+
useLogger().error({ code: 'render-presets-unhandled' },
|
|
1290
|
+
'--render-presets was passed, but no assets plugin is loaded to act on it. '
|
|
1291
|
+
+ 'Nothing was re-derived. Add assets() to the plugins array, or drop the flag.')
|
|
1292
|
+
process.exitCode = 1
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1264
1295
|
const brokenTargets = await reportBrokenReferences(useLogger())
|
|
1265
1296
|
await reportMissingAssets(useLogger(), brokenTargets)
|
|
1266
1297
|
|
package/src/instance.js
CHANGED
|
@@ -69,7 +69,7 @@ export function socketPath(workingFolder) {
|
|
|
69
69
|
// Newline-delimited JSON, one object per line. Deliberately boring: both ends
|
|
70
70
|
// ship together, so there is nothing to negotiate and no version to carry.
|
|
71
71
|
//
|
|
72
|
-
// → { type: 'build', config, clear }
|
|
72
|
+
// → { type: 'build', config, clear, renderPresets }
|
|
73
73
|
// → { type: 'report', config, tool, tools, toolArgs, explain, auditOutput, json }
|
|
74
74
|
// ← { type: 'log', chunk } (zero or more, in order)
|
|
75
75
|
// ← { type: 'done', code }
|
|
@@ -286,6 +286,39 @@ function refuseConfig(socket, request, wrongConfig) {
|
|
|
286
286
|
})
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
+
// An option the instance's own commander does not recognise.
|
|
290
|
+
//
|
|
291
|
+
// A forwarded invocation never reaches commander: app.js pre-parses the few
|
|
292
|
+
// flags it needs and forwards the rest, so `mikser --bogus` against a running
|
|
293
|
+
// watcher built normally and exited 0, while the same command with nothing
|
|
294
|
+
// listening was rejected outright. Same words, two answers, and the quiet one
|
|
295
|
+
// is the one that looks like it worked.
|
|
296
|
+
//
|
|
297
|
+
// parseOptions REPORTS unknowns without applying anything, which is what makes
|
|
298
|
+
// this safe to run against the live instance's option table.
|
|
299
|
+
function refuseUnknownFlags(socket, request) {
|
|
300
|
+
const argv = Array.isArray(request.argv) ? request.argv : null
|
|
301
|
+
if (!argv?.length) return false
|
|
302
|
+
const commander = runtime.engine?.commander
|
|
303
|
+
if (!commander) return false
|
|
304
|
+
let unknown = []
|
|
305
|
+
try {
|
|
306
|
+
unknown = commander.parseOptions([...argv]).unknown ?? []
|
|
307
|
+
} catch {
|
|
308
|
+
return false // never let a probe refuse a legitimate build
|
|
309
|
+
}
|
|
310
|
+
unknown = unknown.filter(a => a.startsWith('-'))
|
|
311
|
+
if (!unknown.length) return false
|
|
312
|
+
frame(socket, {
|
|
313
|
+
type: 'refused',
|
|
314
|
+
reason: `unknown option ${unknown.map(u => `'${u}'`).join(', ')}.`,
|
|
315
|
+
detail: 'Forwarded to the instance running in this folder, which does not recognise it. '
|
|
316
|
+
+ 'The same command with nothing listening would have been rejected too — this says so '
|
|
317
|
+
+ 'rather than building as though the flag had been understood.',
|
|
318
|
+
})
|
|
319
|
+
return true
|
|
320
|
+
}
|
|
321
|
+
|
|
289
322
|
function refuseStale(socket, movedFile) {
|
|
290
323
|
frame(socket, {
|
|
291
324
|
type: 'refused',
|
|
@@ -316,7 +349,17 @@ async function serveBuild(socket, request, logger) {
|
|
|
316
349
|
// change that prompted the request — the watermark bug wearing a
|
|
317
350
|
// different hat. Rescanning makes a forwarded build mean what a
|
|
318
351
|
// one-shot means, which is what every existing caller assumes.
|
|
319
|
-
|
|
352
|
+
// Applied for THIS cycle only. A flag the forwarded path drops is a
|
|
353
|
+
// silent no-op, which is the failure this surface exists to remove —
|
|
354
|
+
// and restoring it after keeps the watcher from forcing every later
|
|
355
|
+
// rebuild.
|
|
356
|
+
const priorRenderPresets = runtime.options.renderPresets
|
|
357
|
+
if (request.renderPresets !== undefined) runtime.options.renderPresets = request.renderPresets
|
|
358
|
+
try {
|
|
359
|
+
await runtime.rebuild()
|
|
360
|
+
} finally {
|
|
361
|
+
runtime.options.renderPresets = priorRenderPresets
|
|
362
|
+
}
|
|
320
363
|
|
|
321
364
|
// From the render-error count, NOT from process.exitCode.
|
|
322
365
|
//
|
|
@@ -363,6 +406,7 @@ export function serveInstance() {
|
|
|
363
406
|
// instance's — a report against the wrong config is the
|
|
364
407
|
// original incident, and it is wrong whether or not it
|
|
365
408
|
// writes anything.
|
|
409
|
+
if (refuseUnknownFlags(socket, request)) return
|
|
366
410
|
const wrongConfig = configMismatch(request.config)
|
|
367
411
|
if (wrongConfig) return refuseConfig(socket, request, wrongConfig)
|
|
368
412
|
const movedFile = await configStale()
|
package/src/plugins/assets.js
CHANGED
|
@@ -173,9 +173,30 @@ export function assets(options = {}) {
|
|
|
173
173
|
// the (revision, format, options) export contract so onImport and
|
|
174
174
|
// onSync stay in sync. Cache-busts local presets so watch-mode edits
|
|
175
175
|
// reload; npm presets import once (their version is the cache key).
|
|
176
|
+
// Presets whose effective definition moved this cycle: a bumped revision,
|
|
177
|
+
// an edited module, or widened patterns.
|
|
178
|
+
//
|
|
179
|
+
// The fan-out this gates is a full catalog scan, and onImport writes every
|
|
180
|
+
// preset entity on every build — so gating on "a preset is in the journal"
|
|
181
|
+
// ran that scan every cycle, including a no-op watch rebuild, where
|
|
182
|
+
// responsiveness matters most.
|
|
183
|
+
const changedPresets = new Set()
|
|
184
|
+
|
|
185
|
+
async function notePresetChange(preset) {
|
|
186
|
+
const prior = await findEntity({ id: preset.id })
|
|
187
|
+
const moved = !prior
|
|
188
|
+
|| prior.checksum !== preset.checksum
|
|
189
|
+
|| JSON.stringify(prior.matches ?? null) !== JSON.stringify(preset.matches ?? null)
|
|
190
|
+
if (moved) changedPresets.add(preset.name)
|
|
191
|
+
return moved
|
|
192
|
+
}
|
|
193
|
+
|
|
176
194
|
async function buildPreset({ name, uri, watchable }) {
|
|
177
195
|
const cacheBust = watchable ? `?stamp=${Date.now()}` : ''
|
|
178
|
-
|
|
196
|
+
// `options` here would SHADOW the plugin's own config, which the
|
|
197
|
+
// patterns below need. The module's export and the factory argument
|
|
198
|
+
// are two different things that were both called options.
|
|
199
|
+
const { revision = 1, format, options: moduleOptions } = await import(`${uri}${cacheBust}`)
|
|
179
200
|
return {
|
|
180
201
|
id: `/presets/${name}`,
|
|
181
202
|
collection,
|
|
@@ -185,7 +206,17 @@ export function assets(options = {}) {
|
|
|
185
206
|
source: uri,
|
|
186
207
|
format,
|
|
187
208
|
checksum: revision,
|
|
188
|
-
|
|
209
|
+
// The patterns this preset selects by, carried on the entity.
|
|
210
|
+
//
|
|
211
|
+
// They are half of what decides which files a preset owns, and
|
|
212
|
+
// they live in CONFIG rather than in the module — so `revision`
|
|
213
|
+
// alone cannot say the preset's effective definition moved.
|
|
214
|
+
// Widening a pattern was a silent no-op: match is evaluated as a
|
|
215
|
+
// file ENTERS the catalog, so a wider one left everything already
|
|
216
|
+
// in it alone, the build stayed green and the derivative never
|
|
217
|
+
// appeared.
|
|
218
|
+
matches: normalizePresetConfig(options.presets?.[name]).matches,
|
|
219
|
+
options: moduleOptions,
|
|
189
220
|
}
|
|
190
221
|
}
|
|
191
222
|
|
|
@@ -251,7 +282,13 @@ export function assets(options = {}) {
|
|
|
251
282
|
destination = changeExtension(destination, entity.preset.format)
|
|
252
283
|
}
|
|
253
284
|
entity.destination = path.join(runtime.options.assetsFolder, entityPreset, destination)
|
|
254
|
-
|
|
285
|
+
// Two gates sit between a scheduled entity and a render: the
|
|
286
|
+
// manifest's "its source did not change", and this plugin's
|
|
287
|
+
// marker. A forced render clears both — a marker at the
|
|
288
|
+
// current revision is exactly what --render-presets exists to
|
|
289
|
+
// disregard.
|
|
290
|
+
const forced = rendererChanged.has(entityToRender.id)
|
|
291
|
+
const ignore = forced ? false : await isPresetRendered(entity)
|
|
255
292
|
tasks.push({
|
|
256
293
|
entity,
|
|
257
294
|
options: {
|
|
@@ -261,7 +298,7 @@ export function assets(options = {}) {
|
|
|
261
298
|
// The preset itself moved this cycle, so the manifest's
|
|
262
299
|
// "its source is unchanged" is true and beside the
|
|
263
300
|
// point. See the skip decision in engine.js.
|
|
264
|
-
rendererChanged:
|
|
301
|
+
rendererChanged: forced,
|
|
265
302
|
ignore
|
|
266
303
|
}
|
|
267
304
|
})
|
|
@@ -397,6 +434,7 @@ export function assets(options = {}) {
|
|
|
397
434
|
const uri = path.join(runtime.options.presetsFolder, relativePath)
|
|
398
435
|
try {
|
|
399
436
|
const preset = await buildPreset({ name, uri, watchable: true })
|
|
437
|
+
await notePresetChange(preset)
|
|
400
438
|
await createEntity(preset)
|
|
401
439
|
presets[name] = preset
|
|
402
440
|
} catch (err) {
|
|
@@ -421,6 +459,7 @@ export function assets(options = {}) {
|
|
|
421
459
|
}
|
|
422
460
|
try {
|
|
423
461
|
const preset = await buildPreset({ name, uri: resolved.uri, watchable: resolved.watchable })
|
|
462
|
+
await notePresetChange(preset)
|
|
424
463
|
await createEntity(preset)
|
|
425
464
|
presets[name] = preset
|
|
426
465
|
logger.debug('Preset loaded from npm: mikser-io-preset-%s', name)
|
|
@@ -480,10 +519,49 @@ export function assets(options = {}) {
|
|
|
480
519
|
}
|
|
481
520
|
|
|
482
521
|
const entitiesToRender = new Map()
|
|
483
|
-
// Entities
|
|
522
|
+
// Entities that must render regardless of markers or manifest: their
|
|
523
|
+
// preset moved, or --render-presets asked for them.
|
|
484
524
|
const presetMoved = new Set()
|
|
525
|
+
|
|
526
|
+
// --render-presets [name]: re-derive though nothing moved.
|
|
527
|
+
//
|
|
528
|
+
// The escape hatch for what the incremental machinery cannot see — a
|
|
529
|
+
// preset edited without bumping `revision`, a marker deleted by hand,
|
|
530
|
+
// an image library upgraded underneath the build. --clear reaches the
|
|
531
|
+
// same end only by rebuilding the whole site, and only at startup, so
|
|
532
|
+
// it cannot be asked of a running watcher at all.
|
|
533
|
+
const wanted = runtime.options.renderPresets
|
|
534
|
+
if (wanted) {
|
|
535
|
+
// Consumed here and nowhere else. The engine checks this at the
|
|
536
|
+
// end of the cycle: a flag that reaches no plugin has to say so,
|
|
537
|
+
// rather than building normally and leaving the operator to
|
|
538
|
+
// notice nothing was re-derived.
|
|
539
|
+
runtime.state.assets.renderPresetsHandled = true
|
|
540
|
+
|
|
541
|
+
const known = Object.keys(runtime.state.assets.presets)
|
|
542
|
+
const names = wanted === true ? known : [wanted]
|
|
543
|
+
for (const name of names.filter(n => !known.includes(n))) {
|
|
544
|
+
useLogger().warn({ code: 'preset-unknown', preset: name },
|
|
545
|
+
'--render-presets asked for %j, which is not configured. Known: %s',
|
|
546
|
+
name, known.join(', ') || '(none)')
|
|
547
|
+
}
|
|
548
|
+
const selected = names.filter(n => known.includes(n))
|
|
549
|
+
if (selected.length) {
|
|
550
|
+
for await (const candidate of iterateEntities({ collection: { $ne: collection } })) {
|
|
551
|
+
const candidatePresets = await getEntityPresets(candidate)
|
|
552
|
+
if (!candidatePresets.some(n => selected.includes(n))) continue
|
|
553
|
+
assetsMap[candidate.id] ??= candidatePresets
|
|
554
|
+
presetMoved.add(candidate.id)
|
|
555
|
+
entitiesToRender.set(candidate.id, candidate)
|
|
556
|
+
}
|
|
557
|
+
useLogger().info('Presets re-rendering: %s (%d source(s))',
|
|
558
|
+
selected.join(', '), entitiesToRender.size)
|
|
559
|
+
}
|
|
560
|
+
}
|
|
485
561
|
await map(useJournal('Assets provision', [OPERATION.CREATE, OPERATION.UPDATE], signal), async ({ entity }) => {
|
|
486
562
|
if (entity.collection == collection) {
|
|
563
|
+
// Only when this preset's definition actually moved.
|
|
564
|
+
if (!changedPresets.has(entity.name)) return
|
|
487
565
|
// A preset moved — its `revision` was bumped, or its module
|
|
488
566
|
// changed. Everything that uses it has to re-render.
|
|
489
567
|
//
|
package/src/references.js
CHANGED
|
@@ -173,5 +173,31 @@ export async function checkReferences(outputFolder, { siteRoots = [] } = {}) {
|
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
// "It was never written" and "it is written somewhere else" are different
|
|
177
|
+
// problems with one symptom, and they were reported with one sentence.
|
|
178
|
+
//
|
|
179
|
+
// A missing target whose FILE exists elsewhere in the output is almost
|
|
180
|
+
// always a base problem: the url was built from the wrong root, or with a
|
|
181
|
+
// segment too many. Naming where the file actually is turns "resolves to
|
|
182
|
+
// nothing" into the answer. A target that exists nowhere really was never
|
|
183
|
+
// produced — a preset that did not run, an extension nothing emits.
|
|
184
|
+
//
|
|
185
|
+
// Indexed only when something is broken, so a clean build pays nothing.
|
|
186
|
+
if (broken.size) {
|
|
187
|
+
const byName = new Map()
|
|
188
|
+
for (const file of await globby('**/*', {
|
|
189
|
+
cwd: outputFolder, followSymbolicLinks: true, onlyFiles: true, suppressErrors: true,
|
|
190
|
+
})) {
|
|
191
|
+
const name = path.basename(file)
|
|
192
|
+
if (!byName.has(name)) byName.set(name, [])
|
|
193
|
+
byName.get(name).push(file)
|
|
194
|
+
}
|
|
195
|
+
for (const entry of broken.values()) {
|
|
196
|
+
const elsewhere = (byName.get(path.basename(entry.target)) ?? [])
|
|
197
|
+
.filter(f => f !== entry.target)
|
|
198
|
+
if (elsewhere.length) entry.elsewhere = elsewhere.slice(0, 3)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
176
202
|
return { broken: [...broken.values()], overDeep: [...overDeepRefs.values()], checked }
|
|
177
203
|
}
|