mikser-io 9.19.1 → 9.20.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": "9.19.1",
3
+ "version": "9.20.0",
4
4
  "description": "<p align=\"center\"> <img src=\"mikser-lockup-stacked.svg\" alt=\"mikser\" width=\"198\" /> </p>",
5
5
  "main": "index.js",
6
6
  "exports": {
@@ -331,6 +331,11 @@ export function createSqliteDatabase({
331
331
  logger: logger ?? null,
332
332
  }
333
333
 
334
+ // Published so plugins can tell an everything-was-evaluated cycle
335
+ // from an incremental one without reaching into the db handle. Read
336
+ // through `isFullCycle()` in utils.js rather than directly.
337
+ runtime.options.firstRun = provisioningCtx.firstRun
338
+
334
339
  // Fire provisioning callbacks before schema apply, so they can
335
340
  // load runtime extensions (sqlite-vec's vec0, etc.) that the
336
341
  // schemas about to apply might reference, register custom
package/src/manifest.js CHANGED
@@ -190,9 +190,34 @@ function snapToRow(snap) {
190
190
  }
191
191
  }
192
192
 
193
- async function hashOutputFile(destination) {
193
+ // Where a `destination` actually is on disk.
194
+ //
195
+ // Two shapes are in circulation and both are legitimate. A page's is
196
+ // output-relative with a leading slash (`/bg/index.html`); an asset's is a
197
+ // real filesystem path, because assets.js builds it from
198
+ // `runtime.options.assetsFolder`, which resolves inside the WORKING folder
199
+ // and can sit outside outputFolder entirely.
200
+ //
201
+ // `path.isAbsolute` cannot separate them — on POSIX `/bg/index.html` is
202
+ // absolute too — so resolve by EXISTENCE, output-relative first since that
203
+ // is the dominant shape. A page destination treated as a filesystem path
204
+ // would otherwise be looked for at the root of the disk.
205
+ //
206
+ // Shared, because the two callers needing it drifted into two separate
207
+ // bugs: verify() reported every asset missing while printing its real
208
+ // path, and hashOutputFile silently recorded no outputHash for one, which
209
+ // left 78% of a real project's snapshots presence-checked only.
210
+ export function resolveOutputPath(destination, outputFolder = runtime.options?.outputFolder) {
194
211
  if (!destination) return undefined
195
- const filePath = path.join(runtime.options.outputFolder, destination)
212
+ const joined = path.join(outputFolder ?? '', destination)
213
+ if (existsSync(joined)) return joined
214
+ if (path.isAbsolute(destination) && existsSync(destination)) return destination
215
+ return joined
216
+ }
217
+
218
+ async function hashOutputFile(destination) {
219
+ const filePath = resolveOutputPath(destination)
220
+ if (!filePath) return undefined
196
221
  try {
197
222
  const buf = await readFile(filePath)
198
223
  return sha1(buf)
@@ -552,31 +577,7 @@ export function createManifest(db) {
552
577
  for (const row of stmtSelectAll.iterate()) {
553
578
  const snap = rowToSnap(row)
554
579
  if (!snap.destination) continue
555
- // `destination` arrives in two shapes and both are legitimate.
556
- // A page's is output-relative with a leading slash
557
- // (`/bg/index.html`); an asset's is a real filesystem path,
558
- // because assets.js builds it from
559
- // `runtime.options.assetsFolder` — which resolves inside the
560
- // WORKING folder and may sit outside outputFolder entirely
561
- // (a `derived/` tree that `shares` symlinks into each language
562
- // root is the case that proves it).
563
- //
564
- // `path.isAbsolute` cannot tell them apart: on POSIX
565
- // `/bg/index.html` is absolute too. So resolve by EXISTENCE,
566
- // output-relative first — that is the dominant shape, and a
567
- // page destination interpreted as a filesystem path would
568
- // otherwise be looked for at the root of the disk.
569
- //
570
- // Only the fallback path is a filesystem path, so it is only
571
- // tried when the destination is absolute and joining changed
572
- // nothing that resolves.
573
- const joined = path.join(outputFolder, snap.destination)
574
- let filePath = joined
575
- if (!existsSync(joined)
576
- && path.isAbsolute(snap.destination)
577
- && existsSync(snap.destination)) {
578
- filePath = snap.destination
579
- }
580
+ const filePath = resolveOutputPath(snap.destination, outputFolder)
580
581
  // Orphan detection compares against a globby walk of
581
582
  // outputFolder, so `claimed` has to hold exactly the relative
582
583
  // form that walk produces. A destination resolving outside
@@ -6,6 +6,7 @@ import { globby } from 'globby'
6
6
  import _ from 'lodash'
7
7
  import map from 'p-map'
8
8
  import { reportWarning } from '../report.js'
9
+ import { isFullCycle } from '../utils.js'
9
10
 
10
11
  // Normalize a `options.presets[name]` value to a consistent
11
12
  // { matches, options } shape so callers don't have to inspect which form
@@ -120,6 +121,14 @@ export function assets(options = {}) {
120
121
  if (matchTally.reported) return
121
122
  const configured = Object.keys(options.presets || {})
122
123
  if (!configured.length || !matchTally.evaluated) return
124
+ // Only when the cycle evaluated everything. On an incremental run the
125
+ // evaluated set is whatever changed, so a healthy preset matches
126
+ // nothing in a run of two and the warning fires on every build —
127
+ // which trains the reader to filter it, and the filtered-out line is
128
+ // the real one. The message already told the reader to use --force to
129
+ // check the whole catalog; that condition gates it now instead of
130
+ // annotating it.
131
+ if (!isFullCycle(runtime)) return
123
132
  matchTally.reported = true
124
133
 
125
134
  for (const preset of configured) {
@@ -129,8 +138,7 @@ export function assets(options = {}) {
129
138
  logger.warn(
130
139
  'Assets preset %j matched none of the %d entities evaluated (patterns: %s). ' +
131
140
  'Patterns run against entity.id, which files({ outputFolder }) does NOT prefix — ' +
132
- 'the prefix appears on name and meta.url only. On an incremental run unchanged ' +
133
- 'entities are not re-evaluated; use --force to check the whole catalog.',
141
+ 'the prefix appears on name and meta.url only.',
134
142
  preset, matchTally.evaluated, matches.join(', '))
135
143
  }
136
144
  }
package/src/utils.js CHANGED
@@ -990,6 +990,29 @@ export async function writeOutput(file, bytes) {
990
990
  return true
991
991
  }
992
992
 
993
+ // Did this cycle evaluate the whole corpus, or only what changed?
994
+ //
995
+ // The distinction is what makes a "declared X matched nothing" warning
996
+ // worth printing. On an incremental cycle only changed entities are
997
+ // re-evaluated, so a pattern or preset legitimately matches nothing in a
998
+ // run of two — printing it there means the warning fires on every healthy
999
+ // build, and a warning that always fires is one people filter out, taking
1000
+ // the real instance with it.
1001
+ //
1002
+ // True for: --force, a first-run or wiped database, and a cache
1003
+ // invalidation. Those are exactly the cases where the import gate is
1004
+ // bypassed and every entity is presented for evaluation.
1005
+ // Takes the runtime rather than reading the singleton, so a plugin passes
1006
+ // the one it was injected with — which is the singleton in a real build and
1007
+ // the harness's stand-in under test.
1008
+ export function isFullCycle(rt = runtime) {
1009
+ return !!(
1010
+ rt?.options?.force
1011
+ || rt?.options?.firstRun
1012
+ || rt?.catalog?.cacheInvalidated
1013
+ )
1014
+ }
1015
+
993
1016
  // ── operating-system and file-manager litter ────────────────────────────
994
1017
  //
995
1018
  // Exposing a source folder over a network filesystem (mikser-io-webdav) or