@yojahny/wp-design-library 0.6.0 → 1.0.1
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/cli/serve.mjs +25 -0
- package/src/mcp/http.mjs +16 -1
- package/src/ui/build.mjs +13 -3
package/package.json
CHANGED
package/src/cli/serve.mjs
CHANGED
|
@@ -11,8 +11,20 @@ async function syncSeed(p) {
|
|
|
11
11
|
if (p.seed !== p.entries && fs.existsSync(p.seed)) {
|
|
12
12
|
const path = await import('node:path');
|
|
13
13
|
fs.mkdirSync(p.entries, { recursive: true });
|
|
14
|
+
// Healing missing files is not enough when the file that changed is one the volume already
|
|
15
|
+
// has. 0.6.0 rewrote every entry.md — artifacts became {key, bytes, sha256} records — and
|
|
16
|
+
// because the volume's copies existed, none were replaced: all 31 entries failed validation
|
|
17
|
+
// and the gallery served nothing but drafts. Copy-if-absent can only ever fix an entry that
|
|
18
|
+
// is incomplete, never one that is stale, so a schema change breaks the corpus permanently.
|
|
19
|
+
// The stamp closes that: when the image's version differs from the one this volume was last
|
|
20
|
+
// seeded at, entry.md is re-copied for the slugs the seed ships.
|
|
21
|
+
const stamp = path.join(p.data, '.seed-version');
|
|
22
|
+
let seeded;
|
|
23
|
+
try { seeded = fs.readFileSync(stamp, 'utf8').trim(); } catch { seeded = null; }
|
|
24
|
+
const upgraded = seeded !== p.version;
|
|
14
25
|
let copied = 0;
|
|
15
26
|
let healed = 0;
|
|
27
|
+
let refreshed = 0;
|
|
16
28
|
for (const d of fs.readdirSync(p.seed, { withFileTypes: true })) {
|
|
17
29
|
if (!d.isDirectory() || !fs.existsSync(path.join(p.seed, d.name, 'entry.md'))) continue;
|
|
18
30
|
const src = path.join(p.seed, d.name);
|
|
@@ -31,9 +43,22 @@ async function syncSeed(p) {
|
|
|
31
43
|
return true;
|
|
32
44
|
},
|
|
33
45
|
});
|
|
46
|
+
// Only entry.md, and only on an upgrade. Everything else the seed ships is inert next to
|
|
47
|
+
// an artifact record, and a hosted ingest never lands on a slug the image also ships — so
|
|
48
|
+
// what this can overwrite is an operator's hand-edit of a seeded entry, which an image
|
|
49
|
+
// upgrade was always going to supersede. Serving a corpus that validates beats preserving
|
|
50
|
+
// an edit that only exists until the next deploy.
|
|
51
|
+
if (!upgraded) continue;
|
|
52
|
+
const from = path.join(src, 'entry.md');
|
|
53
|
+
const to = path.join(dest, 'entry.md');
|
|
54
|
+
if (fs.readFileSync(from, 'utf8') === fs.readFileSync(to, 'utf8')) continue;
|
|
55
|
+
fs.copyFileSync(from, to);
|
|
56
|
+
refreshed++;
|
|
34
57
|
}
|
|
58
|
+
fs.writeFileSync(stamp, p.version);
|
|
35
59
|
process.stderr.write(`seed: copied ${copied} new entries into ${p.entries}\n`);
|
|
36
60
|
process.stderr.write(`seed: healed ${healed} missing files into existing entries\n`);
|
|
61
|
+
process.stderr.write(`seed: refreshed ${refreshed} stale entry.md (volume seeded at ${seeded ?? 'unknown'}, image is ${p.version})\n`);
|
|
37
62
|
}
|
|
38
63
|
}
|
|
39
64
|
|
package/src/mcp/http.mjs
CHANGED
|
@@ -23,8 +23,23 @@ async function handle(req, res, p, token, embedder) {
|
|
|
23
23
|
if (db.vec && db.prepare('SELECT count(*) c FROM vec_text').get().c > 0) arms = ['fts', 'vec'];
|
|
24
24
|
db.close();
|
|
25
25
|
} catch {}
|
|
26
|
+
// A host with a full index and no reachable artifacts serves 31 grey boxes and reported
|
|
27
|
+
// ok:true, which is how a missing R2_PUBLIC_BASE_URL stayed invisible through two rounds of
|
|
28
|
+
// "it still is not showing images". Name it here, where anyone can see it without log access.
|
|
29
|
+
// It is not part of `ok`: the MCP API answers perfectly well on text alone, and failing the
|
|
30
|
+
// health check would take a working server down over missing pictures.
|
|
31
|
+
// "configured", never "ok": this reads the environment, it does not fetch an object, so it
|
|
32
|
+
// cannot claim the bucket is reachable. It catches the failure actually hit — the variable
|
|
33
|
+
// being absent — and says only what it checked.
|
|
34
|
+
let artifacts;
|
|
35
|
+
try {
|
|
36
|
+
const { readConfig } = await import('../r2.mjs');
|
|
37
|
+
artifacts = `configured: ${readConfig().base}`;
|
|
38
|
+
} catch (err) {
|
|
39
|
+
artifacts = `unconfigured: ${err.message}`;
|
|
40
|
+
}
|
|
26
41
|
res.writeHead(200, { 'content-type': 'application/json' });
|
|
27
|
-
return res.end(JSON.stringify({ ok: true, entries, arms, version: p.version }));
|
|
42
|
+
return res.end(JSON.stringify({ ok: true, entries, arms, artifacts, version: p.version }));
|
|
28
43
|
}
|
|
29
44
|
if (url.pathname === '/mcp') {
|
|
30
45
|
if (!authorized(req.headers.authorization, token)) { res.writeHead(401); return res.end('unauthorized'); }
|
package/src/ui/build.mjs
CHANGED
|
@@ -156,6 +156,7 @@ export async function buildUi({ indexPath, entriesDir, outDir, previewMediaDir,
|
|
|
156
156
|
fs.mkdirSync(path.join(outDir, 'thumbnails'), { recursive: true });
|
|
157
157
|
|
|
158
158
|
const rows = [];
|
|
159
|
+
const unresolved = [];
|
|
159
160
|
for (const dir of listEntries(entriesDir)) {
|
|
160
161
|
let entry;
|
|
161
162
|
try { entry = parseEntry(dir); } catch (err) { process.stderr.write(`ui: skipping ${path.basename(dir)}: ${err.message}\n`); continue; }
|
|
@@ -169,11 +170,14 @@ export async function buildUi({ indexPath, entriesDir, outDir, previewMediaDir,
|
|
|
169
170
|
const strip = (entry.fm.captured?.captures?.[0]?.artifacts ?? []).find((a) => a?.key?.endsWith('/strip.webp'));
|
|
170
171
|
// One unreachable object must not take the gallery down with it: the entry renders without a
|
|
171
172
|
// thumbnail, exactly as a draft does, and the build continues.
|
|
172
|
-
|
|
173
|
+
// Degrading silently is what made a misconfigured host indistinguishable from a broken one:
|
|
174
|
+
// a gallery of 31 grey boxes, `ok: true` from healthz, and not one line saying why. Swallow
|
|
175
|
+
// the failure, still — but count it and keep the first reason.
|
|
176
|
+
if (strip) { try { stripSrc = await artifactFile(p, strip); } catch (err) { stripSrc = null; unresolved.push(err); } }
|
|
173
177
|
// Same treatment for measure.json: a record to resolve, never a file next to entry.md.
|
|
174
178
|
let measureSrc = null;
|
|
175
179
|
const measured = (entry.fm.captured?.captures?.[0]?.artifacts ?? []).find((a) => a?.key?.endsWith('/measure.json'));
|
|
176
|
-
if (measured) { try { measureSrc = await artifactFile(p, measured); } catch { measureSrc = null; } }
|
|
180
|
+
if (measured) { try { measureSrc = await artifactFile(p, measured); } catch (err) { measureSrc = null; unresolved.push(err); } }
|
|
177
181
|
const hasStrip = stripSrc !== null;
|
|
178
182
|
if (hasStrip) {
|
|
179
183
|
fs.copyFileSync(stripSrc, path.join(outDir, 'strips', `${slug}.webp`));
|
|
@@ -212,5 +216,11 @@ export async function buildUi({ indexPath, entriesDir, outDir, previewMediaDir,
|
|
|
212
216
|
fs.writeFileSync(path.join(outDir, 'index.html'), render(tpl, { FILTERS: filters, GRID: rows.map(card).join('\n'), NOTICE: notice }));
|
|
213
217
|
fs.copyFileSync(path.join(DIR, 'app.js'), path.join(outDir, 'app.js'));
|
|
214
218
|
|
|
215
|
-
|
|
219
|
+
// One line, not one per artifact: 31 entries failing for the same missing variable is one
|
|
220
|
+
// fact, and printing it 62 times would bury it. The first reason is the one that matters —
|
|
221
|
+
// they are identical when the cause is configuration, which is the case worth naming.
|
|
222
|
+
if (unresolved.length) {
|
|
223
|
+
process.stderr.write(`ui: ${unresolved.length} artifact(s) could not be resolved, so those entries render without a thumbnail; first reason: ${unresolved[0].message}\n`);
|
|
224
|
+
}
|
|
225
|
+
return { entries: rows.length - drafts, drafts, unresolved: unresolved.length };
|
|
216
226
|
}
|