gazetta 0.0.7 → 0.1.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-DKYjjDqE.js +2451 -0
- package/admin-dist/assets/index-yguWG9_J.css +1 -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 +3 -8
- 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 +30 -9
- 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/app.js +1 -1
- package/dist/app.js.map +1 -1
- package/dist/cli/index.js +650 -175
- 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 -10
- 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 +27 -34
- package/dist/publish-rendered.js.map +1 -1
- package/dist/renderer.d.ts +2 -1
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +25 -15
- 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 +19 -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 -16
- 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-CVC2_Byr.js +0 -1941
- package/admin-dist/assets/index-W1ylqX_Y.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,71 +12,201 @@ 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]);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
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);
|
|
56
147
|
}
|
|
57
148
|
}
|
|
58
|
-
|
|
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
|
|
|
73
|
-
export const schema = z.object({
|
|
195
|
+
export const schema = z.object({
|
|
196
|
+
title: z.string().describe('Page title'),
|
|
197
|
+
description: z.string().optional().describe('Page description'),
|
|
198
|
+
})
|
|
74
199
|
|
|
75
|
-
|
|
200
|
+
type Content = z.infer<typeof schema>
|
|
201
|
+
|
|
202
|
+
const template: TemplateFunction<Content> = ({ content, children = [] }) => ({
|
|
76
203
|
html: \`<main>\${children.map(c => c.html).join('\\n')}</main>\`,
|
|
77
204
|
css: \`main { max-width: 800px; margin: 0 auto; padding: 2rem; font-family: system-ui, sans-serif; }
|
|
78
205
|
\${children.map(c => c.css).join('\\n')}\`,
|
|
79
206
|
js: children.map(c => c.js).filter(Boolean).join('\\n'),
|
|
80
|
-
head: \`<
|
|
207
|
+
head: \`<title>\${content?.title ?? ''}</title>
|
|
208
|
+
\${content?.description ? \`<meta name="description" content="\${content.description}">\` : ''}
|
|
209
|
+
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⚡</text></svg>">
|
|
81
210
|
\${children.map(c => c.head).filter(Boolean).join('\\n')}\`,
|
|
82
211
|
})
|
|
83
212
|
|
|
@@ -148,38 +277,45 @@ const template: TemplateFunction = ({ content = {} }) => {
|
|
|
148
277
|
|
|
149
278
|
export default template
|
|
150
279
|
`,
|
|
151
|
-
'fragments/header/fragment.yaml': `template: nav
|
|
280
|
+
'sites/main/fragments/header/fragment.yaml': `template: nav
|
|
152
281
|
content:
|
|
153
282
|
brand: ${name}
|
|
154
283
|
links:
|
|
155
284
|
- label: Home
|
|
156
285
|
href: /
|
|
157
286
|
`,
|
|
158
|
-
'pages/home/page.yaml': `
|
|
159
|
-
|
|
160
|
-
metadata:
|
|
287
|
+
'sites/main/pages/home/page.yaml': `template: page-layout
|
|
288
|
+
content:
|
|
161
289
|
title: ${name}
|
|
290
|
+
description: A site built with Gazetta
|
|
162
291
|
components:
|
|
163
292
|
- "@header"
|
|
164
293
|
- hero
|
|
165
294
|
- intro
|
|
166
295
|
`,
|
|
167
|
-
'pages/home/hero/component.yaml': `template: hero
|
|
296
|
+
'sites/main/pages/home/hero/component.yaml': `template: hero
|
|
168
297
|
content:
|
|
169
298
|
title: Welcome to ${name}
|
|
170
299
|
subtitle: A site built with Gazetta
|
|
171
300
|
`,
|
|
172
|
-
'pages/home/intro/component.yaml': `template: text-block
|
|
301
|
+
'sites/main/pages/home/intro/component.yaml': `template: text-block
|
|
173
302
|
content:
|
|
174
303
|
body: "<p>Edit this content in the CMS at <a href='/admin'>/admin</a>.</p>"
|
|
175
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`,
|
|
176
312
|
'package.json': JSON.stringify({
|
|
177
313
|
name,
|
|
178
314
|
private: true,
|
|
179
315
|
type: 'module',
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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' },
|
|
183
319
|
}, null, 2) + '\n',
|
|
184
320
|
};
|
|
185
321
|
for (const [path, content] of Object.entries(files)) {
|
|
@@ -187,27 +323,48 @@ content:
|
|
|
187
323
|
await mkdir(join(fullPath, '..'), { recursive: true });
|
|
188
324
|
await writeFile(fullPath, content);
|
|
189
325
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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`)}`);
|
|
197
349
|
}
|
|
198
350
|
async function runPublish(siteDir, targetName) {
|
|
199
351
|
const storage = createFilesystemProvider();
|
|
200
|
-
|
|
201
|
-
const
|
|
352
|
+
const projectRoot = detectProjectRoot(siteDir);
|
|
353
|
+
const templatesDir = join(projectRoot, 'templates');
|
|
354
|
+
const site = await loadSite({ siteDir, storage, templatesDir });
|
|
202
355
|
// Load target configs from site.yaml
|
|
203
356
|
const siteYamlPath = join(siteDir, 'site.yaml');
|
|
204
357
|
if (!existsSync(siteYamlPath)) {
|
|
205
|
-
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`);
|
|
206
359
|
process.exit(1);
|
|
207
360
|
}
|
|
208
361
|
const siteYaml = yaml.load(readFileSync(siteYamlPath, 'utf-8'));
|
|
209
362
|
if (!siteYaml.targets || Object.keys(siteYaml.targets).length === 0) {
|
|
210
|
-
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`);
|
|
211
368
|
process.exit(1);
|
|
212
369
|
}
|
|
213
370
|
// Determine which targets to publish to
|
|
@@ -222,10 +379,13 @@ async function runPublish(siteDir, targetName) {
|
|
|
222
379
|
const { createTargetRegistry } = await import('../targets.js');
|
|
223
380
|
const targets = await createTargetRegistry(Object.fromEntries(targetNames.map(n => [n, siteYaml.targets[n]])), siteDir);
|
|
224
381
|
const { publishPageRendered, publishPageStatic, publishFragmentRendered, publishSiteManifest, publishFragmentIndex } = await import('../publish-rendered.js');
|
|
225
|
-
console.log(
|
|
226
|
-
console.log(`
|
|
227
|
-
console.log(
|
|
228
|
-
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();
|
|
229
389
|
for (const name of targetNames) {
|
|
230
390
|
const targetStorage = targets.get(name);
|
|
231
391
|
if (!targetStorage) {
|
|
@@ -233,47 +393,283 @@ async function runPublish(siteDir, targetName) {
|
|
|
233
393
|
continue;
|
|
234
394
|
}
|
|
235
395
|
const targetConfig = siteYaml.targets[name];
|
|
236
|
-
const
|
|
237
|
-
|
|
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})`)}`);
|
|
238
400
|
let totalFiles = 0;
|
|
401
|
+
let totalRemoved = 0;
|
|
239
402
|
if (isStatic) {
|
|
240
403
|
// Static mode — fully assembled HTML, no fragments needed separately
|
|
241
404
|
for (const pageName of site.pages.keys()) {
|
|
242
|
-
const { files } = await publishPageStatic(pageName, storage, siteDir, targetStorage);
|
|
405
|
+
const { files } = await publishPageStatic(pageName, storage, siteDir, targetStorage, templatesDir);
|
|
243
406
|
totalFiles += files;
|
|
244
|
-
console.log(`
|
|
407
|
+
console.log(` ${c.green('✓')} ${pageName}`);
|
|
245
408
|
}
|
|
246
409
|
}
|
|
247
410
|
else {
|
|
248
411
|
// ESI mode — fragments separate, pages with placeholders
|
|
249
412
|
for (const fragName of site.fragments.keys()) {
|
|
250
|
-
const { files } = await publishFragmentRendered(fragName, storage, siteDir, targetStorage);
|
|
413
|
+
const { files, removed } = await publishFragmentRendered(fragName, storage, siteDir, targetStorage, templatesDir);
|
|
251
414
|
totalFiles += files;
|
|
252
|
-
|
|
415
|
+
totalRemoved += removed;
|
|
416
|
+
console.log(` ${c.green('✓')} @${fragName}`);
|
|
253
417
|
}
|
|
254
418
|
for (const pageName of site.pages.keys()) {
|
|
255
|
-
const { files } = await publishPageRendered(pageName, storage, siteDir, targetStorage, targetConfig?.cache);
|
|
419
|
+
const { files, removed } = await publishPageRendered(pageName, storage, siteDir, targetStorage, targetConfig?.cache, templatesDir);
|
|
256
420
|
totalFiles += files;
|
|
257
|
-
|
|
421
|
+
totalRemoved += removed;
|
|
422
|
+
console.log(` ${c.green('✓')} ${pageName}`);
|
|
258
423
|
}
|
|
259
424
|
}
|
|
260
425
|
// Site manifest + fragment index
|
|
261
426
|
await publishSiteManifest(storage, siteDir, targetStorage);
|
|
262
427
|
await publishFragmentIndex(storage, siteDir, targetStorage);
|
|
263
428
|
totalFiles += 2;
|
|
264
|
-
|
|
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`);
|
|
265
431
|
}
|
|
266
|
-
// Purge cache
|
|
267
|
-
const
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
+
}
|
|
274
459
|
}
|
|
275
460
|
console.log(` Done!\n`);
|
|
276
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
|
+
}
|
|
277
673
|
async function runDeploy(siteDir, targetName) {
|
|
278
674
|
const { execSync } = await import('node:child_process');
|
|
279
675
|
const { writeFile, mkdir, rm } = await import('node:fs/promises');
|
|
@@ -288,7 +684,7 @@ async function runDeploy(siteDir, targetName) {
|
|
|
288
684
|
process.exit(1);
|
|
289
685
|
}
|
|
290
686
|
if (!targetName) {
|
|
291
|
-
console.error(`\n Error:
|
|
687
|
+
console.error(`\n ${c.red('Error:')} target is required for deploy\n Usage: gazetta deploy <target-name>\n`);
|
|
292
688
|
process.exit(1);
|
|
293
689
|
}
|
|
294
690
|
const target = siteYaml.targets[targetName];
|
|
@@ -346,19 +742,23 @@ async function runDeploy(siteDir, targetName) {
|
|
|
346
742
|
finally {
|
|
347
743
|
await rm(tmpDir, { recursive: true, force: true });
|
|
348
744
|
}
|
|
349
|
-
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`);
|
|
350
746
|
}
|
|
351
747
|
async function runValidate(siteDir) {
|
|
352
748
|
const storage = createFilesystemProvider();
|
|
353
|
-
|
|
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();
|
|
354
754
|
// 1. Check site.yaml
|
|
355
755
|
let site;
|
|
356
756
|
try {
|
|
357
|
-
site = await loadSite(siteDir, storage);
|
|
358
|
-
console.log(` ✓ site.yaml
|
|
757
|
+
site = await loadSite({ siteDir, storage, templatesDir });
|
|
758
|
+
console.log(` ${c.green('✓')} site.yaml ${c.dim(`— ${site.manifest.name}`)}`);
|
|
359
759
|
}
|
|
360
760
|
catch (err) {
|
|
361
|
-
console.error(` ✗ site.yaml
|
|
761
|
+
console.error(` ${c.red('✗')} site.yaml ${c.dim(`— ${err.message}`)}`);
|
|
362
762
|
process.exit(1);
|
|
363
763
|
}
|
|
364
764
|
let errors = 0;
|
|
@@ -366,14 +766,13 @@ async function runValidate(siteDir) {
|
|
|
366
766
|
for (const [fragName, frag] of site.fragments) {
|
|
367
767
|
try {
|
|
368
768
|
const { resolveComponent } = await import('../resolver.js');
|
|
369
|
-
const
|
|
370
|
-
const ctx = { site, templatesDir, visited: new Set(), path: [`@${fragName}`] };
|
|
769
|
+
const ctx = { site, templatesDir: site.templatesDir, visited: new Set(), path: [`@${fragName}`] };
|
|
371
770
|
await resolveComponent(`@${fragName}`, '', ctx);
|
|
372
771
|
const childCount = frag.components?.length ?? 0;
|
|
373
|
-
console.log(` ✓
|
|
772
|
+
console.log(` ${c.green('✓')} @${fragName} ${c.dim(`(${childCount} components)`)}`);
|
|
374
773
|
}
|
|
375
774
|
catch (err) {
|
|
376
|
-
console.error(` ✗
|
|
775
|
+
console.error(` ${c.red('✗')} @${fragName} ${c.dim(`— ${err.message}`)}`);
|
|
377
776
|
errors++;
|
|
378
777
|
}
|
|
379
778
|
}
|
|
@@ -382,23 +781,57 @@ async function runValidate(siteDir) {
|
|
|
382
781
|
try {
|
|
383
782
|
await resolvePage(pageName, site);
|
|
384
783
|
const componentCount = page.components?.length ?? 0;
|
|
385
|
-
const fragmentCount = page.components?.filter(
|
|
386
|
-
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)`)}`);
|
|
387
786
|
}
|
|
388
787
|
catch (err) {
|
|
389
|
-
console.error(` ✗
|
|
788
|
+
console.error(` ${c.red('✗')} ${pageName} ${c.dim(`— ${err.message}`)}`);
|
|
390
789
|
errors++;
|
|
391
790
|
}
|
|
392
791
|
}
|
|
393
792
|
// 4. List templates
|
|
394
|
-
|
|
793
|
+
let templateNames = [];
|
|
395
794
|
try {
|
|
396
795
|
const entries = await storage.readDir(templatesDir);
|
|
397
|
-
|
|
398
|
-
console.log(` ✓ ${
|
|
796
|
+
templateNames = entries.filter(e => e.isDirectory).map(e => e.name);
|
|
797
|
+
console.log(` ${c.green('✓')} ${c.dim(`${templateNames.length} templates`)}`);
|
|
399
798
|
}
|
|
400
799
|
catch {
|
|
401
|
-
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 */ }
|
|
402
835
|
}
|
|
403
836
|
console.log();
|
|
404
837
|
if (errors > 0) {
|
|
@@ -456,8 +889,10 @@ function renderErrorOverlay(err) {
|
|
|
456
889
|
}
|
|
457
890
|
async function runDev(siteDir, port) {
|
|
458
891
|
const storage = createFilesystemProvider();
|
|
459
|
-
|
|
460
|
-
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 });
|
|
461
896
|
const app = new Hono();
|
|
462
897
|
// ---- Live reload (SSE) ----
|
|
463
898
|
let reloadId = 0;
|
|
@@ -486,9 +921,9 @@ async function runDev(siteDir, port) {
|
|
|
486
921
|
for (const [pageName, page] of site.pages) {
|
|
487
922
|
app.get(page.route, async (c) => {
|
|
488
923
|
try {
|
|
489
|
-
const freshSite = await loadSite(siteDir, storage);
|
|
924
|
+
const freshSite = await loadSite({ siteDir, storage, templatesDir });
|
|
490
925
|
const resolved = await resolvePage(pageName, freshSite);
|
|
491
|
-
const html = await renderPage(resolved,
|
|
926
|
+
const html = await renderPage(resolved, c.req.param());
|
|
492
927
|
return c.html(html.replace('</body>', `${RELOAD_SCRIPT}\n</body>`));
|
|
493
928
|
}
|
|
494
929
|
catch (err) {
|
|
@@ -496,17 +931,17 @@ async function runDev(siteDir, port) {
|
|
|
496
931
|
}
|
|
497
932
|
});
|
|
498
933
|
}
|
|
499
|
-
// ---- Detect mode: dev (monorepo with apps/admin
|
|
934
|
+
// ---- Detect mode: dev (monorepo with apps/admin source) vs production (pre-built) ----
|
|
500
935
|
const cmsWebDir = findCmsDir();
|
|
501
936
|
const cmsStaticDir = findCmsStaticDir();
|
|
502
937
|
const isDevMode = cmsWebDir !== null;
|
|
503
938
|
if (isDevMode) {
|
|
504
|
-
// Dev mode:
|
|
505
|
-
await
|
|
939
|
+
// Dev mode: mount CMS API inline (same process = shared template cache)
|
|
940
|
+
await setupCmsApi(app, siteDir, storage, templatesDir, adminDir);
|
|
506
941
|
}
|
|
507
942
|
else if (cmsStaticDir) {
|
|
508
943
|
// Production mode: inline CMS API + static files
|
|
509
|
-
await setupProductionMode(app, siteDir, storage, cmsStaticDir);
|
|
944
|
+
await setupProductionMode(app, siteDir, storage, cmsStaticDir, templatesDir, adminDir);
|
|
510
945
|
}
|
|
511
946
|
// ---- 404 ----
|
|
512
947
|
app.notFound((c) => {
|
|
@@ -514,36 +949,38 @@ async function runDev(siteDir, port) {
|
|
|
514
949
|
return c.html(`<pre style="padding:2rem">Page not found: ${c.req.path}\n\nAvailable:\n${routes}\n /admin → CMS editor</pre>`, 404);
|
|
515
950
|
});
|
|
516
951
|
// ---- Start server ----
|
|
517
|
-
|
|
518
|
-
if (isDevMode) {
|
|
519
|
-
const apiPort = port + 100;
|
|
520
|
-
apiProc = spawn('npx', ['tsx', join(cmsWebDir, 'src/server/dev.ts'), siteDir], {
|
|
521
|
-
env: { ...process.env, API_PORT: String(apiPort) },
|
|
522
|
-
stdio: 'pipe',
|
|
523
|
-
});
|
|
524
|
-
apiProc.stderr?.on('data', (d) => {
|
|
525
|
-
const msg = d.toString().trim();
|
|
526
|
-
if (msg)
|
|
527
|
-
console.error(` [admin-api] ${msg}`);
|
|
528
|
-
});
|
|
529
|
-
}
|
|
952
|
+
const startTime = performance.now();
|
|
530
953
|
const nodeServer = serve({ fetch: app.fetch, port }, async () => {
|
|
531
|
-
|
|
532
|
-
console.log(
|
|
533
|
-
console.log(`
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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)')}`);
|
|
537
966
|
if (isDevMode && cmsWebDir) {
|
|
538
967
|
try {
|
|
539
968
|
const { createServer: createViteServer } = await import('vite');
|
|
969
|
+
const { searchForWorkspaceRoot } = await import('vite');
|
|
540
970
|
const vite = await createViteServer({
|
|
541
971
|
configFile: join(cmsWebDir, 'vite.config.ts'),
|
|
542
972
|
root: cmsWebDir,
|
|
543
973
|
base: '/admin/',
|
|
974
|
+
resolve: {
|
|
975
|
+
alias: {
|
|
976
|
+
'@editors': join(adminDir, 'editors'),
|
|
977
|
+
'@fields': join(adminDir, 'fields'),
|
|
978
|
+
},
|
|
979
|
+
},
|
|
544
980
|
server: {
|
|
545
981
|
middlewareMode: true,
|
|
546
982
|
hmr: { server: nodeServer },
|
|
983
|
+
fs: { allow: [searchForWorkspaceRoot(cmsWebDir), siteDir] },
|
|
547
984
|
},
|
|
548
985
|
});
|
|
549
986
|
const httpServer = nodeServer;
|
|
@@ -566,69 +1003,53 @@ async function runDev(siteDir, port) {
|
|
|
566
1003
|
honoHandler(req, res);
|
|
567
1004
|
}
|
|
568
1005
|
});
|
|
569
|
-
console.log(` CMS: http://localhost:${port}/admin (dev mode + HMR)`);
|
|
570
1006
|
}
|
|
571
1007
|
catch (err) {
|
|
572
1008
|
console.warn(` Warning: CMS UI failed to start: ${err.message}`);
|
|
573
1009
|
}
|
|
574
1010
|
}
|
|
575
|
-
else if (cmsStaticDir) {
|
|
576
|
-
console.log(` CMS: http://localhost:${port}/admin`);
|
|
577
|
-
}
|
|
578
1011
|
console.log();
|
|
579
1012
|
});
|
|
580
|
-
// ---- Cleanup ----
|
|
581
|
-
process.on('SIGINT', () => {
|
|
582
|
-
apiProc?.kill();
|
|
583
|
-
process.exit(0);
|
|
584
|
-
});
|
|
585
1013
|
// ---- File watching ----
|
|
1014
|
+
// Watch site dir for content changes (yaml manifests)
|
|
586
1015
|
watch(siteDir, { recursive: true }, (_event, filename) => {
|
|
587
1016
|
if (!filename)
|
|
588
1017
|
return;
|
|
589
|
-
if (filename.endsWith('.
|
|
590
|
-
const parts = filename.split('/');
|
|
591
|
-
const idx = parts.indexOf('templates');
|
|
592
|
-
if (idx >= 0 && idx + 1 < parts.length) {
|
|
593
|
-
console.log(` Template changed: ${parts[idx + 1]}`);
|
|
594
|
-
invalidateTemplate(parts[idx + 1]);
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
else if (filename.endsWith('.yaml')) {
|
|
1018
|
+
if (filename.endsWith('.yaml')) {
|
|
598
1019
|
console.log(` Manifest changed: ${filename}`);
|
|
599
1020
|
invalidateAllTemplates();
|
|
1021
|
+
notifyReload();
|
|
600
1022
|
}
|
|
601
|
-
else
|
|
602
|
-
return;
|
|
603
|
-
notifyReload();
|
|
604
1023
|
});
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
headers: c.req.raw.headers,
|
|
618
|
-
body: c.req.method !== 'GET' && c.req.method !== 'HEAD' ? c.req.raw.body : undefined,
|
|
619
|
-
// @ts-expect-error duplex needed for streaming body
|
|
620
|
-
duplex: 'half',
|
|
621
|
-
});
|
|
622
|
-
return new Response(res.body, { status: res.status, headers: res.headers });
|
|
623
|
-
}
|
|
624
|
-
catch {
|
|
625
|
-
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
|
+
}
|
|
626
1036
|
}
|
|
627
1037
|
});
|
|
628
1038
|
}
|
|
629
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
|
+
}
|
|
630
1051
|
// ---- Production mode: inline CMS API + static files from admin-dist/ ----
|
|
631
|
-
async function setupProductionMode(app, siteDir, storage, cmsStaticDir) {
|
|
1052
|
+
async function setupProductionMode(app, siteDir, storage, cmsStaticDir, templatesDir, adminDir) {
|
|
632
1053
|
// Read target configs from site.yaml — targets are initialized lazily on first publish/fetch
|
|
633
1054
|
const siteYamlPath = join(siteDir, 'site.yaml');
|
|
634
1055
|
let targetConfigs;
|
|
@@ -636,10 +1057,10 @@ async function setupProductionMode(app, siteDir, storage, cmsStaticDir) {
|
|
|
636
1057
|
const siteYaml = yaml.load(readFileSync(siteYamlPath, 'utf-8'));
|
|
637
1058
|
targetConfigs = siteYaml.targets;
|
|
638
1059
|
}
|
|
639
|
-
// Mount CMS API inline at /admin
|
|
640
|
-
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 });
|
|
641
1062
|
app.route('/admin', cmsApp);
|
|
642
|
-
// Serve pre-built CMS static files
|
|
1063
|
+
// Serve pre-built CMS static files (includes bundled editors/fields)
|
|
643
1064
|
app.use('/admin/*', serveStatic({
|
|
644
1065
|
root: cmsStaticDir,
|
|
645
1066
|
rewriteRequestPath: (path) => path.replace(/^\/admin/, ''),
|
|
@@ -660,12 +1081,12 @@ async function setupProductionMode(app, siteDir, storage, cmsStaticDir) {
|
|
|
660
1081
|
return c.text('CMS admin UI not found', 404);
|
|
661
1082
|
});
|
|
662
1083
|
}
|
|
663
|
-
/** Find apps/admin
|
|
1084
|
+
/** Find apps/admin source dir (monorepo dev mode) */
|
|
664
1085
|
function findCmsDir() {
|
|
665
1086
|
const candidates = [
|
|
666
|
-
resolve('apps/admin
|
|
667
|
-
resolve(import.meta.dirname, '../../../../apps/admin
|
|
668
|
-
resolve(import.meta.dirname, '../../../apps/admin
|
|
1087
|
+
resolve('apps/admin'),
|
|
1088
|
+
resolve(import.meta.dirname, '../../../../apps/admin'),
|
|
1089
|
+
resolve(import.meta.dirname, '../../../apps/admin'),
|
|
669
1090
|
];
|
|
670
1091
|
for (const dir of candidates) {
|
|
671
1092
|
if (existsSync(join(dir, 'src/server/dev.ts')))
|
|
@@ -691,26 +1112,80 @@ async function main() {
|
|
|
691
1112
|
process.exit(0);
|
|
692
1113
|
}
|
|
693
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
|
+
}
|
|
694
1170
|
switch (command) {
|
|
695
|
-
case 'init':
|
|
696
|
-
await runInit(args[1] ?? '.');
|
|
697
|
-
break;
|
|
698
1171
|
case 'publish':
|
|
699
|
-
await runPublish(
|
|
1172
|
+
await runPublish(siteDir, targetName);
|
|
1173
|
+
break;
|
|
1174
|
+
case 'serve':
|
|
1175
|
+
await runServe(siteDir, parsed.port ?? 3000, targetName);
|
|
700
1176
|
break;
|
|
701
1177
|
case 'deploy':
|
|
702
|
-
await runDeploy(
|
|
1178
|
+
await runDeploy(siteDir, targetName);
|
|
703
1179
|
break;
|
|
704
1180
|
case 'validate':
|
|
705
|
-
await runValidate(
|
|
1181
|
+
await runValidate(siteDir);
|
|
706
1182
|
break;
|
|
707
1183
|
case 'dev':
|
|
708
|
-
await runDev(
|
|
1184
|
+
await runDev(siteDir, parsed.port ?? 3000);
|
|
1185
|
+
break;
|
|
1186
|
+
case 'admin':
|
|
1187
|
+
await runAdmin(siteDir, parsed.port ?? 3000);
|
|
709
1188
|
break;
|
|
710
|
-
default:
|
|
711
|
-
console.error(` Unknown command: ${command}\n`);
|
|
712
|
-
printHelp();
|
|
713
|
-
process.exit(1);
|
|
714
1189
|
}
|
|
715
1190
|
}
|
|
716
1191
|
main().catch((err) => {
|