@zenithbuild/core 0.3.3 → 0.4.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/bin/zen-build.ts +0 -0
- package/bin/zen-dev.ts +0 -0
- package/bin/zen-preview.ts +0 -0
- package/cli/commands/dev.ts +156 -91
- package/cli/utils/branding.ts +25 -0
- package/cli/utils/content.ts +112 -0
- package/cli/utils/logger.ts +22 -16
- package/compiler/parse/parseTemplate.ts +120 -49
- package/compiler/parse/scriptAnalysis.ts +6 -0
- package/compiler/runtime/dataExposure.ts +12 -4
- package/compiler/runtime/generateHydrationBundle.ts +20 -15
- package/compiler/runtime/transformIR.ts +4 -8
- package/compiler/runtime/wrapExpression.ts +23 -12
- package/compiler/runtime/wrapExpressionWithLoop.ts +8 -2
- package/compiler/ssg-build.ts +7 -3
- package/compiler/transform/expressionTransformer.ts +385 -0
- package/compiler/transform/transformNode.ts +1 -1
- package/core/config/index.ts +16 -0
- package/core/config/loader.ts +69 -0
- package/core/config/types.ts +89 -0
- package/core/plugins/index.ts +7 -0
- package/core/plugins/registry.ts +81 -0
- package/dist/cli.js +1 -0
- package/dist/zen-build.js +568 -292
- package/dist/zen-dev.js +568 -292
- package/dist/zen-preview.js +568 -292
- package/dist/zenith.js +568 -292
- package/package.json +4 -1
- package/runtime/bundle-generator.ts +298 -37
- package/runtime/client-runtime.ts +17 -0
package/bin/zen-build.ts
CHANGED
|
File without changes
|
package/bin/zen-dev.ts
CHANGED
|
File without changes
|
package/bin/zen-preview.ts
CHANGED
|
File without changes
|
package/cli/commands/dev.ts
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @zenith/cli - Dev Command
|
|
3
|
-
*
|
|
4
|
-
* Starts the development server with in-memory compilation and hot reload
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
1
|
import path from 'path'
|
|
8
2
|
import fs from 'fs'
|
|
9
|
-
import { serve } from 'bun'
|
|
3
|
+
import { serve, type ServerWebSocket } from 'bun'
|
|
10
4
|
import { requireProject } from '../utils/project'
|
|
11
5
|
import * as logger from '../utils/logger'
|
|
6
|
+
import * as brand from '../utils/branding'
|
|
12
7
|
import { compileZenSource } from '../../compiler/index'
|
|
13
8
|
import { discoverLayouts } from '../../compiler/discovery/layouts'
|
|
14
9
|
import { processLayout } from '../../compiler/transform/layoutProcessor'
|
|
15
10
|
import { generateRouteDefinition } from '../../router/manifest'
|
|
16
11
|
import { generateBundleJS } from '../../runtime/bundle-generator'
|
|
12
|
+
import { loadContent } from '../utils/content'
|
|
13
|
+
import { loadZenithConfig } from '../../core/config/loader'
|
|
14
|
+
import { PluginRegistry, createPluginContext } from '../../core/plugins/registry'
|
|
15
|
+
import type { ContentItem } from '../../core/config/types'
|
|
17
16
|
|
|
18
17
|
export interface DevOptions {
|
|
19
18
|
port?: number
|
|
@@ -32,14 +31,52 @@ const pageCache = new Map<string, CompiledPage>()
|
|
|
32
31
|
export async function dev(options: DevOptions = {}): Promise<void> {
|
|
33
32
|
const project = requireProject()
|
|
34
33
|
const port = options.port || parseInt(process.env.PORT || '3000', 10)
|
|
35
|
-
|
|
36
|
-
// Support both app/ and src/ directory structures
|
|
37
|
-
const appDir = project.root
|
|
38
34
|
const pagesDir = project.pagesDir
|
|
35
|
+
const rootDir = project.root
|
|
36
|
+
const contentDir = path.join(rootDir, 'content')
|
|
37
|
+
|
|
38
|
+
// Load zenith.config.ts if present
|
|
39
|
+
const config = await loadZenithConfig(rootDir)
|
|
40
|
+
const registry = new PluginRegistry()
|
|
41
|
+
|
|
42
|
+
console.log('[Zenith] Config plugins:', config.plugins?.length ?? 0)
|
|
43
|
+
|
|
44
|
+
// Register plugins from config
|
|
45
|
+
for (const plugin of config.plugins || []) {
|
|
46
|
+
console.log('[Zenith] Registering plugin:', plugin.name)
|
|
47
|
+
registry.register(plugin)
|
|
48
|
+
}
|
|
39
49
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
50
|
+
// Initialize content data
|
|
51
|
+
let contentData: Record<string, ContentItem[]> = {}
|
|
52
|
+
|
|
53
|
+
// Initialize plugins with context
|
|
54
|
+
const hasContentPlugin = registry.has('zenith-content')
|
|
55
|
+
console.log('[Zenith] Has zenith-content plugin:', hasContentPlugin)
|
|
56
|
+
|
|
57
|
+
if (hasContentPlugin) {
|
|
58
|
+
await registry.initAll(createPluginContext(rootDir, (data) => {
|
|
59
|
+
console.log('[Zenith] Content plugin set data, collections:', Object.keys(data))
|
|
60
|
+
contentData = data
|
|
61
|
+
}))
|
|
62
|
+
} else {
|
|
63
|
+
// Fallback to legacy content loading if no content plugin configured
|
|
64
|
+
console.log('[Zenith] Using legacy content loading from:', contentDir)
|
|
65
|
+
contentData = loadContent(contentDir)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
console.log('[Zenith] Content collections loaded:', Object.keys(contentData))
|
|
69
|
+
|
|
70
|
+
const clients = new Set<ServerWebSocket<unknown>>()
|
|
71
|
+
|
|
72
|
+
// Branded Startup Panel
|
|
73
|
+
brand.showServerPanel({
|
|
74
|
+
project: project.root,
|
|
75
|
+
pages: project.pagesDir,
|
|
76
|
+
url: `http://localhost:${port}`,
|
|
77
|
+
hmr: true,
|
|
78
|
+
mode: 'In-memory compilation'
|
|
79
|
+
})
|
|
43
80
|
|
|
44
81
|
// File extensions that should be served as static assets
|
|
45
82
|
const STATIC_EXTENSIONS = new Set([
|
|
@@ -47,13 +84,6 @@ export async function dev(options: DevOptions = {}): Promise<void> {
|
|
|
47
84
|
'.webp', '.woff', '.woff2', '.ttf', '.eot', '.json', '.map'
|
|
48
85
|
])
|
|
49
86
|
|
|
50
|
-
/**
|
|
51
|
-
* Generate the shared runtime JavaScript
|
|
52
|
-
*/
|
|
53
|
-
function generateRuntimeJS(): string {
|
|
54
|
-
return generateBundleJS()
|
|
55
|
-
}
|
|
56
|
-
|
|
57
87
|
/**
|
|
58
88
|
* Compile a .zen page in memory
|
|
59
89
|
*/
|
|
@@ -61,22 +91,15 @@ export async function dev(options: DevOptions = {}): Promise<void> {
|
|
|
61
91
|
try {
|
|
62
92
|
const layoutsDir = path.join(pagesDir, '../layouts')
|
|
63
93
|
const layouts = discoverLayouts(layoutsDir)
|
|
64
|
-
|
|
65
94
|
const source = fs.readFileSync(pagePath, 'utf-8')
|
|
66
95
|
|
|
67
|
-
// Find suitable layout
|
|
68
96
|
let processedSource = source
|
|
69
97
|
let layoutToUse = layouts.get('DefaultLayout')
|
|
70
98
|
|
|
71
|
-
if (layoutToUse)
|
|
72
|
-
processedSource = processLayout(source, layoutToUse)
|
|
73
|
-
}
|
|
99
|
+
if (layoutToUse) processedSource = processLayout(source, layoutToUse)
|
|
74
100
|
|
|
75
101
|
const result = compileZenSource(processedSource, pagePath)
|
|
76
|
-
|
|
77
|
-
if (!result.finalized) {
|
|
78
|
-
throw new Error('Compilation failed: No finalized output')
|
|
79
|
-
}
|
|
102
|
+
if (!result.finalized) throw new Error('Compilation failed')
|
|
80
103
|
|
|
81
104
|
const routeDef = generateRouteDefinition(pagePath, pagesDir)
|
|
82
105
|
|
|
@@ -88,110 +111,152 @@ export async function dev(options: DevOptions = {}): Promise<void> {
|
|
|
88
111
|
lastModified: Date.now()
|
|
89
112
|
}
|
|
90
113
|
} catch (error: any) {
|
|
91
|
-
logger.error(`Compilation error
|
|
114
|
+
logger.error(`Compilation error: ${error.message}`)
|
|
92
115
|
return null
|
|
93
116
|
}
|
|
94
117
|
}
|
|
95
118
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
function generateDevHTML(page: CompiledPage): string {
|
|
100
|
-
const runtimeTag = `<script src="/runtime.js"></script>`
|
|
101
|
-
const scriptTag = `<script>\n${page.script}\n</script>`
|
|
102
|
-
const allScripts = `${runtimeTag}\n${scriptTag}`
|
|
119
|
+
// Set up file watching for HMR
|
|
120
|
+
const watcher = fs.watch(path.join(pagesDir, '..'), { recursive: true }, (event, filename) => {
|
|
121
|
+
if (!filename) return
|
|
103
122
|
|
|
104
|
-
if (
|
|
105
|
-
|
|
123
|
+
if (filename.endsWith('.zen')) {
|
|
124
|
+
logger.hmr('Page', filename)
|
|
125
|
+
// Broadcast reload
|
|
126
|
+
for (const client of clients) {
|
|
127
|
+
client.send(JSON.stringify({ type: 'reload' }))
|
|
128
|
+
}
|
|
129
|
+
} else if (filename.endsWith('.css')) {
|
|
130
|
+
logger.hmr('CSS', filename)
|
|
131
|
+
for (const client of clients) {
|
|
132
|
+
client.send(JSON.stringify({
|
|
133
|
+
type: 'style-update',
|
|
134
|
+
url: filename.includes('global.css') ? '/styles/global.css' : `/${filename}`
|
|
135
|
+
}))
|
|
136
|
+
}
|
|
137
|
+
} else if (filename.startsWith('content') || filename.includes('zenith-docs')) {
|
|
138
|
+
logger.hmr('Content', filename)
|
|
139
|
+
// Reinitialize content plugin to reload data
|
|
140
|
+
if (registry.has('zenith-content')) {
|
|
141
|
+
registry.initAll(createPluginContext(rootDir, (data) => {
|
|
142
|
+
contentData = data
|
|
143
|
+
}))
|
|
144
|
+
} else {
|
|
145
|
+
contentData = loadContent(contentDir)
|
|
146
|
+
}
|
|
147
|
+
for (const client of clients) {
|
|
148
|
+
client.send(JSON.stringify({ type: 'reload' }))
|
|
149
|
+
}
|
|
106
150
|
}
|
|
107
|
-
|
|
108
|
-
return `${page.html}\n${allScripts}`
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Find .zen page file for a given route
|
|
113
|
-
*/
|
|
114
|
-
function findPageForRoute(route: string): string | null {
|
|
115
|
-
const exactPath = path.join(pagesDir, route === '/' ? 'index.zen' : `${route.slice(1)}.zen`)
|
|
116
|
-
if (fs.existsSync(exactPath)) return exactPath
|
|
117
|
-
|
|
118
|
-
const indexPath = path.join(pagesDir, route === '/' ? 'index.zen' : `${route.slice(1)}/index.zen`)
|
|
119
|
-
if (fs.existsSync(indexPath)) return indexPath
|
|
120
|
-
|
|
121
|
-
return null
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const cachedRuntimeJS = generateRuntimeJS()
|
|
151
|
+
})
|
|
125
152
|
|
|
126
153
|
const server = serve({
|
|
127
154
|
port,
|
|
128
|
-
|
|
155
|
+
fetch(req, server) {
|
|
156
|
+
const startTime = performance.now()
|
|
129
157
|
const url = new URL(req.url)
|
|
130
158
|
const pathname = url.pathname
|
|
131
159
|
const ext = path.extname(pathname).toLowerCase()
|
|
132
160
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
161
|
+
// Upgrade to WebSocket for HMR
|
|
162
|
+
if (pathname === '/hmr') {
|
|
163
|
+
const upgraded = server.upgrade(req)
|
|
164
|
+
if (upgraded) return undefined
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Handle Zenith assets
|
|
168
|
+
if (pathname === '/runtime.js') {
|
|
169
|
+
const response = new Response(generateBundleJS(), {
|
|
170
|
+
headers: { 'Content-Type': 'application/javascript; charset=utf-8' }
|
|
139
171
|
})
|
|
172
|
+
logger.route('GET', pathname, 200, Math.round(performance.now() - startTime), 0, Math.round(performance.now() - startTime))
|
|
173
|
+
return response
|
|
140
174
|
}
|
|
141
175
|
|
|
142
|
-
if (pathname === '/
|
|
176
|
+
if (pathname === '/styles/global.css') {
|
|
143
177
|
const globalCssPath = path.join(pagesDir, '../styles/global.css')
|
|
144
178
|
if (fs.existsSync(globalCssPath)) {
|
|
145
179
|
const css = fs.readFileSync(globalCssPath, 'utf-8')
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
180
|
+
const response = new Response(css, { headers: { 'Content-Type': 'text/css' } })
|
|
181
|
+
logger.route('GET', pathname, 200, Math.round(performance.now() - startTime), 0, Math.round(performance.now() - startTime))
|
|
182
|
+
return response
|
|
149
183
|
}
|
|
150
184
|
}
|
|
151
185
|
|
|
186
|
+
// Static files
|
|
152
187
|
if (STATIC_EXTENSIONS.has(ext)) {
|
|
153
188
|
const publicPath = path.join(pagesDir, '../public', pathname)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const file = Bun.file(filePath)
|
|
159
|
-
if (await file.exists()) {
|
|
160
|
-
return new Response(file)
|
|
161
|
-
}
|
|
189
|
+
if (fs.existsSync(publicPath)) {
|
|
190
|
+
const response = new Response(Bun.file(publicPath))
|
|
191
|
+
logger.route('GET', pathname, 200, Math.round(performance.now() - startTime), 0, Math.round(performance.now() - startTime))
|
|
192
|
+
return response
|
|
162
193
|
}
|
|
163
|
-
return new Response('Not found', { status: 404 })
|
|
164
194
|
}
|
|
165
195
|
|
|
166
|
-
|
|
196
|
+
// Zenith Pages
|
|
197
|
+
const pagePath = findPageForRoute(pathname, pagesDir)
|
|
167
198
|
if (pagePath) {
|
|
199
|
+
const compileStart = performance.now()
|
|
168
200
|
let cached = pageCache.get(pagePath)
|
|
169
201
|
const stat = fs.statSync(pagePath)
|
|
170
202
|
|
|
171
203
|
if (!cached || stat.mtimeMs > cached.lastModified) {
|
|
172
|
-
|
|
173
|
-
if (
|
|
174
|
-
pageCache.set(pagePath, compiled)
|
|
175
|
-
cached = compiled
|
|
176
|
-
}
|
|
204
|
+
cached = compilePageInMemory(pagePath) || undefined
|
|
205
|
+
if (cached) pageCache.set(pagePath, cached)
|
|
177
206
|
}
|
|
207
|
+
const compileEnd = performance.now()
|
|
178
208
|
|
|
179
209
|
if (cached) {
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
210
|
+
const renderStart = performance.now()
|
|
211
|
+
const html = generateDevHTML(cached, contentData)
|
|
212
|
+
const renderEnd = performance.now()
|
|
213
|
+
|
|
214
|
+
const totalTime = Math.round(performance.now() - startTime)
|
|
215
|
+
const compileTime = Math.round(compileEnd - compileStart)
|
|
216
|
+
const renderTime = Math.round(renderEnd - renderStart)
|
|
217
|
+
|
|
218
|
+
logger.route('GET', pathname, 200, totalTime, compileTime, renderTime)
|
|
219
|
+
return new Response(html, { headers: { 'Content-Type': 'text/html' } })
|
|
184
220
|
}
|
|
185
221
|
}
|
|
186
222
|
|
|
223
|
+
logger.route('GET', pathname, 404, Math.round(performance.now() - startTime), 0, 0)
|
|
187
224
|
return new Response('Not Found', { status: 404 })
|
|
225
|
+
},
|
|
226
|
+
websocket: {
|
|
227
|
+
open(ws) {
|
|
228
|
+
clients.add(ws)
|
|
229
|
+
},
|
|
230
|
+
close(ws) {
|
|
231
|
+
clients.delete(ws)
|
|
232
|
+
},
|
|
233
|
+
message() { }
|
|
188
234
|
}
|
|
189
235
|
})
|
|
190
236
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
237
|
+
process.on('SIGINT', () => {
|
|
238
|
+
watcher.close()
|
|
239
|
+
server.stop()
|
|
240
|
+
process.exit(0)
|
|
241
|
+
})
|
|
195
242
|
|
|
196
243
|
await new Promise(() => { })
|
|
197
244
|
}
|
|
245
|
+
|
|
246
|
+
function findPageForRoute(route: string, pagesDir: string): string | null {
|
|
247
|
+
const exactPath = path.join(pagesDir, route === '/' ? 'index.zen' : `${route.slice(1)}.zen`)
|
|
248
|
+
if (fs.existsSync(exactPath)) return exactPath
|
|
249
|
+
const indexPath = path.join(pagesDir, route === '/' ? 'index.zen' : `${route.slice(1)}/index.zen`)
|
|
250
|
+
if (fs.existsSync(indexPath)) return indexPath
|
|
251
|
+
return null
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function generateDevHTML(page: CompiledPage, contentData: any = {}): string {
|
|
255
|
+
const runtimeTag = `<script src="/runtime.js"></script>`
|
|
256
|
+
const contentTag = `<script>window.__ZENITH_CONTENT__ = ${JSON.stringify(contentData)};</script>`
|
|
257
|
+
const scriptTag = `<script>\n${page.script}\n</script>`
|
|
258
|
+
const allScripts = `${runtimeTag}\n${contentTag}\n${scriptTag}`
|
|
259
|
+
return page.html.includes('</body>')
|
|
260
|
+
? page.html.replace('</body>', `${allScripts}\n</body>`)
|
|
261
|
+
: `${page.html}\n${allScripts}`
|
|
262
|
+
}
|
package/cli/utils/branding.ts
CHANGED
|
@@ -151,3 +151,28 @@ ${pc.cyan('│')} ${pc.c
|
|
|
151
151
|
${pc.cyan('└─────────────────────────────────────────────────────────┘')}
|
|
152
152
|
`)
|
|
153
153
|
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Show dev server startup panel
|
|
157
|
+
*/
|
|
158
|
+
export function showServerPanel(options: {
|
|
159
|
+
project: string,
|
|
160
|
+
pages: string,
|
|
161
|
+
url: string,
|
|
162
|
+
hmr: boolean,
|
|
163
|
+
mode: string
|
|
164
|
+
}) {
|
|
165
|
+
console.clear()
|
|
166
|
+
console.log(LOGO_COMPACT)
|
|
167
|
+
console.log(`${pc.cyan('────────────────────────────────────────────────────────────')}`)
|
|
168
|
+
console.log(` ${pc.magenta('🟣 Zenith Dev Server')}`)
|
|
169
|
+
console.log(`${pc.cyan('────────────────────────────────────────────────────────────')}`)
|
|
170
|
+
console.log(` ${pc.bold('Project:')} ${pc.dim(options.project)}`)
|
|
171
|
+
console.log(` ${pc.bold('Pages:')} ${pc.dim(options.pages)}`)
|
|
172
|
+
console.log(` ${pc.bold('Mode:')} ${pc.cyan(options.mode)} ${pc.dim(`(${options.hmr ? 'HMR enabled' : 'HMR disabled'})`)}`)
|
|
173
|
+
console.log(`${pc.cyan('────────────────────────────────────────────────────────────')}`)
|
|
174
|
+
console.log(` ${pc.bold('Server:')} ${pc.cyan(pc.underline(options.url))} ${pc.dim('(clickable)')}`)
|
|
175
|
+
console.log(` ${pc.bold('Hot Reload:')} ${options.hmr ? pc.green('Enabled ✅') : pc.red('Disabled ✗')}`)
|
|
176
|
+
console.log(`${pc.cyan('────────────────────────────────────────────────────────────')}`)
|
|
177
|
+
console.log(` ${pc.dim('Press Ctrl+C to stop')}\n`)
|
|
178
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { marked } from 'marked';
|
|
4
|
+
|
|
5
|
+
export interface ContentItem {
|
|
6
|
+
id?: string | number;
|
|
7
|
+
slug?: string | null;
|
|
8
|
+
collection?: string | null;
|
|
9
|
+
content?: string | null;
|
|
10
|
+
[key: string]: any | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Load all content from the content directory
|
|
15
|
+
*/
|
|
16
|
+
export function loadContent(contentDir: string): Record<string, ContentItem[]> {
|
|
17
|
+
if (!fs.existsSync(contentDir)) {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const collections: Record<string, ContentItem[]> = {};
|
|
22
|
+
const files = getAllFiles(contentDir);
|
|
23
|
+
|
|
24
|
+
for (const filePath of files) {
|
|
25
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
26
|
+
const relativePath = path.relative(contentDir, filePath);
|
|
27
|
+
const collection = relativePath.split(path.sep)[0];
|
|
28
|
+
if (!collection) continue;
|
|
29
|
+
|
|
30
|
+
const slug = relativePath.replace(/\.(md|mdx|json)$/, '').replace(/\\/g, '/');
|
|
31
|
+
const id = slug;
|
|
32
|
+
|
|
33
|
+
const rawContent = fs.readFileSync(filePath, 'utf-8');
|
|
34
|
+
|
|
35
|
+
if (!collections[collection]) {
|
|
36
|
+
collections[collection] = [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (ext === '.json') {
|
|
40
|
+
try {
|
|
41
|
+
const data = JSON.parse(rawContent);
|
|
42
|
+
collections[collection].push({
|
|
43
|
+
id,
|
|
44
|
+
slug,
|
|
45
|
+
collection,
|
|
46
|
+
content: '',
|
|
47
|
+
...data
|
|
48
|
+
});
|
|
49
|
+
} catch (e) {
|
|
50
|
+
console.error(`Error parsing JSON file ${filePath}:`, e);
|
|
51
|
+
}
|
|
52
|
+
} else if (ext === '.md' || ext === '.mdx') {
|
|
53
|
+
const { metadata, content } = parseMarkdown(rawContent);
|
|
54
|
+
collections[collection].push({
|
|
55
|
+
id,
|
|
56
|
+
slug,
|
|
57
|
+
collection,
|
|
58
|
+
content,
|
|
59
|
+
...metadata
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return collections;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getAllFiles(dir: string, fileList: string[] = []): string[] {
|
|
68
|
+
const files = fs.readdirSync(dir);
|
|
69
|
+
files.forEach((file: string) => {
|
|
70
|
+
const name = path.join(dir, file);
|
|
71
|
+
if (fs.statSync(name).isDirectory()) {
|
|
72
|
+
getAllFiles(name, fileList);
|
|
73
|
+
} else {
|
|
74
|
+
fileList.push(name);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
return fileList;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parseMarkdown(content: string): { metadata: Record<string, any>, content: string } {
|
|
81
|
+
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
82
|
+
const match = content.match(frontmatterRegex);
|
|
83
|
+
|
|
84
|
+
if (!match) {
|
|
85
|
+
return { metadata: {}, content: content.trim() };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const [, yamlStr, body] = match;
|
|
89
|
+
const metadata: Record<string, any> = {};
|
|
90
|
+
|
|
91
|
+
if (yamlStr) {
|
|
92
|
+
yamlStr.split('\n').forEach(line => {
|
|
93
|
+
const [key, ...values] = line.split(':');
|
|
94
|
+
if (key && values.length > 0) {
|
|
95
|
+
const value = values.join(':').trim();
|
|
96
|
+
// Basic type conversion
|
|
97
|
+
if (value === 'true') metadata[key.trim()] = true;
|
|
98
|
+
else if (value === 'false') metadata[key.trim()] = false;
|
|
99
|
+
else if (!isNaN(Number(value))) metadata[key.trim()] = Number(value);
|
|
100
|
+
else if (value.startsWith('[') && value.endsWith(']')) {
|
|
101
|
+
metadata[key.trim()] = value.slice(1, -1).split(',').map(v => v.trim().replace(/^['"]|['"]$/g, ''));
|
|
102
|
+
}
|
|
103
|
+
else metadata[key.trim()] = value.replace(/^['"]|['"]$/g, '');
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
metadata,
|
|
110
|
+
content: marked.parse((body || '').trim()) as string
|
|
111
|
+
};
|
|
112
|
+
}
|
package/cli/utils/logger.ts
CHANGED
|
@@ -4,37 +4,43 @@
|
|
|
4
4
|
* Colored console output for CLI feedback
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
reset: '\x1b[0m',
|
|
9
|
-
bright: '\x1b[1m',
|
|
10
|
-
dim: '\x1b[2m',
|
|
11
|
-
red: '\x1b[31m',
|
|
12
|
-
green: '\x1b[32m',
|
|
13
|
-
yellow: '\x1b[33m',
|
|
14
|
-
blue: '\x1b[34m',
|
|
15
|
-
cyan: '\x1b[36m'
|
|
16
|
-
}
|
|
7
|
+
import pc from 'picocolors'
|
|
17
8
|
|
|
18
9
|
export function log(message: string): void {
|
|
19
|
-
console.log(`${
|
|
10
|
+
console.log(`${pc.cyan('[zenith]')} ${message}`)
|
|
20
11
|
}
|
|
21
12
|
|
|
22
13
|
export function success(message: string): void {
|
|
23
|
-
console.log(`${
|
|
14
|
+
console.log(`${pc.green('✓')} ${message}`)
|
|
24
15
|
}
|
|
25
16
|
|
|
26
17
|
export function warn(message: string): void {
|
|
27
|
-
console.log(`${
|
|
18
|
+
console.log(`${pc.yellow('⚠')} ${message}`)
|
|
28
19
|
}
|
|
29
20
|
|
|
30
21
|
export function error(message: string): void {
|
|
31
|
-
console.error(`${
|
|
22
|
+
console.error(`${pc.red('✗')} ${message}`)
|
|
32
23
|
}
|
|
33
24
|
|
|
34
25
|
export function info(message: string): void {
|
|
35
|
-
console.log(`${
|
|
26
|
+
console.log(`${pc.blue('ℹ')} ${message}`)
|
|
36
27
|
}
|
|
37
28
|
|
|
38
29
|
export function header(title: string): void {
|
|
39
|
-
console.log(`\n${
|
|
30
|
+
console.log(`\n${pc.bold(pc.cyan(title))}\n`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function hmr(type: 'CSS' | 'Page' | 'Layout' | 'Content', path: string): void {
|
|
34
|
+
console.log(`${pc.magenta('[HMR]')} ${pc.bold(type)} updated: ${pc.dim(path)}`)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function route(method: string, path: string, status: number, totalMs: number, compileMs: number, renderMs: number): void {
|
|
38
|
+
const statusColor = status < 400 ? pc.green : pc.red
|
|
39
|
+
const timeColor = totalMs > 1000 ? pc.yellow : pc.gray
|
|
40
|
+
|
|
41
|
+
console.log(
|
|
42
|
+
`${pc.bold(method)} ${pc.cyan(path.padEnd(15))} ` +
|
|
43
|
+
`${statusColor(status)} ${pc.dim('in')} ${timeColor(`${totalMs}ms`)} ` +
|
|
44
|
+
`${pc.dim(`(compile: ${compileMs}ms, render: ${renderMs}ms)`)}`
|
|
45
|
+
)
|
|
40
46
|
}
|