gazetta 0.0.8 → 0.1.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/admin-dist/assets/index-Dj0qNcv2.css +1 -0
- package/admin-dist/assets/index-mw1Q3IRb.js +2459 -0
- package/admin-dist/index.html +3 -2
- package/dist/admin-api/index.d.ts +6 -0
- package/dist/admin-api/index.d.ts.map +1 -1
- package/dist/admin-api/index.js +8 -3
- package/dist/admin-api/index.js.map +1 -1
- package/dist/admin-api/routes/fields.d.ts +4 -0
- package/dist/admin-api/routes/fields.d.ts.map +1 -0
- package/dist/admin-api/routes/fields.js +21 -0
- package/dist/admin-api/routes/fields.js.map +1 -0
- package/dist/admin-api/routes/pages.d.ts.map +1 -1
- package/dist/admin-api/routes/pages.js +2 -4
- package/dist/admin-api/routes/pages.js.map +1 -1
- package/dist/admin-api/routes/preview.d.ts +1 -1
- package/dist/admin-api/routes/preview.d.ts.map +1 -1
- package/dist/admin-api/routes/preview.js +29 -8
- package/dist/admin-api/routes/preview.js.map +1 -1
- package/dist/admin-api/routes/publish.d.ts +1 -1
- package/dist/admin-api/routes/publish.d.ts.map +1 -1
- package/dist/admin-api/routes/publish.js +44 -33
- package/dist/admin-api/routes/publish.js.map +1 -1
- package/dist/admin-api/routes/templates.d.ts +1 -1
- package/dist/admin-api/routes/templates.d.ts.map +1 -1
- package/dist/admin-api/routes/templates.js +32 -8
- package/dist/admin-api/routes/templates.js.map +1 -1
- package/dist/cli/index.js +637 -170
- package/dist/cli/index.js.map +1 -1
- package/dist/editor/mount.d.ts +15 -0
- package/dist/editor/mount.d.ts.map +1 -1
- package/dist/editor/mount.js +684 -29
- package/dist/editor/mount.js.map +1 -1
- package/dist/formats.d.ts +31 -0
- package/dist/formats.d.ts.map +1 -1
- package/dist/formats.js +14 -0
- package/dist/formats.js.map +1 -1
- package/dist/index.d.ts +10 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -5
- package/dist/index.js.map +1 -1
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +10 -9
- package/dist/manifest.js.map +1 -1
- package/dist/providers/r2.d.ts +8 -0
- package/dist/providers/r2.d.ts.map +1 -0
- package/dist/providers/r2.js +83 -0
- package/dist/providers/r2.js.map +1 -0
- package/dist/publish-rendered.d.ts +7 -3
- package/dist/publish-rendered.d.ts.map +1 -1
- package/dist/publish-rendered.js +26 -22
- package/dist/publish-rendered.js.map +1 -1
- package/dist/renderer.d.ts +1 -0
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +22 -3
- package/dist/renderer.js.map +1 -1
- package/dist/resolver.d.ts +1 -0
- package/dist/resolver.d.ts.map +1 -1
- package/dist/resolver.js +23 -3
- package/dist/resolver.js.map +1 -1
- package/dist/scope.d.ts +10 -4
- package/dist/scope.d.ts.map +1 -1
- package/dist/scope.js +17 -8
- package/dist/scope.js.map +1 -1
- package/dist/serve.d.ts +14 -0
- package/dist/serve.d.ts.map +1 -0
- package/dist/serve.js +135 -0
- package/dist/serve.js.map +1 -0
- package/dist/site-loader.d.ts +11 -1
- package/dist/site-loader.d.ts.map +1 -1
- package/dist/site-loader.js +19 -7
- package/dist/site-loader.js.map +1 -1
- package/dist/targets.d.ts +1 -0
- package/dist/targets.d.ts.map +1 -1
- package/dist/targets.js +42 -1
- package/dist/targets.js.map +1 -1
- package/dist/template-loader.d.ts +3 -2
- package/dist/template-loader.d.ts.map +1 -1
- package/dist/template-loader.js +32 -2
- package/dist/template-loader.js.map +1 -1
- package/dist/types.d.ts +35 -15
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -1
- package/dist/types.js.map +1 -1
- package/dist/workers/cloudflare-r2.d.ts.map +1 -1
- package/dist/workers/cloudflare-r2.js +14 -1
- package/dist/workers/cloudflare-r2.js.map +1 -1
- package/package.json +16 -7
- package/admin-dist/assets/index-CAGoaiMq.js +0 -1941
- package/admin-dist/assets/index-NBRKgCFg.css +0 -1
package/dist/cli/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { resolve, join } from 'node:path';
|
|
2
|
+
import { resolve, join, dirname } from 'node:path';
|
|
3
3
|
import { watch, existsSync, readFileSync } from 'node:fs';
|
|
4
|
-
import { spawn } from 'node:child_process';
|
|
5
4
|
import { serve } from '@hono/node-server';
|
|
6
5
|
import { serveStatic } from '@hono/node-server/serve-static';
|
|
7
6
|
import { Hono } from 'hono';
|
|
@@ -13,60 +12,183 @@ import { renderPage } from '../renderer.js';
|
|
|
13
12
|
import { createFilesystemProvider } from '../providers/filesystem.js';
|
|
14
13
|
import { invalidateTemplate, invalidateAllTemplates } from '../template-loader.js';
|
|
15
14
|
import { createAdminApp } from '../admin-api/index.js';
|
|
15
|
+
// ANSI color helpers — no dependency, suppressed when NO_COLOR or CI
|
|
16
|
+
const noColor = !!process.env.NO_COLOR || !process.stdout.isTTY;
|
|
17
|
+
const c = {
|
|
18
|
+
bold: (s) => noColor ? s : `\x1b[1m${s}\x1b[22m`,
|
|
19
|
+
dim: (s) => noColor ? s : `\x1b[2m${s}\x1b[22m`,
|
|
20
|
+
cyan: (s) => noColor ? s : `\x1b[36m${s}\x1b[39m`,
|
|
21
|
+
green: (s) => noColor ? s : `\x1b[32m${s}\x1b[39m`,
|
|
22
|
+
yellow: (s) => noColor ? s : `\x1b[33m${s}\x1b[39m`,
|
|
23
|
+
red: (s) => noColor ? s : `\x1b[31m${s}\x1b[39m`,
|
|
24
|
+
magenta: (s) => noColor ? s : `\x1b[35m${s}\x1b[39m`,
|
|
25
|
+
bgGreen: (s) => noColor ? s : `\x1b[42m\x1b[30m${s}\x1b[39m\x1b[49m`,
|
|
26
|
+
};
|
|
16
27
|
const args = process.argv.slice(2);
|
|
17
28
|
const command = args[0];
|
|
29
|
+
/**
|
|
30
|
+
* Detect the project root from a site directory.
|
|
31
|
+
* Walks up from siteDir looking for a parent that contains templates/.
|
|
32
|
+
* Falls back to siteDir for flat projects (templates/ inside site dir).
|
|
33
|
+
*/
|
|
34
|
+
function detectProjectRoot(siteDir) {
|
|
35
|
+
// If siteDir itself has templates/, it's a flat project
|
|
36
|
+
if (existsSync(join(siteDir, 'templates')))
|
|
37
|
+
return siteDir;
|
|
38
|
+
// Walk up looking for templates/
|
|
39
|
+
let dir = resolve(siteDir);
|
|
40
|
+
const root = resolve('/');
|
|
41
|
+
while (dir !== root) {
|
|
42
|
+
const parent = dirname(dir);
|
|
43
|
+
if (existsSync(join(parent, 'templates')))
|
|
44
|
+
return parent;
|
|
45
|
+
dir = parent;
|
|
46
|
+
}
|
|
47
|
+
// Fallback — use siteDir (templates/ may not exist yet)
|
|
48
|
+
return siteDir;
|
|
49
|
+
}
|
|
18
50
|
function printHelp() {
|
|
19
51
|
console.log(`
|
|
20
52
|
gazetta - Stateless CMS for composable websites
|
|
21
53
|
|
|
22
54
|
Usage:
|
|
23
|
-
gazetta init [dir]
|
|
24
|
-
gazetta dev [site
|
|
25
|
-
gazetta
|
|
26
|
-
gazetta
|
|
27
|
-
gazetta
|
|
28
|
-
gazetta
|
|
55
|
+
gazetta init [dir] Create a new site
|
|
56
|
+
gazetta dev [site] Start dev server + CMS at /admin
|
|
57
|
+
gazetta build Build admin UI for production
|
|
58
|
+
gazetta admin [site] Run production CMS admin server
|
|
59
|
+
gazetta publish [target] [site] Pre-render and publish to a target
|
|
60
|
+
gazetta serve [target] [site] Serve published pages from target storage
|
|
61
|
+
gazetta deploy [target] [site] Deploy worker to hosting (one-time setup)
|
|
62
|
+
gazetta validate [site] Check site for broken references
|
|
63
|
+
gazetta help Show this help message
|
|
29
64
|
|
|
30
65
|
Options:
|
|
31
|
-
--port, -p <port>
|
|
32
|
-
|
|
66
|
+
--port, -p <port> Server port (default: 3000)
|
|
67
|
+
|
|
68
|
+
Auto-detection:
|
|
69
|
+
Site is auto-detected from sites/ directory. If multiple sites exist,
|
|
70
|
+
you'll be prompted to choose (or pass it as an argument).
|
|
71
|
+
|
|
72
|
+
Target is auto-detected as the first target in site.yaml. If multiple
|
|
73
|
+
targets exist, you'll be prompted to choose (or pass it as an argument).
|
|
33
74
|
|
|
34
75
|
Examples:
|
|
35
|
-
gazetta init my-site
|
|
36
|
-
gazetta dev
|
|
37
|
-
gazetta publish
|
|
38
|
-
gazetta publish
|
|
39
|
-
gazetta
|
|
40
|
-
gazetta
|
|
76
|
+
gazetta init my-site # scaffold a new site
|
|
77
|
+
gazetta dev # dev server (auto-detect site)
|
|
78
|
+
gazetta publish # publish to default target
|
|
79
|
+
gazetta publish production # publish to production
|
|
80
|
+
gazetta publish production my-site # publish specific site to production
|
|
81
|
+
gazetta serve production -p 8080 # serve production on port 8080
|
|
82
|
+
gazetta validate # check site for errors
|
|
41
83
|
`);
|
|
42
84
|
}
|
|
43
85
|
function parseArgs(input) {
|
|
44
|
-
|
|
86
|
+
const positional = [];
|
|
45
87
|
let port;
|
|
46
|
-
let target;
|
|
47
88
|
for (let i = 0; i < input.length; i++) {
|
|
48
89
|
if (input[i] === '--port' || input[i] === '-p') {
|
|
49
90
|
port = parseInt(input[++i], 10);
|
|
50
91
|
}
|
|
51
|
-
else if (input[i] === '--target' || input[i] === '-t') {
|
|
52
|
-
target = input[++i];
|
|
53
|
-
}
|
|
54
92
|
else if (!input[i].startsWith('-')) {
|
|
55
|
-
|
|
93
|
+
positional.push(input[i]);
|
|
56
94
|
}
|
|
57
95
|
}
|
|
58
|
-
return {
|
|
96
|
+
return { positional, port };
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Resolve the site directory from positional args or auto-detection.
|
|
100
|
+
* For commands like `dev` and `validate`, the first positional is the site.
|
|
101
|
+
* For commands like `publish` and `serve`, the first positional is the target
|
|
102
|
+
* and the second is the site.
|
|
103
|
+
*/
|
|
104
|
+
async function resolveSiteDir(positionalSite) {
|
|
105
|
+
// Explicit site dir provided
|
|
106
|
+
if (positionalSite) {
|
|
107
|
+
const dir = resolve(positionalSite);
|
|
108
|
+
if (existsSync(join(dir, 'site.yaml')))
|
|
109
|
+
return dir;
|
|
110
|
+
// Maybe it's a site name under sites/
|
|
111
|
+
const sitesSubdir = resolve('sites', positionalSite);
|
|
112
|
+
if (existsSync(join(sitesSubdir, 'site.yaml')))
|
|
113
|
+
return sitesSubdir;
|
|
114
|
+
// Maybe it's a project root with sites/
|
|
115
|
+
const mainSite = resolve(dir, 'sites/main');
|
|
116
|
+
if (existsSync(join(mainSite, 'site.yaml')))
|
|
117
|
+
return mainSite;
|
|
118
|
+
return dir; // let loadSite produce a clear error
|
|
119
|
+
}
|
|
120
|
+
// Auto-detect: check current dir first
|
|
121
|
+
if (existsSync(join(resolve('.'), 'site.yaml')))
|
|
122
|
+
return resolve('.');
|
|
123
|
+
// Check sites/ directory
|
|
124
|
+
const sitesDir = resolve('sites');
|
|
125
|
+
if (existsSync(sitesDir)) {
|
|
126
|
+
const { readdirSync, statSync } = await import('node:fs');
|
|
127
|
+
const sites = readdirSync(sitesDir)
|
|
128
|
+
.filter(name => {
|
|
129
|
+
const dir = join(sitesDir, name);
|
|
130
|
+
return statSync(dir).isDirectory() && existsSync(join(dir, 'site.yaml'));
|
|
131
|
+
});
|
|
132
|
+
if (sites.length === 1)
|
|
133
|
+
return join(sitesDir, sites[0]);
|
|
134
|
+
if (sites.length > 1) {
|
|
135
|
+
if (process.env.CI) {
|
|
136
|
+
console.error(`\n Error: multiple sites found. Specify one: gazetta ${command} <site>\n Available: ${sites.join(', ')}\n`);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
const { select } = await import('@clack/prompts');
|
|
140
|
+
const result = await select({
|
|
141
|
+
message: 'Select site:',
|
|
142
|
+
options: sites.map(s => ({ value: s, label: s })),
|
|
143
|
+
});
|
|
144
|
+
if (typeof result === 'symbol')
|
|
145
|
+
process.exit(0); // cancelled
|
|
146
|
+
return join(sitesDir, result);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// No site found — give a helpful error
|
|
150
|
+
console.error(`\n Error: no site found in current directory.\n`);
|
|
151
|
+
console.error(` To create a new project: gazetta init my-site`);
|
|
152
|
+
console.error(` To use an existing site: gazetta ${command} <path-to-site>\n`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Resolve target from positional args or auto-detection.
|
|
157
|
+
* Prompts if multiple targets and no explicit choice.
|
|
158
|
+
*/
|
|
159
|
+
async function resolveTarget(positionalTarget, siteDir) {
|
|
160
|
+
if (positionalTarget)
|
|
161
|
+
return positionalTarget;
|
|
162
|
+
const siteYamlPath = join(siteDir, 'site.yaml');
|
|
163
|
+
if (!existsSync(siteYamlPath))
|
|
164
|
+
return undefined;
|
|
165
|
+
const siteYaml = yaml.load(readFileSync(siteYamlPath, 'utf-8'));
|
|
166
|
+
const targets = Object.keys(siteYaml.targets ?? {});
|
|
167
|
+
if (targets.length <= 1)
|
|
168
|
+
return targets[0]; // auto-select if 0 or 1
|
|
169
|
+
if (process.env.CI) {
|
|
170
|
+
console.error(`\n Error: multiple targets found. Specify one: gazetta ${command} <target>\n Available: ${targets.join(', ')}\n`);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
const { select } = await import('@clack/prompts');
|
|
174
|
+
const result = await select({
|
|
175
|
+
message: 'Select target:',
|
|
176
|
+
options: targets.map(t => ({ value: t, label: t })),
|
|
177
|
+
});
|
|
178
|
+
if (typeof result === 'symbol')
|
|
179
|
+
process.exit(0);
|
|
180
|
+
return result;
|
|
59
181
|
}
|
|
60
182
|
async function runInit(dir) {
|
|
61
183
|
const { writeFile, mkdir } = await import('node:fs/promises');
|
|
62
184
|
const target = resolve(dir);
|
|
63
|
-
if (existsSync(join(target, 'site.yaml'))) {
|
|
64
|
-
console.error(`\n Error:
|
|
185
|
+
if (existsSync(join(target, 'sites')) || existsSync(join(target, 'site.yaml'))) {
|
|
186
|
+
console.error(`\n Error: project already exists in ${target}\n`);
|
|
65
187
|
process.exit(1);
|
|
66
188
|
}
|
|
67
189
|
const name = target.split('/').pop() ?? 'my-site';
|
|
68
190
|
const files = {
|
|
69
|
-
'site.yaml': `name: ${name}\nversion: 1.0.0\n`,
|
|
191
|
+
'sites/main/site.yaml': `name: ${name}\nversion: 1.0.0\n`,
|
|
70
192
|
'templates/page-layout/index.ts': `import { z } from 'zod'
|
|
71
193
|
import type { TemplateFunction } from 'gazetta'
|
|
72
194
|
|
|
@@ -155,15 +277,14 @@ const template: TemplateFunction = ({ content = {} }) => {
|
|
|
155
277
|
|
|
156
278
|
export default template
|
|
157
279
|
`,
|
|
158
|
-
'fragments/header/fragment.yaml': `template: nav
|
|
280
|
+
'sites/main/fragments/header/fragment.yaml': `template: nav
|
|
159
281
|
content:
|
|
160
282
|
brand: ${name}
|
|
161
283
|
links:
|
|
162
284
|
- label: Home
|
|
163
285
|
href: /
|
|
164
286
|
`,
|
|
165
|
-
'pages/home/page.yaml': `
|
|
166
|
-
template: page-layout
|
|
287
|
+
'sites/main/pages/home/page.yaml': `template: page-layout
|
|
167
288
|
content:
|
|
168
289
|
title: ${name}
|
|
169
290
|
description: A site built with Gazetta
|
|
@@ -172,22 +293,29 @@ components:
|
|
|
172
293
|
- hero
|
|
173
294
|
- intro
|
|
174
295
|
`,
|
|
175
|
-
'pages/home/hero/component.yaml': `template: hero
|
|
296
|
+
'sites/main/pages/home/hero/component.yaml': `template: hero
|
|
176
297
|
content:
|
|
177
298
|
title: Welcome to ${name}
|
|
178
299
|
subtitle: A site built with Gazetta
|
|
179
300
|
`,
|
|
180
|
-
'pages/home/intro/component.yaml': `template: text-block
|
|
301
|
+
'sites/main/pages/home/intro/component.yaml': `template: text-block
|
|
181
302
|
content:
|
|
182
303
|
body: "<p>Edit this content in the CMS at <a href='/admin'>/admin</a>.</p>"
|
|
183
304
|
`,
|
|
305
|
+
'sites/main/pages/404/page.yaml': `template: page-layout
|
|
306
|
+
content:
|
|
307
|
+
title: "Page Not Found"
|
|
308
|
+
description: "The page you're looking for doesn't exist."
|
|
309
|
+
`,
|
|
310
|
+
'admin/.gitkeep': '',
|
|
311
|
+
'.gitignore': `node_modules/\ndist/\n.env.local\n`,
|
|
184
312
|
'package.json': JSON.stringify({
|
|
185
313
|
name,
|
|
186
314
|
private: true,
|
|
187
315
|
type: 'module',
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
316
|
+
engines: { node: '>=22' },
|
|
317
|
+
scripts: { dev: 'gazetta dev' },
|
|
318
|
+
dependencies: { gazetta: '*', react: '^19.0.0', 'react-dom': '^19.0.0', zod: '^4.0.0' },
|
|
191
319
|
}, null, 2) + '\n',
|
|
192
320
|
};
|
|
193
321
|
for (const [path, content] of Object.entries(files)) {
|
|
@@ -195,27 +323,48 @@ content:
|
|
|
195
323
|
await mkdir(join(fullPath, '..'), { recursive: true });
|
|
196
324
|
await writeFile(fullPath, content);
|
|
197
325
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
326
|
+
const { intro, outro, note, spinner } = await import('@clack/prompts');
|
|
327
|
+
intro(c.bgGreen(c.bold(' gazetta ')));
|
|
328
|
+
note(`${c.bold('templates/')} ${c.dim('4 templates (hero, nav, page-layout, text-block)')}\n` +
|
|
329
|
+
`${c.bold('admin/')} ${c.dim('custom editors and fields')}\n` +
|
|
330
|
+
`${c.bold('sites/main/')} ${c.dim('site content')}\n` +
|
|
331
|
+
` ${c.dim('pages/home/')} ${c.dim('home page with hero + intro')}\n` +
|
|
332
|
+
` ${c.dim('pages/404/')} ${c.dim('error page')}\n` +
|
|
333
|
+
` ${c.dim('fragments/header/')} ${c.dim('shared header nav')}\n` +
|
|
334
|
+
` ${c.dim('site.yaml')} ${c.dim('site config')}\n` +
|
|
335
|
+
`${c.bold('package.json')}`, `Created ${c.green(name)}/`);
|
|
336
|
+
// Run npm install
|
|
337
|
+
const s = spinner();
|
|
338
|
+
s.start('Installing dependencies');
|
|
339
|
+
try {
|
|
340
|
+
const { execSync } = await import('node:child_process');
|
|
341
|
+
execSync('npm install', { cwd: target, stdio: 'pipe' });
|
|
342
|
+
s.stop('Dependencies installed');
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
s.stop('npm install failed — run it manually');
|
|
346
|
+
}
|
|
347
|
+
const cdStep = dir !== '.' ? `cd ${dir} && ` : '';
|
|
348
|
+
outro(`Done! Run: ${c.cyan(`${cdStep}npx gazetta dev`)}`);
|
|
205
349
|
}
|
|
206
350
|
async function runPublish(siteDir, targetName) {
|
|
207
351
|
const storage = createFilesystemProvider();
|
|
208
|
-
|
|
209
|
-
const
|
|
352
|
+
const projectRoot = detectProjectRoot(siteDir);
|
|
353
|
+
const templatesDir = join(projectRoot, 'templates');
|
|
354
|
+
const site = await loadSite({ siteDir, storage, templatesDir });
|
|
210
355
|
// Load target configs from site.yaml
|
|
211
356
|
const siteYamlPath = join(siteDir, 'site.yaml');
|
|
212
357
|
if (!existsSync(siteYamlPath)) {
|
|
213
|
-
console.error(`\n Error: No site.yaml found at ${siteDir}\n`);
|
|
358
|
+
console.error(`\n ${c.red('Error:')} No site.yaml found at ${siteDir}\n`);
|
|
214
359
|
process.exit(1);
|
|
215
360
|
}
|
|
216
361
|
const siteYaml = yaml.load(readFileSync(siteYamlPath, 'utf-8'));
|
|
217
362
|
if (!siteYaml.targets || Object.keys(siteYaml.targets).length === 0) {
|
|
218
|
-
console.error(`\n Error:
|
|
363
|
+
console.error(`\n Error: no targets configured in ${siteYamlPath}`);
|
|
364
|
+
console.error(`\n Add a target to site.yaml:\n`);
|
|
365
|
+
console.error(` targets:`);
|
|
366
|
+
console.error(` staging:`);
|
|
367
|
+
console.error(` storage: { type: filesystem, path: ./dist/staging }\n`);
|
|
219
368
|
process.exit(1);
|
|
220
369
|
}
|
|
221
370
|
// Determine which targets to publish to
|
|
@@ -230,10 +379,13 @@ async function runPublish(siteDir, targetName) {
|
|
|
230
379
|
const { createTargetRegistry } = await import('../targets.js');
|
|
231
380
|
const targets = await createTargetRegistry(Object.fromEntries(targetNames.map(n => [n, siteYaml.targets[n]])), siteDir);
|
|
232
381
|
const { publishPageRendered, publishPageStatic, publishFragmentRendered, publishSiteManifest, publishFragmentIndex } = await import('../publish-rendered.js');
|
|
233
|
-
console.log(
|
|
234
|
-
console.log(`
|
|
235
|
-
console.log(
|
|
236
|
-
console.log(`
|
|
382
|
+
console.log();
|
|
383
|
+
console.log(` ${c.bgGreen(c.bold(' gazetta '))} ${c.green('publish')} ${c.dim(site.manifest.name)}`);
|
|
384
|
+
console.log();
|
|
385
|
+
console.log(` ${c.dim('┃')} Pages ${c.dim([...site.pages.keys()].join(', '))}`);
|
|
386
|
+
console.log(` ${c.dim('┃')} Fragments ${c.dim([...site.fragments.keys()].join(', '))}`);
|
|
387
|
+
console.log(` ${c.dim('┃')} Targets ${targetNames.join(', ')}`);
|
|
388
|
+
console.log();
|
|
237
389
|
for (const name of targetNames) {
|
|
238
390
|
const targetStorage = targets.get(name);
|
|
239
391
|
if (!targetStorage) {
|
|
@@ -241,47 +393,283 @@ async function runPublish(siteDir, targetName) {
|
|
|
241
393
|
continue;
|
|
242
394
|
}
|
|
243
395
|
const targetConfig = siteYaml.targets[name];
|
|
244
|
-
const
|
|
245
|
-
|
|
396
|
+
const { getPublishMode } = await import('../types.js');
|
|
397
|
+
const publishMode = targetConfig ? getPublishMode(targetConfig) : 'static';
|
|
398
|
+
const isStatic = publishMode === 'static';
|
|
399
|
+
console.log(` ${c.bold(name)} ${c.dim(`(${publishMode})`)}`);
|
|
246
400
|
let totalFiles = 0;
|
|
401
|
+
let totalRemoved = 0;
|
|
247
402
|
if (isStatic) {
|
|
248
403
|
// Static mode — fully assembled HTML, no fragments needed separately
|
|
249
404
|
for (const pageName of site.pages.keys()) {
|
|
250
|
-
const { files } = await publishPageStatic(pageName, storage, siteDir, targetStorage);
|
|
405
|
+
const { files } = await publishPageStatic(pageName, storage, siteDir, targetStorage, templatesDir);
|
|
251
406
|
totalFiles += files;
|
|
252
|
-
console.log(`
|
|
407
|
+
console.log(` ${c.green('✓')} ${pageName}`);
|
|
253
408
|
}
|
|
254
409
|
}
|
|
255
410
|
else {
|
|
256
411
|
// ESI mode — fragments separate, pages with placeholders
|
|
257
412
|
for (const fragName of site.fragments.keys()) {
|
|
258
|
-
const { files } = await publishFragmentRendered(fragName, storage, siteDir, targetStorage);
|
|
413
|
+
const { files, removed } = await publishFragmentRendered(fragName, storage, siteDir, targetStorage, templatesDir);
|
|
259
414
|
totalFiles += files;
|
|
260
|
-
|
|
415
|
+
totalRemoved += removed;
|
|
416
|
+
console.log(` ${c.green('✓')} @${fragName}`);
|
|
261
417
|
}
|
|
262
418
|
for (const pageName of site.pages.keys()) {
|
|
263
|
-
const { files } = await publishPageRendered(pageName, storage, siteDir, targetStorage, targetConfig?.cache);
|
|
419
|
+
const { files, removed } = await publishPageRendered(pageName, storage, siteDir, targetStorage, targetConfig?.cache, templatesDir);
|
|
264
420
|
totalFiles += files;
|
|
265
|
-
|
|
421
|
+
totalRemoved += removed;
|
|
422
|
+
console.log(` ${c.green('✓')} ${pageName}`);
|
|
266
423
|
}
|
|
267
424
|
}
|
|
268
425
|
// Site manifest + fragment index
|
|
269
426
|
await publishSiteManifest(storage, siteDir, targetStorage);
|
|
270
427
|
await publishFragmentIndex(storage, siteDir, targetStorage);
|
|
271
428
|
totalFiles += 2;
|
|
272
|
-
|
|
429
|
+
const removedMsg = totalRemoved > 0 ? c.dim(` (${totalRemoved} old files cleaned)`) : '';
|
|
430
|
+
console.log(`\n ${c.green('✓')} ${c.bold(name)}: ${totalFiles} files published${removedMsg}\n`);
|
|
273
431
|
}
|
|
274
|
-
// Purge cache
|
|
275
|
-
const
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
432
|
+
// Purge CDN cache per target
|
|
433
|
+
const { resolveEnvVars } = await import('../targets.js');
|
|
434
|
+
for (const [name, config] of Object.entries(siteYaml.targets ?? {})) {
|
|
435
|
+
const purge = config.cache?.purge;
|
|
436
|
+
if (!purge)
|
|
437
|
+
continue;
|
|
438
|
+
if (purge.type === 'cloudflare') {
|
|
439
|
+
const apiToken = resolveEnvVars(purge.apiToken);
|
|
440
|
+
if (!apiToken) {
|
|
441
|
+
console.log(` ${name}: purge.apiToken not set, skipping cache purge`);
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
try {
|
|
445
|
+
const { lookupCloudflareZoneId } = await import('../publish-rendered.js');
|
|
446
|
+
const zoneId = resolveEnvVars(purge.zoneId) ?? (config.siteUrl ? await lookupCloudflareZoneId(config.siteUrl, apiToken) : null);
|
|
447
|
+
if (!zoneId) {
|
|
448
|
+
console.log(` ${name}: zone not found, set purge.zoneId or siteUrl`);
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
const { createCloudflarePurge } = await import('../publish-rendered.js');
|
|
452
|
+
await createCloudflarePurge(zoneId, apiToken).purgeAll();
|
|
453
|
+
console.log(` ${name}: cache purged`);
|
|
454
|
+
}
|
|
455
|
+
catch (err) {
|
|
456
|
+
console.warn(` ${name}: cache purge failed: ${err.message}`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
282
459
|
}
|
|
283
460
|
console.log(` Done!\n`);
|
|
284
461
|
}
|
|
462
|
+
async function runBuild(siteDir) {
|
|
463
|
+
const projectRoot = detectProjectRoot(siteDir);
|
|
464
|
+
const outDir = join(projectRoot, 'dist', 'admin');
|
|
465
|
+
console.log();
|
|
466
|
+
console.log(` ${c.bgGreen(c.bold(' gazetta '))} ${c.green('build')}`);
|
|
467
|
+
console.log();
|
|
468
|
+
// Find the admin source (monorepo) or pre-built admin (npm package)
|
|
469
|
+
const cmsWebDir = findCmsDir();
|
|
470
|
+
const cmsStaticDir = findCmsStaticDir();
|
|
471
|
+
if (cmsWebDir) {
|
|
472
|
+
// Monorepo — build from source via Vite
|
|
473
|
+
console.log(` ${c.dim('┃')} Admin source ${c.dim(cmsWebDir)}`);
|
|
474
|
+
console.log(` ${c.dim('┃')} Output ${c.dim(outDir)}`);
|
|
475
|
+
console.log();
|
|
476
|
+
const { build } = await import('vite');
|
|
477
|
+
await build({
|
|
478
|
+
configFile: join(cmsWebDir, 'vite.config.ts'),
|
|
479
|
+
root: cmsWebDir,
|
|
480
|
+
base: '/admin/',
|
|
481
|
+
build: {
|
|
482
|
+
outDir,
|
|
483
|
+
emptyOutDir: true,
|
|
484
|
+
chunkSizeWarningLimit: 2000,
|
|
485
|
+
rollupOptions: {
|
|
486
|
+
output: {
|
|
487
|
+
manualChunks: {
|
|
488
|
+
'vendor-react': ['react', 'react-dom', 'react-dom/client'],
|
|
489
|
+
'vendor-editor': ['@rjsf/core', '@rjsf/utils', '@rjsf/validator-ajv8', '@hello-pangea/dnd'],
|
|
490
|
+
'vendor-tiptap': ['@tiptap/react', '@tiptap/starter-kit', '@tiptap/extension-link', '@tiptap/extension-placeholder'],
|
|
491
|
+
},
|
|
492
|
+
},
|
|
493
|
+
onwarn(warning, defaultHandler) {
|
|
494
|
+
if (warning.code === 'MODULE_LEVEL_DIRECTIVE')
|
|
495
|
+
return;
|
|
496
|
+
if (warning.code === 'PLUGIN_WARNING' && warning.message?.includes('dynamically imported'))
|
|
497
|
+
return;
|
|
498
|
+
defaultHandler(warning);
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
logLevel: 'warn',
|
|
503
|
+
});
|
|
504
|
+
console.log(` ${c.green('✓')} Admin UI built to ${c.dim(outDir)}`);
|
|
505
|
+
}
|
|
506
|
+
else if (cmsStaticDir) {
|
|
507
|
+
// npm package — copy pre-built admin
|
|
508
|
+
const { cp } = await import('node:fs/promises');
|
|
509
|
+
const { mkdir } = await import('node:fs/promises');
|
|
510
|
+
await mkdir(outDir, { recursive: true });
|
|
511
|
+
await cp(cmsStaticDir, outDir, { recursive: true });
|
|
512
|
+
console.log(` ${c.green('✓')} Admin UI copied to ${c.dim(outDir)}`);
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
console.error(` ${c.red('Error:')} admin UI source not found`);
|
|
516
|
+
console.error(` ${c.dim('Run from monorepo or install gazetta from npm')}`);
|
|
517
|
+
process.exit(1);
|
|
518
|
+
}
|
|
519
|
+
// Bundle custom editors and fields with esbuild + shared import map
|
|
520
|
+
const adminDir = join(projectRoot, 'admin');
|
|
521
|
+
const editorsDir = join(adminDir, 'editors');
|
|
522
|
+
const fieldsDir = join(adminDir, 'fields');
|
|
523
|
+
const entryExtensions = ['.ts', '.tsx', '.jsx'];
|
|
524
|
+
const hasEditors = existsSync(editorsDir) && (await import('node:fs')).readdirSync(editorsDir).some(f => entryExtensions.some(ext => f.endsWith(ext)));
|
|
525
|
+
const hasFields = existsSync(fieldsDir) && (await import('node:fs')).readdirSync(fieldsDir).some(f => entryExtensions.some(ext => f.endsWith(ext)));
|
|
526
|
+
if (hasEditors || hasFields) {
|
|
527
|
+
const { build: esbuild } = await import('esbuild');
|
|
528
|
+
const { writeFile: writeFileAsync, mkdir: mkdirAsync } = await import('node:fs/promises');
|
|
529
|
+
const sharedDir = join(outDir, '_shared');
|
|
530
|
+
await mkdirAsync(sharedDir, { recursive: true });
|
|
531
|
+
// Build shared dependency bundles (one copy of React, etc.)
|
|
532
|
+
const sharedDeps = {
|
|
533
|
+
'react': 'export * from "react"; import React from "react"; export default React;',
|
|
534
|
+
'react-dom/client': 'export * from "react-dom/client";',
|
|
535
|
+
'react/jsx-runtime': 'export * from "react/jsx-runtime";',
|
|
536
|
+
'gazetta/editor': 'export * from "gazetta/editor";',
|
|
537
|
+
'gazetta/types': 'export * from "gazetta/types";',
|
|
538
|
+
};
|
|
539
|
+
const importMap = {};
|
|
540
|
+
for (const [specifier, stub] of Object.entries(sharedDeps)) {
|
|
541
|
+
const safeName = specifier.replace(/\//g, '_');
|
|
542
|
+
const stubFile = join(sharedDir, `_stub_${safeName}.js`);
|
|
543
|
+
await writeFileAsync(stubFile, stub);
|
|
544
|
+
const outfile = join(sharedDir, `${safeName}.js`);
|
|
545
|
+
try {
|
|
546
|
+
await esbuild({
|
|
547
|
+
entryPoints: [stubFile],
|
|
548
|
+
outfile,
|
|
549
|
+
bundle: true,
|
|
550
|
+
format: 'esm',
|
|
551
|
+
platform: 'browser',
|
|
552
|
+
target: 'es2022',
|
|
553
|
+
minify: true,
|
|
554
|
+
define: { 'process.env.NODE_ENV': '"production"' },
|
|
555
|
+
logLevel: 'warning',
|
|
556
|
+
});
|
|
557
|
+
importMap[specifier] = `/admin/_shared/${safeName}.js`;
|
|
558
|
+
}
|
|
559
|
+
catch { /* skip — dep may not be installed */ }
|
|
560
|
+
await import('node:fs/promises').then(fs => fs.rm(stubFile, { force: true }));
|
|
561
|
+
}
|
|
562
|
+
console.log(` ${c.green('✓')} Shared deps: ${Object.keys(importMap).join(', ')}`);
|
|
563
|
+
// Bundle each custom editor/field with shared deps externalized
|
|
564
|
+
const externals = Object.keys(importMap);
|
|
565
|
+
let bundledCount = 0;
|
|
566
|
+
for (const [kind, srcDir] of [['editors', editorsDir], ['fields', fieldsDir]]) {
|
|
567
|
+
if (!existsSync(srcDir))
|
|
568
|
+
continue;
|
|
569
|
+
const { readdirSync } = await import('node:fs');
|
|
570
|
+
const files = readdirSync(srcDir).filter(f => entryExtensions.some(ext => f.endsWith(ext)) && !f.startsWith('.') && !f.startsWith('_'));
|
|
571
|
+
for (const file of files) {
|
|
572
|
+
const name = file.replace(/\.(ts|tsx|jsx)$/, '');
|
|
573
|
+
const entryPoint = join(srcDir, file);
|
|
574
|
+
const outfile = join(outDir, kind, `${name}.js`);
|
|
575
|
+
await esbuild({
|
|
576
|
+
entryPoints: [entryPoint],
|
|
577
|
+
outfile,
|
|
578
|
+
bundle: true,
|
|
579
|
+
format: 'esm',
|
|
580
|
+
platform: 'browser',
|
|
581
|
+
target: 'es2022',
|
|
582
|
+
minify: true,
|
|
583
|
+
external: externals,
|
|
584
|
+
define: { 'process.env.NODE_ENV': '"production"' },
|
|
585
|
+
logLevel: 'warning',
|
|
586
|
+
});
|
|
587
|
+
bundledCount++;
|
|
588
|
+
console.log(` ${c.green('✓')} ${kind}/${name}.js`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
// Inject import map into index.html
|
|
592
|
+
const indexPath = join(outDir, 'index.html');
|
|
593
|
+
if (existsSync(indexPath)) {
|
|
594
|
+
let html = readFileSync(indexPath, 'utf-8');
|
|
595
|
+
const mapScript = `<script type="importmap">\n${JSON.stringify({ imports: importMap }, null, 2)}\n</script>`;
|
|
596
|
+
html = html.replace('<head>', `<head>\n${mapScript}`);
|
|
597
|
+
await writeFileAsync(indexPath, html);
|
|
598
|
+
console.log(` ${c.green('✓')} Import map injected into index.html`);
|
|
599
|
+
}
|
|
600
|
+
console.log(`\n ${bundledCount} custom ${bundledCount === 1 ? 'module' : 'modules'} bundled`);
|
|
601
|
+
}
|
|
602
|
+
console.log();
|
|
603
|
+
}
|
|
604
|
+
async function runAdmin(siteDir, port) {
|
|
605
|
+
const projectRoot = detectProjectRoot(siteDir);
|
|
606
|
+
const templatesDir = join(projectRoot, 'templates');
|
|
607
|
+
const adminDir = join(projectRoot, 'admin');
|
|
608
|
+
const builtAdminDir = join(projectRoot, 'dist', 'admin');
|
|
609
|
+
if (!existsSync(join(builtAdminDir, 'index.html'))) {
|
|
610
|
+
console.error(`\n ${c.red('Error:')} admin UI not built`);
|
|
611
|
+
console.error(` Run ${c.cyan('gazetta build')} first\n`);
|
|
612
|
+
process.exit(1);
|
|
613
|
+
}
|
|
614
|
+
const app = new Hono();
|
|
615
|
+
app.get('/__reload', (ctx) => ctx.body(null, 204));
|
|
616
|
+
const fsStorage = createFilesystemProvider();
|
|
617
|
+
await setupProductionMode(app, siteDir, fsStorage, builtAdminDir, templatesDir, adminDir);
|
|
618
|
+
// SPA fallback for non-API admin routes
|
|
619
|
+
app.get('*', (ctx) => {
|
|
620
|
+
const indexPath = join(builtAdminDir, 'index.html');
|
|
621
|
+
if (existsSync(indexPath))
|
|
622
|
+
return ctx.html(readFileSync(indexPath, 'utf-8'));
|
|
623
|
+
return ctx.notFound();
|
|
624
|
+
});
|
|
625
|
+
const siteYaml = yaml.load(readFileSync(join(siteDir, 'site.yaml'), 'utf-8'));
|
|
626
|
+
const server = serve({ fetch: app.fetch, port }, () => {
|
|
627
|
+
console.log();
|
|
628
|
+
console.log(` ${c.bgGreen(c.bold(' gazetta '))} ${c.green('admin')} ${c.dim(siteYaml.name)}`);
|
|
629
|
+
console.log();
|
|
630
|
+
console.log(` ${c.dim('┃')} Admin ${c.cyan(`http://localhost:${port}/admin`)}`);
|
|
631
|
+
console.log();
|
|
632
|
+
});
|
|
633
|
+
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
634
|
+
process.on(signal, () => { console.log(`\n Shutting down...`); server.close(() => process.exit(0)); });
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
async function runServe(siteDir, port, targetName) {
|
|
638
|
+
const siteYamlPath = join(siteDir, 'site.yaml');
|
|
639
|
+
if (!existsSync(siteYamlPath)) {
|
|
640
|
+
console.error(`\n Error: ${siteYamlPath} not found\n`);
|
|
641
|
+
process.exit(1);
|
|
642
|
+
}
|
|
643
|
+
const siteYaml = yaml.load(readFileSync(siteYamlPath, 'utf-8'));
|
|
644
|
+
if (!siteYaml.targets || Object.keys(siteYaml.targets).length === 0) {
|
|
645
|
+
console.error('\n Error: no targets configured in site.yaml\n');
|
|
646
|
+
process.exit(1);
|
|
647
|
+
}
|
|
648
|
+
const name = targetName ?? Object.keys(siteYaml.targets)[0];
|
|
649
|
+
const config = siteYaml.targets[name];
|
|
650
|
+
if (!config) {
|
|
651
|
+
console.error(`\n Error: target "${name}" not found in site.yaml\n`);
|
|
652
|
+
process.exit(1);
|
|
653
|
+
}
|
|
654
|
+
const { createStorageProvider } = await import('../targets.js');
|
|
655
|
+
const storage = await createStorageProvider(config.storage, siteDir);
|
|
656
|
+
const { getPublishMode } = await import('../types.js');
|
|
657
|
+
const { createServer } = await import('../serve.js');
|
|
658
|
+
const app = createServer({ storage, mode: getPublishMode(config) });
|
|
659
|
+
const server = serve({ fetch: app.fetch, port }, () => {
|
|
660
|
+
console.log();
|
|
661
|
+
console.log(` ${c.bgGreen(c.bold(' gazetta '))} ${c.green('serve')} ${c.dim(siteYaml.name)} ${c.dim(`(${name})`)}`);
|
|
662
|
+
console.log();
|
|
663
|
+
console.log(` ${c.dim('┃')} Local ${c.cyan(`http://localhost:${port}/`)}`);
|
|
664
|
+
console.log();
|
|
665
|
+
});
|
|
666
|
+
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
667
|
+
process.on(signal, () => {
|
|
668
|
+
console.log(`\n Shutting down...`);
|
|
669
|
+
server.close(() => process.exit(0));
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
}
|
|
285
673
|
async function runDeploy(siteDir, targetName) {
|
|
286
674
|
const { execSync } = await import('node:child_process');
|
|
287
675
|
const { writeFile, mkdir, rm } = await import('node:fs/promises');
|
|
@@ -296,7 +684,7 @@ async function runDeploy(siteDir, targetName) {
|
|
|
296
684
|
process.exit(1);
|
|
297
685
|
}
|
|
298
686
|
if (!targetName) {
|
|
299
|
-
console.error(`\n Error:
|
|
687
|
+
console.error(`\n ${c.red('Error:')} target is required for deploy\n Usage: gazetta deploy <target-name>\n`);
|
|
300
688
|
process.exit(1);
|
|
301
689
|
}
|
|
302
690
|
const target = siteYaml.targets[targetName];
|
|
@@ -354,19 +742,23 @@ async function runDeploy(siteDir, targetName) {
|
|
|
354
742
|
finally {
|
|
355
743
|
await rm(tmpDir, { recursive: true, force: true });
|
|
356
744
|
}
|
|
357
|
-
console.log(`\n Worker deployed. Now publish content:\n gazetta publish
|
|
745
|
+
console.log(`\n ${c.green('✓')} Worker deployed. Now publish content:\n ${c.cyan(`gazetta publish ${targetName}`)}\n`);
|
|
358
746
|
}
|
|
359
747
|
async function runValidate(siteDir) {
|
|
360
748
|
const storage = createFilesystemProvider();
|
|
361
|
-
|
|
749
|
+
const projectRoot = detectProjectRoot(siteDir);
|
|
750
|
+
const templatesDir = join(projectRoot, 'templates');
|
|
751
|
+
console.log();
|
|
752
|
+
console.log(` ${c.bgGreen(c.bold(' gazetta '))} ${c.green('validate')} ${c.dim(siteDir)}`);
|
|
753
|
+
console.log();
|
|
362
754
|
// 1. Check site.yaml
|
|
363
755
|
let site;
|
|
364
756
|
try {
|
|
365
|
-
site = await loadSite(siteDir, storage);
|
|
366
|
-
console.log(` ✓ site.yaml
|
|
757
|
+
site = await loadSite({ siteDir, storage, templatesDir });
|
|
758
|
+
console.log(` ${c.green('✓')} site.yaml ${c.dim(`— ${site.manifest.name}`)}`);
|
|
367
759
|
}
|
|
368
760
|
catch (err) {
|
|
369
|
-
console.error(` ✗ site.yaml
|
|
761
|
+
console.error(` ${c.red('✗')} site.yaml ${c.dim(`— ${err.message}`)}`);
|
|
370
762
|
process.exit(1);
|
|
371
763
|
}
|
|
372
764
|
let errors = 0;
|
|
@@ -374,14 +766,13 @@ async function runValidate(siteDir) {
|
|
|
374
766
|
for (const [fragName, frag] of site.fragments) {
|
|
375
767
|
try {
|
|
376
768
|
const { resolveComponent } = await import('../resolver.js');
|
|
377
|
-
const
|
|
378
|
-
const ctx = { site, templatesDir, visited: new Set(), path: [`@${fragName}`] };
|
|
769
|
+
const ctx = { site, templatesDir: site.templatesDir, visited: new Set(), path: [`@${fragName}`] };
|
|
379
770
|
await resolveComponent(`@${fragName}`, '', ctx);
|
|
380
771
|
const childCount = frag.components?.length ?? 0;
|
|
381
|
-
console.log(` ✓
|
|
772
|
+
console.log(` ${c.green('✓')} @${fragName} ${c.dim(`(${childCount} components)`)}`);
|
|
382
773
|
}
|
|
383
774
|
catch (err) {
|
|
384
|
-
console.error(` ✗
|
|
775
|
+
console.error(` ${c.red('✗')} @${fragName} ${c.dim(`— ${err.message}`)}`);
|
|
385
776
|
errors++;
|
|
386
777
|
}
|
|
387
778
|
}
|
|
@@ -390,23 +781,57 @@ async function runValidate(siteDir) {
|
|
|
390
781
|
try {
|
|
391
782
|
await resolvePage(pageName, site);
|
|
392
783
|
const componentCount = page.components?.length ?? 0;
|
|
393
|
-
const fragmentCount = page.components?.filter(
|
|
394
|
-
console.log(` ✓
|
|
784
|
+
const fragmentCount = page.components?.filter(cc => cc.startsWith('@')).length ?? 0;
|
|
785
|
+
console.log(` ${c.green('✓')} ${pageName} ${c.dim(`(${componentCount} components, ${fragmentCount} fragments)`)}`);
|
|
395
786
|
}
|
|
396
787
|
catch (err) {
|
|
397
|
-
console.error(` ✗
|
|
788
|
+
console.error(` ${c.red('✗')} ${pageName} ${c.dim(`— ${err.message}`)}`);
|
|
398
789
|
errors++;
|
|
399
790
|
}
|
|
400
791
|
}
|
|
401
792
|
// 4. List templates
|
|
402
|
-
|
|
793
|
+
let templateNames = [];
|
|
403
794
|
try {
|
|
404
795
|
const entries = await storage.readDir(templatesDir);
|
|
405
|
-
|
|
406
|
-
console.log(` ✓ ${
|
|
796
|
+
templateNames = entries.filter(e => e.isDirectory).map(e => e.name);
|
|
797
|
+
console.log(` ${c.green('✓')} ${c.dim(`${templateNames.length} templates`)}`);
|
|
407
798
|
}
|
|
408
799
|
catch {
|
|
409
|
-
console.log(` ⚠ templates/ directory not found`);
|
|
800
|
+
console.log(` ${c.yellow('⚠')} ${c.dim('templates/ directory not found')}`);
|
|
801
|
+
}
|
|
802
|
+
// 5. Check for orphaned editors (editor exists but template doesn't)
|
|
803
|
+
const adminDir = join(projectRoot, 'admin');
|
|
804
|
+
const editorsDir = join(adminDir, 'editors');
|
|
805
|
+
if (existsSync(editorsDir)) {
|
|
806
|
+
const editorFiles = (await import('node:fs')).readdirSync(editorsDir).filter(f => f.endsWith('.ts') || f.endsWith('.tsx'));
|
|
807
|
+
for (const file of editorFiles) {
|
|
808
|
+
const editorName = file.replace(/\.(ts|tsx)$/, '');
|
|
809
|
+
if (!templateNames.includes(editorName)) {
|
|
810
|
+
console.log(` ${c.yellow('⚠')} orphaned editor: ${c.dim(`admin/editors/${file}`)} ${c.dim('— no matching template')}`);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
// 6. Check for missing custom fields (schema references field but file doesn't exist)
|
|
815
|
+
const fieldsDir = join(adminDir, 'fields');
|
|
816
|
+
const fieldFiles = existsSync(fieldsDir) ? (await import('node:fs')).readdirSync(fieldsDir).filter(f => f.endsWith('.ts') || f.endsWith('.tsx')).map(f => f.replace(/\.(ts|tsx)$/, '')) : [];
|
|
817
|
+
const { loadTemplate } = await import('../template-loader.js');
|
|
818
|
+
const zod = await import('zod');
|
|
819
|
+
for (const tplName of templateNames) {
|
|
820
|
+
try {
|
|
821
|
+
const loaded = await loadTemplate(storage, templatesDir, tplName);
|
|
822
|
+
const jsonSchema = zod.z.toJSONSchema(loaded.schema);
|
|
823
|
+
const props = jsonSchema.properties;
|
|
824
|
+
if (!props)
|
|
825
|
+
continue;
|
|
826
|
+
for (const [propName, prop] of Object.entries(props)) {
|
|
827
|
+
const fieldRef = prop.field;
|
|
828
|
+
if (fieldRef && !fieldFiles.includes(fieldRef)) {
|
|
829
|
+
console.error(` ${c.red('✗')} template ${tplName}.${propName} references field "${fieldRef}" ${c.dim('— not found in admin/fields/')}`);
|
|
830
|
+
errors++;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
catch { /* template load errors already caught above */ }
|
|
410
835
|
}
|
|
411
836
|
console.log();
|
|
412
837
|
if (errors > 0) {
|
|
@@ -464,8 +889,10 @@ function renderErrorOverlay(err) {
|
|
|
464
889
|
}
|
|
465
890
|
async function runDev(siteDir, port) {
|
|
466
891
|
const storage = createFilesystemProvider();
|
|
467
|
-
|
|
468
|
-
const
|
|
892
|
+
const projectRoot = detectProjectRoot(siteDir);
|
|
893
|
+
const templatesDir = join(projectRoot, 'templates');
|
|
894
|
+
const adminDir = join(projectRoot, 'admin');
|
|
895
|
+
const site = await loadSite({ siteDir, storage, templatesDir });
|
|
469
896
|
const app = new Hono();
|
|
470
897
|
// ---- Live reload (SSE) ----
|
|
471
898
|
let reloadId = 0;
|
|
@@ -494,7 +921,7 @@ async function runDev(siteDir, port) {
|
|
|
494
921
|
for (const [pageName, page] of site.pages) {
|
|
495
922
|
app.get(page.route, async (c) => {
|
|
496
923
|
try {
|
|
497
|
-
const freshSite = await loadSite(siteDir, storage);
|
|
924
|
+
const freshSite = await loadSite({ siteDir, storage, templatesDir });
|
|
498
925
|
const resolved = await resolvePage(pageName, freshSite);
|
|
499
926
|
const html = await renderPage(resolved, c.req.param());
|
|
500
927
|
return c.html(html.replace('</body>', `${RELOAD_SCRIPT}\n</body>`));
|
|
@@ -504,17 +931,17 @@ async function runDev(siteDir, port) {
|
|
|
504
931
|
}
|
|
505
932
|
});
|
|
506
933
|
}
|
|
507
|
-
// ---- Detect mode: dev (monorepo with apps/admin
|
|
934
|
+
// ---- Detect mode: dev (monorepo with apps/admin source) vs production (pre-built) ----
|
|
508
935
|
const cmsWebDir = findCmsDir();
|
|
509
936
|
const cmsStaticDir = findCmsStaticDir();
|
|
510
937
|
const isDevMode = cmsWebDir !== null;
|
|
511
938
|
if (isDevMode) {
|
|
512
|
-
// Dev mode:
|
|
513
|
-
await
|
|
939
|
+
// Dev mode: mount CMS API inline (same process = shared template cache)
|
|
940
|
+
await setupCmsApi(app, siteDir, storage, templatesDir, adminDir);
|
|
514
941
|
}
|
|
515
942
|
else if (cmsStaticDir) {
|
|
516
943
|
// Production mode: inline CMS API + static files
|
|
517
|
-
await setupProductionMode(app, siteDir, storage, cmsStaticDir);
|
|
944
|
+
await setupProductionMode(app, siteDir, storage, cmsStaticDir, templatesDir, adminDir);
|
|
518
945
|
}
|
|
519
946
|
// ---- 404 ----
|
|
520
947
|
app.notFound((c) => {
|
|
@@ -522,36 +949,38 @@ async function runDev(siteDir, port) {
|
|
|
522
949
|
return c.html(`<pre style="padding:2rem">Page not found: ${c.req.path}\n\nAvailable:\n${routes}\n /admin → CMS editor</pre>`, 404);
|
|
523
950
|
});
|
|
524
951
|
// ---- Start server ----
|
|
525
|
-
|
|
526
|
-
if (isDevMode) {
|
|
527
|
-
const apiPort = port + 100;
|
|
528
|
-
apiProc = spawn('npx', ['tsx', join(cmsWebDir, 'src/server/dev.ts'), siteDir], {
|
|
529
|
-
env: { ...process.env, API_PORT: String(apiPort) },
|
|
530
|
-
stdio: 'pipe',
|
|
531
|
-
});
|
|
532
|
-
apiProc.stderr?.on('data', (d) => {
|
|
533
|
-
const msg = d.toString().trim();
|
|
534
|
-
if (msg)
|
|
535
|
-
console.error(` [admin-api] ${msg}`);
|
|
536
|
-
});
|
|
537
|
-
}
|
|
952
|
+
const startTime = performance.now();
|
|
538
953
|
const nodeServer = serve({ fetch: app.fetch, port }, async () => {
|
|
539
|
-
|
|
540
|
-
console.log(
|
|
541
|
-
console.log(`
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
954
|
+
const elapsed = Math.round(performance.now() - startTime);
|
|
955
|
+
console.log();
|
|
956
|
+
console.log(` ${c.bgGreen(c.bold(' gazetta '))} ${c.green(site.manifest.name)} ${c.dim(`ready in ${elapsed} ms`)}`);
|
|
957
|
+
console.log();
|
|
958
|
+
console.log(` ${c.dim('┃')} Local ${c.cyan(`http://localhost:${port}/`)}`);
|
|
959
|
+
if (isDevMode) {
|
|
960
|
+
console.log(` ${c.dim('┃')} CMS ${c.cyan(`http://localhost:${port}/admin`)}`);
|
|
961
|
+
console.log(` ${c.dim('┃')} Dev ${c.cyan(`http://localhost:${port}/admin/dev`)}`);
|
|
962
|
+
}
|
|
963
|
+
console.log();
|
|
964
|
+
console.log(` ${c.dim('┃')} Pages ${[...site.pages.entries()].map(([n, p]) => `${c.dim(p.route)} ${c.dim('→')} ${n}`).join(c.dim(', '))}`);
|
|
965
|
+
console.log(` ${c.dim('┃')} Frags ${c.dim([...site.fragments.keys()].join(', ') || '(none)')}`);
|
|
545
966
|
if (isDevMode && cmsWebDir) {
|
|
546
967
|
try {
|
|
547
968
|
const { createServer: createViteServer } = await import('vite');
|
|
969
|
+
const { searchForWorkspaceRoot } = await import('vite');
|
|
548
970
|
const vite = await createViteServer({
|
|
549
971
|
configFile: join(cmsWebDir, 'vite.config.ts'),
|
|
550
972
|
root: cmsWebDir,
|
|
551
973
|
base: '/admin/',
|
|
974
|
+
resolve: {
|
|
975
|
+
alias: {
|
|
976
|
+
'@editors': join(adminDir, 'editors'),
|
|
977
|
+
'@fields': join(adminDir, 'fields'),
|
|
978
|
+
},
|
|
979
|
+
},
|
|
552
980
|
server: {
|
|
553
981
|
middlewareMode: true,
|
|
554
982
|
hmr: { server: nodeServer },
|
|
983
|
+
fs: { allow: [searchForWorkspaceRoot(cmsWebDir), siteDir] },
|
|
555
984
|
},
|
|
556
985
|
});
|
|
557
986
|
const httpServer = nodeServer;
|
|
@@ -574,69 +1003,53 @@ async function runDev(siteDir, port) {
|
|
|
574
1003
|
honoHandler(req, res);
|
|
575
1004
|
}
|
|
576
1005
|
});
|
|
577
|
-
console.log(` CMS: http://localhost:${port}/admin (dev mode + HMR)`);
|
|
578
1006
|
}
|
|
579
1007
|
catch (err) {
|
|
580
1008
|
console.warn(` Warning: CMS UI failed to start: ${err.message}`);
|
|
581
1009
|
}
|
|
582
1010
|
}
|
|
583
|
-
else if (cmsStaticDir) {
|
|
584
|
-
console.log(` CMS: http://localhost:${port}/admin`);
|
|
585
|
-
}
|
|
586
1011
|
console.log();
|
|
587
1012
|
});
|
|
588
|
-
// ---- Cleanup ----
|
|
589
|
-
process.on('SIGINT', () => {
|
|
590
|
-
apiProc?.kill();
|
|
591
|
-
process.exit(0);
|
|
592
|
-
});
|
|
593
1013
|
// ---- File watching ----
|
|
1014
|
+
// Watch site dir for content changes (yaml manifests)
|
|
594
1015
|
watch(siteDir, { recursive: true }, (_event, filename) => {
|
|
595
1016
|
if (!filename)
|
|
596
1017
|
return;
|
|
597
|
-
if (filename.endsWith('.
|
|
598
|
-
const parts = filename.split('/');
|
|
599
|
-
const idx = parts.indexOf('templates');
|
|
600
|
-
if (idx >= 0 && idx + 1 < parts.length) {
|
|
601
|
-
console.log(` Template changed: ${parts[idx + 1]}`);
|
|
602
|
-
invalidateTemplate(parts[idx + 1]);
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
else if (filename.endsWith('.yaml')) {
|
|
1018
|
+
if (filename.endsWith('.yaml')) {
|
|
606
1019
|
console.log(` Manifest changed: ${filename}`);
|
|
607
1020
|
invalidateAllTemplates();
|
|
1021
|
+
notifyReload();
|
|
608
1022
|
}
|
|
609
|
-
else
|
|
610
|
-
return;
|
|
611
|
-
notifyReload();
|
|
612
1023
|
});
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
headers: c.req.raw.headers,
|
|
626
|
-
body: c.req.method !== 'GET' && c.req.method !== 'HEAD' ? c.req.raw.body : undefined,
|
|
627
|
-
// @ts-expect-error duplex needed for streaming body
|
|
628
|
-
duplex: 'half',
|
|
629
|
-
});
|
|
630
|
-
return new Response(res.body, { status: res.status, headers: res.headers });
|
|
631
|
-
}
|
|
632
|
-
catch {
|
|
633
|
-
return c.json({ error: 'CMS API not ready' }, 502);
|
|
1024
|
+
// Watch templates dir for template source changes
|
|
1025
|
+
if (existsSync(templatesDir)) {
|
|
1026
|
+
watch(templatesDir, { recursive: true }, (_event, filename) => {
|
|
1027
|
+
if (!filename)
|
|
1028
|
+
return;
|
|
1029
|
+
if (filename.endsWith('.ts') || filename.endsWith('.tsx')) {
|
|
1030
|
+
const parts = filename.split('/');
|
|
1031
|
+
if (parts.length >= 1) {
|
|
1032
|
+
console.log(` Template changed: ${parts[0]}`);
|
|
1033
|
+
invalidateTemplate(parts[0]);
|
|
1034
|
+
notifyReload();
|
|
1035
|
+
}
|
|
634
1036
|
}
|
|
635
1037
|
});
|
|
636
1038
|
}
|
|
637
1039
|
}
|
|
1040
|
+
// ---- Mount CMS API on the main Hono app (shared process = shared template cache) ----
|
|
1041
|
+
async function setupCmsApi(app, siteDir, storage, templatesDir, adminDir) {
|
|
1042
|
+
const siteYamlPath = join(siteDir, 'site.yaml');
|
|
1043
|
+
let targetConfigs;
|
|
1044
|
+
if (existsSync(siteYamlPath)) {
|
|
1045
|
+
const siteYaml = yaml.load(readFileSync(siteYamlPath, 'utf-8'));
|
|
1046
|
+
targetConfigs = siteYaml.targets;
|
|
1047
|
+
}
|
|
1048
|
+
const cmsApp = createAdminApp({ siteDir, storage, templatesDir, adminDir, targetConfigs });
|
|
1049
|
+
app.route('/admin', cmsApp);
|
|
1050
|
+
}
|
|
638
1051
|
// ---- Production mode: inline CMS API + static files from admin-dist/ ----
|
|
639
|
-
async function setupProductionMode(app, siteDir, storage, cmsStaticDir) {
|
|
1052
|
+
async function setupProductionMode(app, siteDir, storage, cmsStaticDir, templatesDir, adminDir) {
|
|
640
1053
|
// Read target configs from site.yaml — targets are initialized lazily on first publish/fetch
|
|
641
1054
|
const siteYamlPath = join(siteDir, 'site.yaml');
|
|
642
1055
|
let targetConfigs;
|
|
@@ -644,10 +1057,10 @@ async function setupProductionMode(app, siteDir, storage, cmsStaticDir) {
|
|
|
644
1057
|
const siteYaml = yaml.load(readFileSync(siteYamlPath, 'utf-8'));
|
|
645
1058
|
targetConfigs = siteYaml.targets;
|
|
646
1059
|
}
|
|
647
|
-
// Mount CMS API inline at /admin
|
|
648
|
-
const cmsApp = createAdminApp({ siteDir, storage, targetConfigs });
|
|
1060
|
+
// Mount CMS API inline at /admin (production mode — bundled editors/fields)
|
|
1061
|
+
const cmsApp = createAdminApp({ siteDir, storage, templatesDir, adminDir, production: true, targetConfigs });
|
|
649
1062
|
app.route('/admin', cmsApp);
|
|
650
|
-
// Serve pre-built CMS static files
|
|
1063
|
+
// Serve pre-built CMS static files (includes bundled editors/fields)
|
|
651
1064
|
app.use('/admin/*', serveStatic({
|
|
652
1065
|
root: cmsStaticDir,
|
|
653
1066
|
rewriteRequestPath: (path) => path.replace(/^\/admin/, ''),
|
|
@@ -668,12 +1081,12 @@ async function setupProductionMode(app, siteDir, storage, cmsStaticDir) {
|
|
|
668
1081
|
return c.text('CMS admin UI not found', 404);
|
|
669
1082
|
});
|
|
670
1083
|
}
|
|
671
|
-
/** Find apps/admin
|
|
1084
|
+
/** Find apps/admin source dir (monorepo dev mode) */
|
|
672
1085
|
function findCmsDir() {
|
|
673
1086
|
const candidates = [
|
|
674
|
-
resolve('apps/admin
|
|
675
|
-
resolve(import.meta.dirname, '../../../../apps/admin
|
|
676
|
-
resolve(import.meta.dirname, '../../../apps/admin
|
|
1087
|
+
resolve('apps/admin'),
|
|
1088
|
+
resolve(import.meta.dirname, '../../../../apps/admin'),
|
|
1089
|
+
resolve(import.meta.dirname, '../../../apps/admin'),
|
|
677
1090
|
];
|
|
678
1091
|
for (const dir of candidates) {
|
|
679
1092
|
if (existsSync(join(dir, 'src/server/dev.ts')))
|
|
@@ -699,26 +1112,80 @@ async function main() {
|
|
|
699
1112
|
process.exit(0);
|
|
700
1113
|
}
|
|
701
1114
|
const parsed = parseArgs(args.slice(1));
|
|
1115
|
+
// Commands that take [target] [site] positional args
|
|
1116
|
+
const targetFirstCommands = new Set(['publish', 'serve', 'deploy']);
|
|
1117
|
+
// Commands that take [site] positional arg
|
|
1118
|
+
const siteOnlyCommands = new Set(['dev', 'validate', 'admin']);
|
|
1119
|
+
let siteDir;
|
|
1120
|
+
let targetName;
|
|
1121
|
+
if (command === 'init') {
|
|
1122
|
+
await runInit(parsed.positional[0] ?? '.');
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
else if (command === 'build') {
|
|
1126
|
+
const siteDir = await resolveSiteDir(parsed.positional[0]);
|
|
1127
|
+
await runBuild(siteDir);
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
else if (targetFirstCommands.has(command)) {
|
|
1131
|
+
// gazetta publish [target] [site]
|
|
1132
|
+
const [first, second] = parsed.positional;
|
|
1133
|
+
// If first arg looks like a site path (contains / or has site.yaml), it's the site
|
|
1134
|
+
const firstIsSite = first && (first.includes('/') || existsSync(join(resolve(first), 'site.yaml')));
|
|
1135
|
+
if (firstIsSite) {
|
|
1136
|
+
siteDir = await resolveSiteDir(first);
|
|
1137
|
+
targetName = await resolveTarget(undefined, siteDir);
|
|
1138
|
+
}
|
|
1139
|
+
else {
|
|
1140
|
+
siteDir = await resolveSiteDir(second);
|
|
1141
|
+
targetName = await resolveTarget(first, siteDir);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
else if (siteOnlyCommands.has(command)) {
|
|
1145
|
+
siteDir = await resolveSiteDir(parsed.positional[0]);
|
|
1146
|
+
}
|
|
1147
|
+
else {
|
|
1148
|
+
console.error(` Unknown command: ${command}\n`);
|
|
1149
|
+
printHelp();
|
|
1150
|
+
process.exit(1);
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
// Load .env from project root and site dir (skipped in CI)
|
|
1154
|
+
if (!process.env.CI) {
|
|
1155
|
+
const projectRoot = detectProjectRoot(siteDir);
|
|
1156
|
+
const envDirs = projectRoot !== siteDir ? [projectRoot, siteDir] : [siteDir];
|
|
1157
|
+
for (const dir of envDirs) {
|
|
1158
|
+
for (const name of ['.env', '.env.local']) {
|
|
1159
|
+
const envPath = join(dir, name);
|
|
1160
|
+
if (existsSync(envPath)) {
|
|
1161
|
+
for (const line of readFileSync(envPath, 'utf-8').split('\n')) {
|
|
1162
|
+
const m = line.match(/^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.*)$/);
|
|
1163
|
+
if (m && !(m[1] in process.env))
|
|
1164
|
+
process.env[m[1]] = m[2].replace(/^["']|["']$/g, '');
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
702
1170
|
switch (command) {
|
|
703
|
-
case 'init':
|
|
704
|
-
await runInit(args[1] ?? '.');
|
|
705
|
-
break;
|
|
706
1171
|
case 'publish':
|
|
707
|
-
await runPublish(
|
|
1172
|
+
await runPublish(siteDir, targetName);
|
|
1173
|
+
break;
|
|
1174
|
+
case 'serve':
|
|
1175
|
+
await runServe(siteDir, parsed.port ?? 3000, targetName);
|
|
708
1176
|
break;
|
|
709
1177
|
case 'deploy':
|
|
710
|
-
await runDeploy(
|
|
1178
|
+
await runDeploy(siteDir, targetName);
|
|
711
1179
|
break;
|
|
712
1180
|
case 'validate':
|
|
713
|
-
await runValidate(
|
|
1181
|
+
await runValidate(siteDir);
|
|
714
1182
|
break;
|
|
715
1183
|
case 'dev':
|
|
716
|
-
await runDev(
|
|
1184
|
+
await runDev(siteDir, parsed.port ?? 3000);
|
|
1185
|
+
break;
|
|
1186
|
+
case 'admin':
|
|
1187
|
+
await runAdmin(siteDir, parsed.port ?? 3000);
|
|
717
1188
|
break;
|
|
718
|
-
default:
|
|
719
|
-
console.error(` Unknown command: ${command}\n`);
|
|
720
|
-
printHelp();
|
|
721
|
-
process.exit(1);
|
|
722
1189
|
}
|
|
723
1190
|
}
|
|
724
1191
|
main().catch((err) => {
|