mikser-io 10.10.0 → 10.11.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/package.json +1 -1
- package/src/engine.js +17 -5
- package/src/manifest.js +47 -19
- package/src/plugins/assets.js +91 -37
package/package.json
CHANGED
package/src/engine.js
CHANGED
|
@@ -835,7 +835,17 @@ The full version, with what each code means: docs/diagnostics.md`)
|
|
|
835
835
|
// creates for itself. By `import` every onLoaded has run and the
|
|
836
836
|
// registry is complete. Nothing is imported, because this exits first,
|
|
837
837
|
// the same way --explain and --audit-output do.
|
|
838
|
-
|
|
838
|
+
//
|
|
839
|
+
// --audit-output is here for the same reason, found the same way. It
|
|
840
|
+
// asks which trees hold outputs, and a plugin cannot answer until its
|
|
841
|
+
// own onLoaded has run — the assets plugin resolves the folder its
|
|
842
|
+
// derivatives live in there, because `--assets` is a plugin option
|
|
843
|
+
// and stage two of the parse is what makes it readable. Dispatched
|
|
844
|
+
// from the engine's onLoaded it ran ahead of all of them, so the one
|
|
845
|
+
// check that reports unclaimed files could not see the one tree whose
|
|
846
|
+
// files reach the site through a symlink it does not follow. It
|
|
847
|
+
// reported `0 orphaned` over any amount of debris.
|
|
848
|
+
if (runtime.options.tools || runtime.options.tool || runtime.options.auditOutput) {
|
|
839
849
|
const code = await runReportOnly()
|
|
840
850
|
if (code !== null) process.exit(code)
|
|
841
851
|
}
|
|
@@ -877,14 +887,16 @@ The full version, with what each code means: docs/diagnostics.md`)
|
|
|
877
887
|
// one implementation, so a forwarded --audit-output cannot disagree with a
|
|
878
888
|
// local one about what it checked.
|
|
879
889
|
//
|
|
880
|
-
// --tools and --
|
|
881
|
-
// instead, because
|
|
882
|
-
// plugin's onLoaded has run and this hook runs
|
|
890
|
+
// --tools, --tool and --audit-output are NOT among them: they are
|
|
891
|
+
// dispatched at `import` instead, because what they read is not
|
|
892
|
+
// complete until every plugin's onLoaded has run and this hook runs
|
|
893
|
+
// ahead of all of them — the tool registry for the first two, the set
|
|
894
|
+
// of trees that hold outputs for the third.
|
|
883
895
|
// The import dispatch existed already and was documented as
|
|
884
896
|
// load-bearing, but this call reached the same code first and exited,
|
|
885
897
|
// so it never ran — and every tool a plugin registers was missing from
|
|
886
898
|
// the CLI while the listing looked healthy, just short.
|
|
887
|
-
if (!runtime.options.tools && !runtime.options.tool) {
|
|
899
|
+
if (!runtime.options.tools && !runtime.options.tool && !runtime.options.auditOutput) {
|
|
888
900
|
const code = await runReportOnly()
|
|
889
901
|
if (code !== null) process.exit(code)
|
|
890
902
|
}
|
package/src/manifest.js
CHANGED
|
@@ -1102,7 +1102,7 @@ export function createManifest(db) {
|
|
|
1102
1102
|
// Walk the output folder against recorded snapshots, returning
|
|
1103
1103
|
// a diff describing missing / mismatched / orphaned /
|
|
1104
1104
|
// unverifiable. Backs `mikser --audit-output`. Pure: no mutations.
|
|
1105
|
-
async auditOutput({ outputFolder } = {}) {
|
|
1105
|
+
async auditOutput({ outputFolder, roots } = {}) {
|
|
1106
1106
|
outputFolder = outputFolder || runtime.options.outputFolder
|
|
1107
1107
|
const missing = []
|
|
1108
1108
|
const mismatched = []
|
|
@@ -1112,16 +1112,12 @@ export function createManifest(db) {
|
|
|
1112
1112
|
const snap = rowToSnap(row)
|
|
1113
1113
|
if (!snap.destination) continue
|
|
1114
1114
|
const filePath = resolveOutputPath(snap.destination, outputFolder)
|
|
1115
|
-
//
|
|
1116
|
-
//
|
|
1117
|
-
//
|
|
1118
|
-
//
|
|
1119
|
-
//
|
|
1120
|
-
|
|
1121
|
-
const relative = path.relative(outputFolder, filePath)
|
|
1122
|
-
if (relative && !relative.startsWith('..') && !path.isAbsolute(relative)) {
|
|
1123
|
-
claimed.add(relative)
|
|
1124
|
-
}
|
|
1115
|
+
// Claimed by ABSOLUTE path, so the set does not depend on the
|
|
1116
|
+
// relative form any particular walk produces. It was keyed on
|
|
1117
|
+
// a path relative to outputFolder, which worked only while
|
|
1118
|
+
// that was the one tree walked — and quietly claimed nothing
|
|
1119
|
+
// for every destination outside it.
|
|
1120
|
+
claimed.add(path.resolve(filePath))
|
|
1125
1121
|
if (!existsSync(filePath)) {
|
|
1126
1122
|
missing.push({ id: snap.id, destination: snap.destination })
|
|
1127
1123
|
continue
|
|
@@ -1150,15 +1146,47 @@ export function createManifest(db) {
|
|
|
1150
1146
|
}
|
|
1151
1147
|
}
|
|
1152
1148
|
const { globby } = await import('globby')
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1149
|
+
// Every tree mikser writes into, not only the output folder.
|
|
1150
|
+
//
|
|
1151
|
+
// Preset derivatives live at the working-folder root and reach
|
|
1152
|
+
// the site through a symlink, which the walk does not follow — so
|
|
1153
|
+
// a file nothing claims there was invisible to this check, which
|
|
1154
|
+
// is the one place the concept of an orphan exists. Plugins
|
|
1155
|
+
// declare their own trees on `runtime.options.auditRoots`,
|
|
1156
|
+
// carrying their own ignore patterns, so nothing here has to know
|
|
1157
|
+
// what a preset is or that a `.md5` beside a derivative is
|
|
1158
|
+
// bookkeeping rather than output.
|
|
1159
|
+
const declared = [{ path: outputFolder }, ...(roots ?? runtime.options.auditRoots ?? [])]
|
|
1160
|
+
const walked = []
|
|
1161
|
+
for (const root of declared) {
|
|
1162
|
+
const resolved = path.resolve(root.path ?? root)
|
|
1163
|
+
// A root inside one already walked would report every file in
|
|
1164
|
+
// it twice.
|
|
1165
|
+
const nested = walked.some(({ resolved: seen }) =>
|
|
1166
|
+
resolved === seen || resolved.startsWith(seen + path.sep))
|
|
1167
|
+
if (nested) continue
|
|
1168
|
+
walked.push({ resolved, ignore: root.ignore ?? [] })
|
|
1169
|
+
}
|
|
1158
1170
|
const orphaned = []
|
|
1159
|
-
for (const
|
|
1160
|
-
|
|
1161
|
-
|
|
1171
|
+
for (const { resolved, ignore } of walked) {
|
|
1172
|
+
const onDisk = await globby('**/*', {
|
|
1173
|
+
cwd: resolved,
|
|
1174
|
+
onlyFiles: true,
|
|
1175
|
+
followSymbolicLinks: false,
|
|
1176
|
+
ignore,
|
|
1177
|
+
})
|
|
1178
|
+
for (const rel of onDisk) {
|
|
1179
|
+
if (claimed.has(path.join(resolved, rel))) continue
|
|
1180
|
+
// Relative to the output folder for the output folder, as
|
|
1181
|
+
// before; relative to the working folder for anything
|
|
1182
|
+
// else, because `web/media/x.jpg` alone does not say which
|
|
1183
|
+
// tree it is in.
|
|
1184
|
+
const isOutput = resolved === path.resolve(outputFolder)
|
|
1185
|
+
const display = isOutput
|
|
1186
|
+
? rel
|
|
1187
|
+
: path.relative(runtime.options.workingFolder ?? resolved, path.join(resolved, rel))
|
|
1188
|
+
orphaned.push({ path: display, root: resolved })
|
|
1189
|
+
}
|
|
1162
1190
|
}
|
|
1163
1191
|
// Reported alongside, not as a mismatch: two entities claiming one
|
|
1164
1192
|
// destination usually produces NO mismatch at all, because each
|
package/src/plugins/assets.js
CHANGED
|
@@ -418,11 +418,12 @@ export function assets(options = {}) {
|
|
|
418
418
|
}
|
|
419
419
|
|
|
420
420
|
async function forgetPresetMarkers(entity) {
|
|
421
|
-
|
|
421
|
+
const destination = path.resolve(entity.destination)
|
|
422
|
+
for (const marker of markersByDestination.get(destination) ?? []) {
|
|
422
423
|
await rm(marker, { force: true })
|
|
423
424
|
checksumMap.delete(marker)
|
|
424
425
|
}
|
|
425
|
-
markersByDestination.delete(
|
|
426
|
+
markersByDestination.delete(destination)
|
|
426
427
|
}
|
|
427
428
|
|
|
428
429
|
async function renderPresets(entities, { rendererChanged = new Set() } = {}) {
|
|
@@ -504,6 +505,7 @@ export function assets(options = {}) {
|
|
|
504
505
|
runtime.engine ??= {}
|
|
505
506
|
runtime.engine.assets = { explainMissing }
|
|
506
507
|
|
|
508
|
+
|
|
507
509
|
// `??`, not `||`: an option commander did not see is undefined, and
|
|
508
510
|
// falling through to the config is the point. `||` would also fall
|
|
509
511
|
// through for an intentional empty string, which is a different
|
|
@@ -515,6 +517,28 @@ export function assets(options = {}) {
|
|
|
515
517
|
|
|
516
518
|
runtime.options.assets = runtime.options.assets ?? options.assetsFolder ?? 'assets'
|
|
517
519
|
runtime.options.assetsFolder = path.join(runtime.options.workingFolder, runtime.options.assets)
|
|
520
|
+
// This tree holds outputs too, and --audit-output could not see it:
|
|
521
|
+
// it walks the output folder, and derivatives reach the site through
|
|
522
|
+
// a symlink the walk does not follow. Declared rather than hardcoded
|
|
523
|
+
// in the manifest, with the ignore that only this plugin can state —
|
|
524
|
+
// a `.md5` beside a derivative is bookkeeping, and without excluding
|
|
525
|
+
// them every derivative on the site would report one orphan.
|
|
526
|
+
//
|
|
527
|
+
// `auditIgnore` is the escape hatch for a preset that writes MORE
|
|
528
|
+
// than one file. The engine records the one destination it handed
|
|
529
|
+
// over, so a poster frame written beside a video is genuinely
|
|
530
|
+
// unclaimed and genuinely reported — correct, and a permanent
|
|
531
|
+
// non-zero exit for a legitimate preset if there were no way to say
|
|
532
|
+
// "these are expected". Stated in config, next to the preset that
|
|
533
|
+
// produces them, rather than inferred.
|
|
534
|
+
const auditRoot = {
|
|
535
|
+
path: runtime.options.assetsFolder,
|
|
536
|
+
ignore: ['**/*.md5', ...(options.auditIgnore ?? [])],
|
|
537
|
+
}
|
|
538
|
+
runtime.options.auditRoots = [
|
|
539
|
+
...(runtime.options.auditRoots ?? []).filter(root => root.path !== auditRoot.path),
|
|
540
|
+
auditRoot,
|
|
541
|
+
]
|
|
518
542
|
logger.debug('Assets folder: %s', runtime.options.assetsFolder)
|
|
519
543
|
|
|
520
544
|
// --clear means "throw away what was derived and build it again", and
|
|
@@ -702,27 +726,22 @@ export function assets(options = {}) {
|
|
|
702
726
|
|
|
703
727
|
checksumMap.clear()
|
|
704
728
|
markersByDestination.clear()
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
const presetFolders = Object.keys(runtime.state.assets.presets).map(name => `${name}/**/*`)
|
|
710
|
-
const files = presetFolders.length
|
|
711
|
-
? await globby(presetFolders, { cwd: runtime.options.assetsFolder, onlyFiles: true })
|
|
712
|
-
: []
|
|
713
|
-
const derivatives = new Set()
|
|
714
|
-
for (let file of files) {
|
|
715
|
-
const absolute = path.join(runtime.options.assetsFolder, file)
|
|
716
|
-
if (!file.endsWith('.md5')) {
|
|
717
|
-
derivatives.add(absolute)
|
|
718
|
-
continue
|
|
719
|
-
}
|
|
720
|
-
checksumMap.add(absolute)
|
|
729
|
+
const checksumFiles = await globby('**/*.md5', { cwd: runtime.options.assetsFolder })
|
|
730
|
+
for (let checksumFile of checksumFiles) {
|
|
731
|
+
const marker = path.join(runtime.options.assetsFolder, checksumFile)
|
|
732
|
+
checksumMap.add(marker)
|
|
721
733
|
// `<destination>.<revision>.md5` — the same shape the orphan
|
|
722
734
|
// sweep strips to get back to the derivative.
|
|
723
|
-
|
|
735
|
+
//
|
|
736
|
+
// Resolved on both sides of the comparison, because the other
|
|
737
|
+
// side is a manifest destination and these two are built by
|
|
738
|
+
// different subsystems. It is a no-op while assetsFolder is
|
|
739
|
+
// derived from workingFolder (see onLoaded), which is why no test
|
|
740
|
+
// can tell it apart — normalization at the boundary, not a
|
|
741
|
+
// transform anything depends on.
|
|
742
|
+
const destination = path.resolve(marker.replace(/\.[^.]+\.md5$/, ''))
|
|
724
743
|
if (!markersByDestination.has(destination)) markersByDestination.set(destination, [])
|
|
725
|
-
markersByDestination.get(destination).push(
|
|
744
|
+
markersByDestination.get(destination).push(marker)
|
|
726
745
|
}
|
|
727
746
|
|
|
728
747
|
const entitiesToRender = new Map()
|
|
@@ -745,28 +764,63 @@ export function assets(options = {}) {
|
|
|
745
764
|
// Empty on any healthy build, and the catalog walk is behind that
|
|
746
765
|
// emptiness: this costs one Set lookup per cycle until something has
|
|
747
766
|
// actually gone wrong.
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
767
|
+
// Asked of the MANIFEST, not of the folder.
|
|
768
|
+
//
|
|
769
|
+
// The first version walked every file under each preset folder and
|
|
770
|
+
// called anything without a marker an interrupted render. But only
|
|
771
|
+
// the destination the engine hands the preset gets a marker, so a
|
|
772
|
+
// preset that legitimately writes MORE than one file — a poster frame
|
|
773
|
+
// beside a video — left permanent evidence of a failure that never
|
|
774
|
+
// happened. It warned on every build for ever, said the derivative
|
|
775
|
+
// was "being re-derived", and re-derived nothing, because no entity's
|
|
776
|
+
// destination ever matched the extra file. A warning that cries wolf
|
|
777
|
+
// on every build is worse than the silence it replaced, and it
|
|
778
|
+
// teaches people to ignore the one that matters.
|
|
779
|
+
//
|
|
780
|
+
// A file nothing claims is an ORPHAN, which is a different fault with
|
|
781
|
+
// its own report. This asks only about outputs the manifest recorded,
|
|
782
|
+
// which is also what makes the recovery possible: a snapshot carries
|
|
783
|
+
// the entity id, so there is nothing to reverse-map and no catalog to
|
|
784
|
+
// walk.
|
|
785
|
+
const presetRoots = Object.keys(presets)
|
|
786
|
+
.map(name => path.resolve(runtime.options.assetsFolder, name) + path.sep)
|
|
787
|
+
const unfinished = new Map()
|
|
788
|
+
for (const snapshot of runtime.manifest?.all?.() ?? []) {
|
|
789
|
+
if (!snapshot.destination) continue
|
|
790
|
+
const destination = path.resolve(snapshot.destination)
|
|
791
|
+
if (!presetRoots.some(root => destination.startsWith(root))) continue
|
|
792
|
+
if (markersByDestination.has(destination)) continue
|
|
793
|
+
// Gone entirely is a different fault, and the engine's
|
|
794
|
+
// missing-output path already has it. Reporting it here as well
|
|
795
|
+
// would name the wrong cause.
|
|
796
|
+
if (outputMissing(snapshot.destination)) continue
|
|
797
|
+
unfinished.set(snapshot.id, destination)
|
|
751
798
|
}
|
|
752
|
-
if (
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
const
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
799
|
+
if (unfinished.size) {
|
|
800
|
+
const damaged = []
|
|
801
|
+
for (const [id, destination] of unfinished) {
|
|
802
|
+
const entity = await findEntity({ id })
|
|
803
|
+
// Source gone: the orphan sweep removes the derivative at
|
|
804
|
+
// finalize. Nothing to re-derive and nothing to say.
|
|
805
|
+
if (!entity) continue
|
|
806
|
+
assetsMap[entity.id] ??= await getEntityPresets(entity)
|
|
759
807
|
// Forced past the reuse check as well as scheduled: the file
|
|
760
808
|
// on disk is exactly what must not be trusted, and
|
|
761
|
-
// isPresetRendered
|
|
809
|
+
// isPresetRendered cannot tell half-written bytes from
|
|
762
810
|
// finished ones.
|
|
763
|
-
presetMoved.add(
|
|
764
|
-
entitiesToRender.set(
|
|
811
|
+
presetMoved.add(entity.id)
|
|
812
|
+
entitiesToRender.set(entity.id, entity)
|
|
813
|
+
damaged.push(destination)
|
|
814
|
+
}
|
|
815
|
+
// Only when something is actually being re-derived. The warning
|
|
816
|
+
// claimed the action rather than reporting it, so a build that
|
|
817
|
+
// scheduled nothing still announced a recovery.
|
|
818
|
+
if (damaged.length) {
|
|
819
|
+
useLogger().warn({ code: 'preset-unfinished', derivatives: damaged },
|
|
820
|
+
'Assets: %d derivative(s) have no completed-render marker and are being re-derived. '
|
|
821
|
+
+ 'A preset render did not finish — the file left behind is not trustworthy.',
|
|
822
|
+
damaged.length)
|
|
765
823
|
}
|
|
766
|
-
useLogger().warn({ code: 'preset-unfinished', derivatives: [...unmarked], sources: entitiesToRender.size },
|
|
767
|
-
'Assets: %d derivative(s) have no completed-render marker and are being re-derived. '
|
|
768
|
-
+ 'A preset render did not finish — the file left behind is not trustworthy.',
|
|
769
|
-
unmarked.size)
|
|
770
824
|
}
|
|
771
825
|
|
|
772
826
|
// --render-presets [name]: re-derive though nothing moved.
|