@zenithbuild/core 0.3.1 → 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 CHANGED
File without changes
package/bin/zen-dev.ts CHANGED
File without changes
File without changes
@@ -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
- logger.header('Zenith Dev Server')
41
- logger.log(`Project: ${project.root}`)
42
- logger.log(`Pages: ${project.pagesDir}`)
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 for ${pagePath}: ${error.message}`)
114
+ logger.error(`Compilation error: ${error.message}`)
92
115
  return null
93
116
  }
94
117
  }
95
118
 
96
- /**
97
- * Generate full HTML page from compiled output
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 (page.html.includes('</body>')) {
105
- return page.html.replace('</body>', `${allScripts}\n</body>`)
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
- async fetch(req) {
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
- if (pathname === '/runtime.js' || pathname === '/assets/bundle.js') {
134
- return new Response(cachedRuntimeJS, {
135
- headers: {
136
- 'Content-Type': 'application/javascript; charset=utf-8',
137
- 'Cache-Control': 'no-cache'
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 === '/assets/styles.css' || pathname === '/styles/global.css' || pathname === '/app/styles/global.css') {
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
- return new Response(css, {
147
- headers: { 'Content-Type': 'text/css; charset=utf-8' }
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
- const distPath = path.join(pagesDir, '../dist', pathname)
155
- const appRelativePath = path.join(pagesDir, '..', pathname)
156
-
157
- for (const filePath of [publicPath, distPath, appRelativePath]) {
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
- const pagePath = findPageForRoute(pathname)
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
- const compiled = compilePageInMemory(pagePath)
173
- if (compiled) {
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 html = generateDevHTML(cached)
181
- return new Response(html, {
182
- headers: { 'Content-Type': 'text/html; charset=utf-8' }
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
- logger.success(`Server running at http://localhost:${server.port}`)
192
- logger.info('• In-memory compilation active')
193
- logger.info('• Auto-recompile on file changes')
194
- logger.info('Press Ctrl+C to stop')
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
+ }
@@ -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
+ }
@@ -4,37 +4,43 @@
4
4
  * Colored console output for CLI feedback
5
5
  */
6
6
 
7
- const colors = {
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(`${colors.cyan}[zenith]${colors.reset} ${message}`)
10
+ console.log(`${pc.cyan('[zenith]')} ${message}`)
20
11
  }
21
12
 
22
13
  export function success(message: string): void {
23
- console.log(`${colors.green}✓${colors.reset} ${message}`)
14
+ console.log(`${pc.green('✓')} ${message}`)
24
15
  }
25
16
 
26
17
  export function warn(message: string): void {
27
- console.log(`${colors.yellow}⚠${colors.reset} ${message}`)
18
+ console.log(`${pc.yellow('⚠')} ${message}`)
28
19
  }
29
20
 
30
21
  export function error(message: string): void {
31
- console.error(`${colors.red}✗${colors.reset} ${message}`)
22
+ console.error(`${pc.red('✗')} ${message}`)
32
23
  }
33
24
 
34
25
  export function info(message: string): void {
35
- console.log(`${colors.blue}ℹ${colors.reset} ${message}`)
26
+ console.log(`${pc.blue('ℹ')} ${message}`)
36
27
  }
37
28
 
38
29
  export function header(title: string): void {
39
- console.log(`\n${colors.bright}${colors.cyan}${title}${colors.reset}\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
  }
@@ -28,8 +28,8 @@ export function findProjectRoot(startDir: string = process.cwd()): string | null
28
28
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
29
29
  const deps = { ...pkg.dependencies, ...pkg.devDependencies }
30
30
 
31
- // Check for any @zenith/* dependency
32
- const hasZenith = Object.keys(deps).some(d => d.startsWith('@zenith/'))
31
+ // Check for any @zenith/* or @zenithbuild/* dependency
32
+ const hasZenith = Object.keys(deps).some(d => d.startsWith('@zenith/') || d.startsWith('@zenithbuild/'))
33
33
  if (hasZenith) {
34
34
  return current
35
35
  }
@@ -51,10 +51,16 @@ export function getProject(cwd: string = process.cwd()): ZenithProject | null {
51
51
  const root = findProjectRoot(cwd)
52
52
  if (!root) return null
53
53
 
54
+ // Support both app/ and src/ directory structures
55
+ let appDir = path.join(root, 'app')
56
+ if (!fs.existsSync(appDir)) {
57
+ appDir = path.join(root, 'src')
58
+ }
59
+
54
60
  return {
55
61
  root,
56
- pagesDir: path.join(root, 'app/pages'),
57
- distDir: path.join(root, 'app/dist'),
62
+ pagesDir: path.join(appDir, 'pages'),
63
+ distDir: path.join(appDir, 'dist'),
58
64
  hasZenithDeps: true
59
65
  }
60
66
  }