mikser-io 10.10.1 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mikser-io",
3
- "version": "10.10.1",
3
+ "version": "10.11.0",
4
4
  "files": [
5
5
  "app.js",
6
6
  "index.js",
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
- if (runtime.options.tools || runtime.options.tool) {
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 --tool are NOT among them: they are dispatched at `import`
881
- // instead, because the tool registry is not complete until every
882
- // plugin's onLoaded has run and this hook runs ahead of all of them.
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
- // Orphan detection compares against a globby walk of
1116
- // outputFolder, so `claimed` has to hold exactly the relative
1117
- // form that walk produces. A destination resolving outside
1118
- // outputFolder can never appear in it and is not claimable;
1119
- // one inside it must be claimed by its relative path, not by
1120
- // the raw string with its leading slashes stripped.
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
- const onDisk = await globby('**/*', {
1154
- cwd: outputFolder,
1155
- onlyFiles: true,
1156
- followSymbolicLinks: false,
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 rel of onDisk) {
1160
- if (claimed.has(rel)) continue
1161
- orphaned.push({ path: rel })
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
@@ -505,6 +505,7 @@ export function assets(options = {}) {
505
505
  runtime.engine ??= {}
506
506
  runtime.engine.assets = { explainMissing }
507
507
 
508
+
508
509
  // `??`, not `||`: an option commander did not see is undefined, and
509
510
  // falling through to the config is the point. `||` would also fall
510
511
  // through for an intentional empty string, which is a different
@@ -516,6 +517,28 @@ export function assets(options = {}) {
516
517
 
517
518
  runtime.options.assets = runtime.options.assets ?? options.assetsFolder ?? 'assets'
518
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
+ ]
519
542
  logger.debug('Assets folder: %s', runtime.options.assetsFolder)
520
543
 
521
544
  // --clear means "throw away what was derived and build it again", and