gazetta 0.3.0 → 0.5.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/admin-dist/assets/index-BZAFKsUp.js +608 -0
- package/admin-dist/assets/index-BpRotMuK.css +1 -0
- package/admin-dist/assets/vendor-primevue-BnR1c_bQ.js +2097 -0
- package/admin-dist/assets/vendor-vue-DSjyxCX6.js +1 -0
- package/admin-dist/index.html +4 -4
- package/dist/admin-api/index.d.ts +8 -2
- package/dist/admin-api/index.d.ts.map +1 -1
- package/dist/admin-api/index.js +31 -5
- package/dist/admin-api/index.js.map +1 -1
- package/dist/admin-api/routes/compare.d.ts +2 -1
- package/dist/admin-api/routes/compare.d.ts.map +1 -1
- package/dist/admin-api/routes/compare.js +6 -1
- package/dist/admin-api/routes/compare.js.map +1 -1
- package/dist/admin-api/routes/fragments.d.ts +2 -1
- package/dist/admin-api/routes/fragments.d.ts.map +1 -1
- package/dist/admin-api/routes/fragments.js +3 -1
- package/dist/admin-api/routes/fragments.js.map +1 -1
- package/dist/admin-api/routes/pages.d.ts +2 -1
- package/dist/admin-api/routes/pages.d.ts.map +1 -1
- package/dist/admin-api/routes/pages.js +3 -1
- package/dist/admin-api/routes/pages.js.map +1 -1
- package/dist/admin-api/routes/publish.d.ts +36 -1
- package/dist/admin-api/routes/publish.d.ts.map +1 -1
- package/dist/admin-api/routes/publish.js +216 -53
- package/dist/admin-api/routes/publish.js.map +1 -1
- package/dist/cli/index.js +228 -29
- package/dist/cli/index.js.map +1 -1
- package/dist/compare.d.ts +14 -0
- package/dist/compare.d.ts.map +1 -1
- package/dist/compare.js +44 -38
- package/dist/compare.js.map +1 -1
- package/dist/concurrency.d.ts +63 -0
- package/dist/concurrency.d.ts.map +1 -0
- package/dist/concurrency.js +134 -0
- package/dist/concurrency.js.map +1 -0
- package/dist/editor/mount.js +71 -71
- package/dist/editor/mount.js.map +1 -1
- package/dist/hash.d.ts +15 -0
- package/dist/hash.d.ts.map +1 -1
- package/dist/hash.js +47 -7
- package/dist/hash.js.map +1 -1
- package/dist/publish-rendered.d.ts +6 -5
- package/dist/publish-rendered.d.ts.map +1 -1
- package/dist/publish-rendered.js +39 -44
- package/dist/publish-rendered.js.map +1 -1
- package/dist/publish.d.ts +38 -0
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +154 -14
- package/dist/publish.js.map +1 -1
- package/dist/sidecars.d.ts +56 -0
- package/dist/sidecars.d.ts.map +1 -0
- package/dist/sidecars.js +141 -0
- package/dist/sidecars.js.map +1 -0
- package/dist/site-loader.d.ts.map +1 -1
- package/dist/site-loader.js +13 -10
- package/dist/site-loader.js.map +1 -1
- package/dist/source-sidecars.d.ts +13 -0
- package/dist/source-sidecars.d.ts.map +1 -0
- package/dist/source-sidecars.js +52 -0
- package/dist/source-sidecars.js.map +1 -0
- package/dist/targets.d.ts.map +1 -1
- package/dist/targets.js +21 -6
- package/dist/targets.js.map +1 -1
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/admin-dist/assets/index-C-xOOPUK.js +0 -604
- package/admin-dist/assets/index-CKOOkleR.css +0 -1
- package/admin-dist/assets/vendor-primevue-BNJTiXAs.js +0 -1670
- package/admin-dist/assets/vendor-vue-GhBrpzPN.js +0 -1
package/dist/cli/index.js
CHANGED
|
@@ -26,6 +26,80 @@ const c = {
|
|
|
26
26
|
};
|
|
27
27
|
const args = process.argv.slice(2);
|
|
28
28
|
const command = args[0];
|
|
29
|
+
// Served to /admin/* requests during dev-server startup before Vite middleware
|
|
30
|
+
// is attached. Polls /admin/ping every 500ms and reloads when the admin becomes
|
|
31
|
+
// reachable. See #132 and cli/index.ts for why this is needed.
|
|
32
|
+
const LOADER_HTML = `<!doctype html>
|
|
33
|
+
<html lang="en">
|
|
34
|
+
<head>
|
|
35
|
+
<meta charset="utf-8">
|
|
36
|
+
<title>Loading Gazetta admin…</title>
|
|
37
|
+
<style>
|
|
38
|
+
/* Neutral tones — the loader is a transitional state and we don't yet know
|
|
39
|
+
whether the user has customized the admin theme. Mid-gray reads as
|
|
40
|
+
"loading" on any conceivable admin background. */
|
|
41
|
+
html, body { height: 100%; margin: 0; overflow: hidden; }
|
|
42
|
+
body { font-family: system-ui, -apple-system, sans-serif; background: #262626; color: #a3a3a3; transition: opacity 200ms ease; position: relative; }
|
|
43
|
+
body.light { background: #f5f5f5; color: #525252; }
|
|
44
|
+
body.leaving { opacity: 0; }
|
|
45
|
+
|
|
46
|
+
/* Watermark — full-width brand fill, faint so it reads as background. */
|
|
47
|
+
.brand {
|
|
48
|
+
position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
|
|
49
|
+
font-weight: 800; letter-spacing: -0.04em; color: currentColor; opacity: 0.08;
|
|
50
|
+
font-size: clamp(6rem, 22vw, 18rem); line-height: 1;
|
|
51
|
+
pointer-events: none; user-select: none;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* Status pill — top-left, fades in only if startup is slow. */
|
|
55
|
+
.progress {
|
|
56
|
+
position: absolute; top: 1rem; left: 1rem;
|
|
57
|
+
display: flex; align-items: center; gap: 0.5rem;
|
|
58
|
+
font-size: 0.8125rem; opacity: 0; transition: opacity 300ms ease;
|
|
59
|
+
}
|
|
60
|
+
.progress.shown { opacity: 0.75; }
|
|
61
|
+
.spinner { width: 14px; height: 14px; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: spin 0.8s linear infinite; }
|
|
62
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
63
|
+
</style>
|
|
64
|
+
</head>
|
|
65
|
+
<body>
|
|
66
|
+
<div class="brand" aria-hidden="true">GAZETTA</div>
|
|
67
|
+
<div class="progress" role="status" aria-live="polite">
|
|
68
|
+
<div class="spinner" aria-hidden="true"></div>
|
|
69
|
+
<span class="label">Starting admin…</span>
|
|
70
|
+
</div>
|
|
71
|
+
<script>
|
|
72
|
+
// Match the user's saved admin theme if present; fall back to dark default
|
|
73
|
+
// (admin defaults to dark on first load).
|
|
74
|
+
try {
|
|
75
|
+
var saved = localStorage.getItem('gazetta_theme')
|
|
76
|
+
if (saved === 'light') document.body.classList.add('light')
|
|
77
|
+
} catch (e) { /* ignore */ }
|
|
78
|
+
|
|
79
|
+
// Delay the spinner + label by 400ms so warm restarts never paint them.
|
|
80
|
+
// The brand wordmark is always visible — it's safe; nothing about it
|
|
81
|
+
// implies "this is taking a while".
|
|
82
|
+
var progress = document.querySelector('.progress')
|
|
83
|
+
var showTimer = setTimeout(function () { progress.classList.add('shown') }, 400)
|
|
84
|
+
|
|
85
|
+
;(function poll() {
|
|
86
|
+
fetch('/admin/ping', { cache: 'no-store' })
|
|
87
|
+
.then(function (r) { return r.ok ? r.json() : null })
|
|
88
|
+
.then(function (j) {
|
|
89
|
+
if (j && j.ready) {
|
|
90
|
+
clearTimeout(showTimer)
|
|
91
|
+
// Fade body out, then reload — softer handoff than an instant swap
|
|
92
|
+
document.body.classList.add('leaving')
|
|
93
|
+
setTimeout(function () { window.location.reload() }, 200)
|
|
94
|
+
} else {
|
|
95
|
+
setTimeout(poll, 500)
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
.catch(function () { setTimeout(poll, 500) })
|
|
99
|
+
})()
|
|
100
|
+
</script>
|
|
101
|
+
</body>
|
|
102
|
+
</html>`;
|
|
29
103
|
/**
|
|
30
104
|
* Detect the project root from a site directory.
|
|
31
105
|
* Walks up from siteDir looking for a parent that contains templates/.
|
|
@@ -64,6 +138,7 @@ function printHelp() {
|
|
|
64
138
|
|
|
65
139
|
Options:
|
|
66
140
|
--port, -p <port> Server port (default: 3000)
|
|
141
|
+
--force, -f Publish all items (skip unchanged check)
|
|
67
142
|
|
|
68
143
|
Auto-detection:
|
|
69
144
|
Site is auto-detected from sites/ directory. If multiple sites exist,
|
|
@@ -85,15 +160,19 @@ function printHelp() {
|
|
|
85
160
|
function parseArgs(input) {
|
|
86
161
|
const positional = [];
|
|
87
162
|
let port;
|
|
163
|
+
let force = false;
|
|
88
164
|
for (let i = 0; i < input.length; i++) {
|
|
89
165
|
if (input[i] === '--port' || input[i] === '-p') {
|
|
90
166
|
port = parseInt(input[++i], 10);
|
|
91
167
|
}
|
|
168
|
+
else if (input[i] === '--force' || input[i] === '-f') {
|
|
169
|
+
force = true;
|
|
170
|
+
}
|
|
92
171
|
else if (!input[i].startsWith('-')) {
|
|
93
172
|
positional.push(input[i]);
|
|
94
173
|
}
|
|
95
174
|
}
|
|
96
|
-
return { positional, port };
|
|
175
|
+
return { positional, port, force };
|
|
97
176
|
}
|
|
98
177
|
/**
|
|
99
178
|
* Resolve the site directory from positional args or auto-detection.
|
|
@@ -334,7 +413,7 @@ export default template
|
|
|
334
413
|
const cdStep = dir !== '.' ? `cd ${dir} && ` : '';
|
|
335
414
|
outro(`Done! Run: ${c.cyan(`${cdStep}npx gazetta dev`)}`);
|
|
336
415
|
}
|
|
337
|
-
async function runPublish(siteDir, targetName) {
|
|
416
|
+
async function runPublish(siteDir, targetName, opts = {}) {
|
|
338
417
|
const storage = createFilesystemProvider();
|
|
339
418
|
const projectRoot = detectProjectRoot(siteDir);
|
|
340
419
|
const templatesDir = join(projectRoot, 'templates');
|
|
@@ -396,11 +475,40 @@ async function runPublish(siteDir, targetName) {
|
|
|
396
475
|
console.log(` ${c.bold(name)} ${c.dim(`(${publishMode})`)}`);
|
|
397
476
|
let totalFiles = 0;
|
|
398
477
|
let totalRemoved = 0;
|
|
478
|
+
// Incremental: compare source hashes against target sidecars, skip
|
|
479
|
+
// items whose hash already matches the target. --force bypasses.
|
|
480
|
+
const unchanged = new Set();
|
|
481
|
+
if (!opts.force) {
|
|
482
|
+
const { compareTargets } = await import('../compare.js');
|
|
483
|
+
const cmp = await compareTargets({
|
|
484
|
+
source: storage,
|
|
485
|
+
target: targetStorage,
|
|
486
|
+
siteDir,
|
|
487
|
+
templatesDir,
|
|
488
|
+
projectRoot,
|
|
489
|
+
publishMode,
|
|
490
|
+
scanTemplates: async () => templateInfos,
|
|
491
|
+
});
|
|
492
|
+
for (const item of cmp.unchanged)
|
|
493
|
+
unchanged.add(item);
|
|
494
|
+
}
|
|
495
|
+
let skipped = 0;
|
|
399
496
|
if (isStatic) {
|
|
400
|
-
// Static mode — fully assembled HTML, no fragments needed separately
|
|
497
|
+
// Static mode — fully assembled HTML, no fragments needed separately.
|
|
498
|
+
// Page hash must include fragment hashes so a fragment change
|
|
499
|
+
// invalidates every page that bakes it in (compareTargets uses the
|
|
500
|
+
// same combination on the local side).
|
|
501
|
+
const fragmentHashes = new Map();
|
|
502
|
+
for (const [fragName, frag] of site.fragments) {
|
|
503
|
+
fragmentHashes.set(fragName, hashManifest(frag, { templateHashes }));
|
|
504
|
+
}
|
|
401
505
|
for (const [pageName, page] of site.pages) {
|
|
402
|
-
|
|
403
|
-
|
|
506
|
+
if (unchanged.has(`pages/${pageName}`)) {
|
|
507
|
+
skipped++;
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
const manifestHash = hashManifest(page, { templateHashes, fragmentHashes });
|
|
511
|
+
const { files } = await publishPageStatic(pageName, storage, siteDir, targetStorage, templatesDir, manifestHash, site);
|
|
404
512
|
totalFiles += files;
|
|
405
513
|
console.log(` ${c.green('✓')} ${pageName}`);
|
|
406
514
|
}
|
|
@@ -408,23 +516,33 @@ async function runPublish(siteDir, targetName) {
|
|
|
408
516
|
else {
|
|
409
517
|
// ESI mode — fragments separate, pages with placeholders
|
|
410
518
|
for (const [fragName, frag] of site.fragments) {
|
|
519
|
+
if (unchanged.has(`fragments/${fragName}`)) {
|
|
520
|
+
skipped++;
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
411
523
|
const manifestHash = hashManifest(frag, { templateHashes });
|
|
412
|
-
const { files, removed } = await publishFragmentRendered(fragName, storage, siteDir, targetStorage, templatesDir, manifestHash);
|
|
524
|
+
const { files, removed } = await publishFragmentRendered(fragName, storage, siteDir, targetStorage, templatesDir, manifestHash, site);
|
|
413
525
|
totalFiles += files;
|
|
414
526
|
totalRemoved += removed;
|
|
415
527
|
console.log(` ${c.green('✓')} @${fragName}`);
|
|
416
528
|
}
|
|
417
529
|
for (const [pageName, page] of site.pages) {
|
|
530
|
+
if (unchanged.has(`pages/${pageName}`)) {
|
|
531
|
+
skipped++;
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
418
534
|
const manifestHash = hashManifest(page, { templateHashes });
|
|
419
|
-
const { files, removed } = await publishPageRendered(pageName, storage, siteDir, targetStorage, targetConfig?.cache, templatesDir, manifestHash);
|
|
535
|
+
const { files, removed } = await publishPageRendered(pageName, storage, siteDir, targetStorage, targetConfig?.cache, templatesDir, manifestHash, site);
|
|
420
536
|
totalFiles += files;
|
|
421
537
|
totalRemoved += removed;
|
|
422
538
|
console.log(` ${c.green('✓')} ${pageName}`);
|
|
423
539
|
}
|
|
424
540
|
}
|
|
541
|
+
if (skipped > 0)
|
|
542
|
+
console.log(` ${c.dim(`· ${skipped} unchanged (skipped)`)}`);
|
|
425
543
|
// Site manifest + fragment index
|
|
426
|
-
await publishSiteManifest(storage, siteDir, targetStorage);
|
|
427
|
-
await publishFragmentIndex(storage, siteDir, targetStorage);
|
|
544
|
+
await publishSiteManifest(storage, siteDir, targetStorage, site);
|
|
545
|
+
await publishFragmentIndex(storage, siteDir, targetStorage, site);
|
|
428
546
|
totalFiles += 2;
|
|
429
547
|
const removedMsg = totalRemoved > 0 ? c.dim(` (${totalRemoved} old files cleaned)`) : '';
|
|
430
548
|
console.log(`\n ${c.green('✓')} ${c.bold(name)}: ${totalFiles} files published${removedMsg}\n`);
|
|
@@ -943,13 +1061,16 @@ async function runDev(siteDir, port) {
|
|
|
943
1061
|
const cmsWebDir = findCmsDir();
|
|
944
1062
|
const cmsStaticDir = findCmsStaticDir();
|
|
945
1063
|
const isDevMode = cmsWebDir !== null;
|
|
1064
|
+
// Admin Hono instance — captured so the template file watcher can
|
|
1065
|
+
// invalidate its memoized template-scan cache on .ts/.tsx changes.
|
|
1066
|
+
let cmsApp = null;
|
|
946
1067
|
if (isDevMode) {
|
|
947
1068
|
// Dev mode: mount CMS API inline (same process = shared template cache)
|
|
948
|
-
await setupCmsApi(app, siteDir, storage, templatesDir, adminDir);
|
|
1069
|
+
cmsApp = await setupCmsApi(app, siteDir, storage, templatesDir, adminDir);
|
|
949
1070
|
}
|
|
950
1071
|
else if (cmsStaticDir) {
|
|
951
1072
|
// Production mode: inline CMS API + static files
|
|
952
|
-
await setupProductionMode(app, siteDir, storage, cmsStaticDir, templatesDir, adminDir);
|
|
1073
|
+
cmsApp = await setupProductionMode(app, siteDir, storage, cmsStaticDir, templatesDir, adminDir);
|
|
953
1074
|
}
|
|
954
1075
|
// ---- 404 ----
|
|
955
1076
|
app.notFound((c) => {
|
|
@@ -972,6 +1093,38 @@ async function runDev(siteDir, port) {
|
|
|
972
1093
|
console.log(` ${c.dim('┃')} Pages ${[...site.pages.entries()].map(([n, p]) => `${c.dim(p.route)} ${c.dim('→')} ${n}`).join(c.dim(', '))}`);
|
|
973
1094
|
console.log(` ${c.dim('┃')} Frags ${c.dim([...site.fragments.keys()].join(', ') || '(none)')}`);
|
|
974
1095
|
if (isDevMode && cmsWebDir) {
|
|
1096
|
+
// While Vite is spinning up (compiling, scanning deps, attaching
|
|
1097
|
+
// middleware), any /admin/* request falls through to the site's 404
|
|
1098
|
+
// handler showing a raw page list. Intercept early and serve a loader
|
|
1099
|
+
// page that polls /admin/ping until ready, then reloads (#132).
|
|
1100
|
+
let cmsReady = false;
|
|
1101
|
+
const httpServer = nodeServer;
|
|
1102
|
+
const originalListeners = httpServer.listeners('request').slice();
|
|
1103
|
+
httpServer.removeAllListeners('request');
|
|
1104
|
+
const loaderHandler = (req, res) => {
|
|
1105
|
+
const url = req.url ?? '';
|
|
1106
|
+
if (url === '/admin/ping' || url.startsWith('/admin/ping?')) {
|
|
1107
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
|
1108
|
+
res.end(JSON.stringify({ ready: cmsReady }));
|
|
1109
|
+
return true;
|
|
1110
|
+
}
|
|
1111
|
+
if (url === '/admin' || url.startsWith('/admin/') || url.startsWith('/@')) {
|
|
1112
|
+
res.writeHead(503, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store', 'Retry-After': '2' });
|
|
1113
|
+
res.end(LOADER_HTML);
|
|
1114
|
+
return true;
|
|
1115
|
+
}
|
|
1116
|
+
return false;
|
|
1117
|
+
};
|
|
1118
|
+
// During startup: route /admin/* to the loader, everything else to Hono
|
|
1119
|
+
httpServer.on('request', (req, res) => {
|
|
1120
|
+
if (cmsReady)
|
|
1121
|
+
return; // real handler installed below will run
|
|
1122
|
+
if (loaderHandler(req, res))
|
|
1123
|
+
return;
|
|
1124
|
+
for (const l of originalListeners) {
|
|
1125
|
+
l(req, res);
|
|
1126
|
+
}
|
|
1127
|
+
});
|
|
975
1128
|
try {
|
|
976
1129
|
const { createServer: createViteServer } = await import('vite');
|
|
977
1130
|
const { searchForWorkspaceRoot } = await import('vite');
|
|
@@ -1023,17 +1176,22 @@ async function runDev(siteDir, port) {
|
|
|
1023
1176
|
fs: { allow: [searchForWorkspaceRoot(cmsWebDir), siteDir] },
|
|
1024
1177
|
},
|
|
1025
1178
|
});
|
|
1026
|
-
const httpServer = nodeServer;
|
|
1027
|
-
const originalListeners = httpServer.listeners('request').slice();
|
|
1028
|
-
httpServer.removeAllListeners('request');
|
|
1029
1179
|
const honoHandler = (req, res) => {
|
|
1030
1180
|
for (const listener of originalListeners) {
|
|
1031
1181
|
listener(req, res);
|
|
1032
1182
|
}
|
|
1033
1183
|
};
|
|
1184
|
+
httpServer.removeAllListeners('request');
|
|
1034
1185
|
httpServer.on('request', (req, res) => {
|
|
1035
1186
|
const url = req.url ?? '';
|
|
1036
|
-
|
|
1187
|
+
// Keep /admin/ping live so the loader page can continue polling —
|
|
1188
|
+
// useful if the server hot-reloads and Vite rebinds.
|
|
1189
|
+
if (url === '/admin/ping' || url.startsWith('/admin/ping?')) {
|
|
1190
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
|
1191
|
+
res.end(JSON.stringify({ ready: true }));
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
if (url.startsWith('/admin/api') || url.startsWith('/admin/preview') || url === '/admin/theme.css' || url.startsWith('/admin/theme.css?')) {
|
|
1037
1195
|
honoHandler(req, res);
|
|
1038
1196
|
}
|
|
1039
1197
|
else if (url.startsWith('/admin') || url.startsWith('/@')) {
|
|
@@ -1043,6 +1201,7 @@ async function runDev(siteDir, port) {
|
|
|
1043
1201
|
honoHandler(req, res);
|
|
1044
1202
|
}
|
|
1045
1203
|
});
|
|
1204
|
+
cmsReady = true;
|
|
1046
1205
|
}
|
|
1047
1206
|
catch (err) {
|
|
1048
1207
|
console.warn(` Warning: CMS UI failed to start: ${err.message}`);
|
|
@@ -1051,19 +1210,34 @@ async function runDev(siteDir, port) {
|
|
|
1051
1210
|
console.log();
|
|
1052
1211
|
});
|
|
1053
1212
|
// ---- File watching ----
|
|
1054
|
-
// Watch site dir for content changes (JSON manifests + site.yaml config)
|
|
1055
|
-
|
|
1213
|
+
// Watch site dir for content changes (JSON manifests + site.yaml config).
|
|
1214
|
+
// Swallow FSWatcher 'error' events — Node's recursive watcher throws ENOENT
|
|
1215
|
+
// when a watched subdir disappears (rm -rf during publish, git checkout).
|
|
1216
|
+
// Letting it crash would take the whole dev server down; logging a warning
|
|
1217
|
+
// is enough, the watcher recovers for still-existing paths.
|
|
1218
|
+
const siteWatcher = watch(siteDir, { recursive: true }, (_event, filename) => {
|
|
1056
1219
|
if (!filename)
|
|
1057
1220
|
return;
|
|
1058
1221
|
if (filename.endsWith('.json') || filename.endsWith('.yaml')) {
|
|
1059
1222
|
console.log(` Manifest changed: ${filename}`);
|
|
1060
1223
|
invalidateAllTemplates();
|
|
1224
|
+
// Refresh source sidecars for external edits (git pull, direct file
|
|
1225
|
+
// edit). PUT routes already handle their own writes — this catches
|
|
1226
|
+
// everything outside the admin UI.
|
|
1227
|
+
const norm = filename.replace(/\\/g, '/');
|
|
1228
|
+
const pageMatch = /^pages\/(.+)\/page\.json$/.exec(norm);
|
|
1229
|
+
const fragMatch = /^fragments\/(.+)\/fragment\.json$/.exec(norm);
|
|
1230
|
+
if (pageMatch)
|
|
1231
|
+
cmsApp?.writeSourceSidecar('page', pageMatch[1]).catch(() => { });
|
|
1232
|
+
else if (fragMatch)
|
|
1233
|
+
cmsApp?.writeSourceSidecar('fragment', fragMatch[1]).catch(() => { });
|
|
1061
1234
|
notifyReload();
|
|
1062
1235
|
}
|
|
1063
1236
|
});
|
|
1237
|
+
siteWatcher.on('error', (err) => console.warn(` File watcher warning (site): ${err.message}`));
|
|
1064
1238
|
// Watch templates dir for template source changes
|
|
1065
1239
|
if (existsSync(templatesDir)) {
|
|
1066
|
-
watch(templatesDir, { recursive: true }, (_event, filename) => {
|
|
1240
|
+
const tplWatcher = watch(templatesDir, { recursive: true }, (_event, filename) => {
|
|
1067
1241
|
if (!filename)
|
|
1068
1242
|
return;
|
|
1069
1243
|
if (filename.endsWith('.ts') || filename.endsWith('.tsx')) {
|
|
@@ -1071,13 +1245,39 @@ async function runDev(siteDir, port) {
|
|
|
1071
1245
|
if (parts.length >= 1) {
|
|
1072
1246
|
console.log(` Template changed: ${parts[0]}`);
|
|
1073
1247
|
invalidateTemplate(parts[0]);
|
|
1248
|
+
// Drop the admin-api's cached scan so next compare/publish
|
|
1249
|
+
// rehashes. Cheap (the scan is what's slow, not invalidation).
|
|
1250
|
+
cmsApp?.invalidateTemplatesCache();
|
|
1251
|
+
cmsApp?.invalidateSourceSidecars();
|
|
1074
1252
|
notifyReload();
|
|
1075
1253
|
}
|
|
1076
1254
|
}
|
|
1077
1255
|
});
|
|
1256
|
+
tplWatcher.on('error', (err) => console.warn(` File watcher warning (templates): ${err.message}`));
|
|
1078
1257
|
}
|
|
1079
1258
|
}
|
|
1080
1259
|
// ---- Mount CMS API on the main Hono app (shared process = shared template cache) ----
|
|
1260
|
+
/**
|
|
1261
|
+
* Mount a Hono route serving the user's admin/theme.css. 404 if not present.
|
|
1262
|
+
* Cache-Control no-cache so devs see edits immediately.
|
|
1263
|
+
*
|
|
1264
|
+
* The link tag is added at runtime by main.ts (after PrimeVue + tokens.css)
|
|
1265
|
+
* so user declarations win the cascade. See #134 and css-theming.md.
|
|
1266
|
+
*/
|
|
1267
|
+
function mountUserThemeRoute(cmsApp, adminDir) {
|
|
1268
|
+
cmsApp.get('/theme.css', (c) => {
|
|
1269
|
+
const themePath = join(adminDir, 'theme.css');
|
|
1270
|
+
c.header('Content-Type', 'text/css; charset=utf-8');
|
|
1271
|
+
c.header('Cache-Control', 'no-cache');
|
|
1272
|
+
// When the user hasn't authored a theme.css, return an empty 200 rather
|
|
1273
|
+
// than a 404. The link tag in main.ts always references this URL; a 404
|
|
1274
|
+
// would log a browser console error on every cold load, polluting error
|
|
1275
|
+
// reports and failing strict console-error checks.
|
|
1276
|
+
if (!existsSync(themePath))
|
|
1277
|
+
return c.body('');
|
|
1278
|
+
return c.body(readFileSync(themePath, 'utf-8'));
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1081
1281
|
async function setupCmsApi(app, siteDir, storage, templatesDir, adminDir) {
|
|
1082
1282
|
const siteYamlPath = join(siteDir, 'site.yaml');
|
|
1083
1283
|
let targetConfigs;
|
|
@@ -1086,7 +1286,9 @@ async function setupCmsApi(app, siteDir, storage, templatesDir, adminDir) {
|
|
|
1086
1286
|
targetConfigs = siteYaml.targets;
|
|
1087
1287
|
}
|
|
1088
1288
|
const cmsApp = createAdminApp({ siteDir, storage, templatesDir, adminDir, targetConfigs });
|
|
1289
|
+
mountUserThemeRoute(cmsApp, adminDir);
|
|
1089
1290
|
app.route('/admin', cmsApp);
|
|
1291
|
+
return cmsApp;
|
|
1090
1292
|
}
|
|
1091
1293
|
// ---- Production mode: inline CMS API + static files from admin-dist/ ----
|
|
1092
1294
|
async function setupProductionMode(app, siteDir, storage, cmsStaticDir, templatesDir, adminDir) {
|
|
@@ -1099,27 +1301,24 @@ async function setupProductionMode(app, siteDir, storage, cmsStaticDir, template
|
|
|
1099
1301
|
}
|
|
1100
1302
|
// Mount CMS API inline at /admin (production mode — bundled editors/fields)
|
|
1101
1303
|
const cmsApp = createAdminApp({ siteDir, storage, templatesDir, adminDir, production: true, targetConfigs });
|
|
1304
|
+
mountUserThemeRoute(cmsApp, adminDir);
|
|
1102
1305
|
app.route('/admin', cmsApp);
|
|
1103
1306
|
// Serve pre-built CMS static files (includes bundled editors/fields)
|
|
1104
1307
|
app.use('/admin/*', serveStatic({
|
|
1105
1308
|
root: cmsStaticDir,
|
|
1106
1309
|
rewriteRequestPath: (path) => path.replace(/^\/admin/, ''),
|
|
1107
1310
|
}));
|
|
1108
|
-
// SPA fallback: serve index.html for unmatched /admin routes
|
|
1109
|
-
|
|
1311
|
+
// SPA fallback: serve index.html for /admin and unmatched /admin/* routes
|
|
1312
|
+
const serveIndex = (c) => {
|
|
1110
1313
|
const indexPath = join(cmsStaticDir, 'index.html');
|
|
1111
1314
|
if (existsSync(indexPath)) {
|
|
1112
1315
|
return c.html(readFileSync(indexPath, 'utf-8'));
|
|
1113
1316
|
}
|
|
1114
1317
|
return c.text('CMS admin UI not found', 404);
|
|
1115
|
-
}
|
|
1116
|
-
app.get('/admin',
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
return c.html(readFileSync(indexPath, 'utf-8'));
|
|
1120
|
-
}
|
|
1121
|
-
return c.text('CMS admin UI not found', 404);
|
|
1122
|
-
});
|
|
1318
|
+
};
|
|
1319
|
+
app.get('/admin/*', serveIndex);
|
|
1320
|
+
app.get('/admin', serveIndex);
|
|
1321
|
+
return cmsApp;
|
|
1123
1322
|
}
|
|
1124
1323
|
/** Find apps/admin source dir (monorepo dev mode) */
|
|
1125
1324
|
function findCmsDir() {
|
|
@@ -1209,7 +1408,7 @@ async function main() {
|
|
|
1209
1408
|
}
|
|
1210
1409
|
switch (command) {
|
|
1211
1410
|
case 'publish':
|
|
1212
|
-
await runPublish(siteDir, targetName);
|
|
1411
|
+
await runPublish(siteDir, targetName, { force: parsed.force });
|
|
1213
1412
|
break;
|
|
1214
1413
|
case 'serve':
|
|
1215
1414
|
await runServe(siteDir, parsed.port ?? 3000, targetName);
|