mikser-io 9.19.0 → 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/docs/diagnostics.md +10 -3
- package/package.json +1 -1
- package/src/database/index.js +5 -0
- package/src/engine.js +8 -1
- package/src/manifest.js +44 -6
- package/src/plugins/assets.js +10 -2
- package/src/utils.js +23 -0
package/docs/diagnostics.md
CHANGED
|
@@ -106,9 +106,16 @@ there is no manifest to check against.
|
|
|
106
106
|
npx mikser --verify || echo "output folder has drifted"
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
A destination is resolved against the output folder first and, when that
|
|
110
|
+
finds nothing, treated as a filesystem path — assets carry an absolute
|
|
111
|
+
destination built from `assetsFolder`, which may sit outside the output
|
|
112
|
+
folder entirely.
|
|
113
|
+
|
|
114
|
+
`Orphan` still needs reading with that in mind: only files under the
|
|
115
|
+
output folder are walked, and a file is an orphan when no snapshot claims
|
|
116
|
+
it. That is normal for anything written without a render snapshot (the
|
|
117
|
+
`files` and `data` plugins), and a real signal for a page whose layout
|
|
118
|
+
stopped producing it.
|
|
112
119
|
|
|
113
120
|
### The rest, briefly
|
|
114
121
|
|
package/package.json
CHANGED
package/src/database/index.js
CHANGED
|
@@ -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/engine.js
CHANGED
|
@@ -265,8 +265,15 @@ export async function setup(options) {
|
|
|
265
265
|
for (const e of unverifiable) logger.warn('No hash: %s (entity %s)', e.destination, e.id)
|
|
266
266
|
for (const e of orphaned) logger.warn('Orphan: %s', e.path)
|
|
267
267
|
|
|
268
|
+
// Level picked from the verdict, because the level IS the marker
|
|
269
|
+
// in pino-pretty's messageFormat: notice renders 🟢, warn 🟡,
|
|
270
|
+
// error 🔴. A fixed `notice` prints a green tick next to the word
|
|
271
|
+
// FAIL, which reads as success at a glance even though the exit
|
|
272
|
+
// code is right.
|
|
268
273
|
const verdict = errors > 0 ? 'FAIL' : (warnings > 0 ? 'WARN' : 'OK')
|
|
269
|
-
logger.
|
|
274
|
+
const report = errors > 0 ? logger.error : (warnings > 0 ? logger.warn : logger.notice)
|
|
275
|
+
report.call(logger,
|
|
276
|
+
'Verify %s: %d snapshots, %d missing, %d mismatched, %d unverifiable, %d orphaned',
|
|
270
277
|
verdict, total, missing.length, mismatched.length, unverifiable.length, orphaned.length)
|
|
271
278
|
process.exit(errors > 0 ? 2 : (warnings > 0 ? 1 : 0))
|
|
272
279
|
}
|
package/src/manifest.js
CHANGED
|
@@ -66,7 +66,10 @@ import { useDatabase, registerSchema } from './database/index.js'
|
|
|
66
66
|
// PK index, no separate id index needed. Parent index is for pagination
|
|
67
67
|
// cleanup (drop all children of X). WITHOUT ROWID keeps the file
|
|
68
68
|
// smaller.
|
|
69
|
-
|
|
69
|
+
// Exported so tests build against the real schema rather than a copy. A
|
|
70
|
+
// copy drifts the moment a column is added, and the failure is a bare
|
|
71
|
+
// SQLITE_ERROR that names nothing.
|
|
72
|
+
export const SNAPSHOTS_SCHEMA = `
|
|
70
73
|
CREATE TABLE IF NOT EXISTS mikser_snapshots (
|
|
71
74
|
id TEXT NOT NULL,
|
|
72
75
|
destination TEXT NOT NULL,
|
|
@@ -78,7 +81,8 @@ registerSchema('mikser_snapshots', `
|
|
|
78
81
|
PRIMARY KEY (id, destination)
|
|
79
82
|
) WITHOUT ROWID;
|
|
80
83
|
CREATE INDEX IF NOT EXISTS idx_mikser_snapshots_parent ON mikser_snapshots(parent) WHERE parent IS NOT NULL;
|
|
81
|
-
`
|
|
84
|
+
`
|
|
85
|
+
registerSchema('mikser_snapshots', SNAPSHOTS_SCHEMA)
|
|
82
86
|
|
|
83
87
|
// Module-level DB handle + prepared statements for the lifecycle
|
|
84
88
|
// integration. Tests build their own via `createManifest(db)` —
|
|
@@ -186,9 +190,34 @@ function snapToRow(snap) {
|
|
|
186
190
|
}
|
|
187
191
|
}
|
|
188
192
|
|
|
189
|
-
|
|
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) {
|
|
190
211
|
if (!destination) return undefined
|
|
191
|
-
const
|
|
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
|
|
192
221
|
try {
|
|
193
222
|
const buf = await readFile(filePath)
|
|
194
223
|
return sha1(buf)
|
|
@@ -548,8 +577,17 @@ export function createManifest(db) {
|
|
|
548
577
|
for (const row of stmtSelectAll.iterate()) {
|
|
549
578
|
const snap = rowToSnap(row)
|
|
550
579
|
if (!snap.destination) continue
|
|
551
|
-
|
|
552
|
-
|
|
580
|
+
const filePath = resolveOutputPath(snap.destination, outputFolder)
|
|
581
|
+
// Orphan detection compares against a globby walk of
|
|
582
|
+
// outputFolder, so `claimed` has to hold exactly the relative
|
|
583
|
+
// form that walk produces. A destination resolving outside
|
|
584
|
+
// outputFolder can never appear in it and is not claimable;
|
|
585
|
+
// one inside it must be claimed by its relative path, not by
|
|
586
|
+
// the raw string with its leading slashes stripped.
|
|
587
|
+
const relative = path.relative(outputFolder, filePath)
|
|
588
|
+
if (relative && !relative.startsWith('..') && !path.isAbsolute(relative)) {
|
|
589
|
+
claimed.add(relative)
|
|
590
|
+
}
|
|
553
591
|
if (!existsSync(filePath)) {
|
|
554
592
|
missing.push({ id: snap.id, destination: snap.destination })
|
|
555
593
|
continue
|
package/src/plugins/assets.js
CHANGED
|
@@ -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.
|
|
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
|