@zenithbuild/core 0.4.2 → 0.4.5
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/cli/commands/dev.ts +59 -15
- package/compiler/css/index.ts +317 -0
- package/compiler/discovery/componentDiscovery.ts +174 -0
- package/compiler/errors/compilerError.ts +32 -0
- package/compiler/finalize/finalizeOutput.ts +37 -8
- package/compiler/index.ts +26 -5
- package/compiler/ir/types.ts +66 -0
- package/compiler/parse/parseTemplate.ts +66 -9
- package/compiler/runtime/generateDOM.ts +102 -1
- package/compiler/runtime/transformIR.ts +2 -2
- package/compiler/ssg-build.ts +17 -11
- package/compiler/transform/classifyExpression.ts +444 -0
- package/compiler/transform/componentResolver.ts +289 -0
- package/compiler/transform/fragmentLowering.ts +634 -0
- package/compiler/transform/slotResolver.ts +292 -0
- package/compiler/validate/invariants.ts +292 -0
- package/package.json +1 -1
package/cli/commands/dev.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { loadContent } from '../utils/content'
|
|
|
13
13
|
import { loadZenithConfig } from '../../core/config/loader'
|
|
14
14
|
import { PluginRegistry, createPluginContext } from '../../core/plugins/registry'
|
|
15
15
|
import type { ContentItem } from '../../core/config/types'
|
|
16
|
+
import { compileCssAsync, resolveGlobalsCss } from '../../compiler/css'
|
|
16
17
|
|
|
17
18
|
export interface DevOptions {
|
|
18
19
|
port?: number
|
|
@@ -67,6 +68,23 @@ export async function dev(options: DevOptions = {}): Promise<void> {
|
|
|
67
68
|
|
|
68
69
|
console.log('[Zenith] Content collections loaded:', Object.keys(contentData))
|
|
69
70
|
|
|
71
|
+
// ============================================
|
|
72
|
+
// CSS Compilation (Compiler-Owned)
|
|
73
|
+
// ============================================
|
|
74
|
+
const globalsCssPath = resolveGlobalsCss(rootDir)
|
|
75
|
+
let compiledCss = ''
|
|
76
|
+
|
|
77
|
+
if (globalsCssPath) {
|
|
78
|
+
console.log('[Zenith] Compiling CSS:', path.relative(rootDir, globalsCssPath))
|
|
79
|
+
const cssResult = await compileCssAsync({ input: globalsCssPath, output: ':memory:' })
|
|
80
|
+
if (cssResult.success) {
|
|
81
|
+
compiledCss = cssResult.css
|
|
82
|
+
console.log(`[Zenith] CSS compiled in ${cssResult.duration}ms`)
|
|
83
|
+
} else {
|
|
84
|
+
console.error('[Zenith] CSS compilation failed:', cssResult.error)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
70
88
|
const clients = new Set<ServerWebSocket<unknown>>()
|
|
71
89
|
|
|
72
90
|
// Branded Startup Panel
|
|
@@ -90,6 +108,7 @@ export async function dev(options: DevOptions = {}): Promise<void> {
|
|
|
90
108
|
function compilePageInMemory(pagePath: string): CompiledPage | null {
|
|
91
109
|
try {
|
|
92
110
|
const layoutsDir = path.join(pagesDir, '../layouts')
|
|
111
|
+
const componentsDir = path.join(pagesDir, '../components')
|
|
93
112
|
const layouts = discoverLayouts(layoutsDir)
|
|
94
113
|
const source = fs.readFileSync(pagePath, 'utf-8')
|
|
95
114
|
|
|
@@ -98,7 +117,9 @@ export async function dev(options: DevOptions = {}): Promise<void> {
|
|
|
98
117
|
|
|
99
118
|
if (layoutToUse) processedSource = processLayout(source, layoutToUse)
|
|
100
119
|
|
|
101
|
-
const result = compileZenSource(processedSource, pagePath
|
|
120
|
+
const result = compileZenSource(processedSource, pagePath, {
|
|
121
|
+
componentsDir: fs.existsSync(componentsDir) ? componentsDir : undefined
|
|
122
|
+
})
|
|
102
123
|
if (!result.finalized) throw new Error('Compilation failed')
|
|
103
124
|
|
|
104
125
|
const routeDef = generateRouteDefinition(pagePath, pagesDir)
|
|
@@ -117,22 +138,37 @@ export async function dev(options: DevOptions = {}): Promise<void> {
|
|
|
117
138
|
}
|
|
118
139
|
|
|
119
140
|
// Set up file watching for HMR
|
|
120
|
-
const watcher = fs.watch(path.join(pagesDir, '..'), { recursive: true }, (event, filename) => {
|
|
141
|
+
const watcher = fs.watch(path.join(pagesDir, '..'), { recursive: true }, async (event, filename) => {
|
|
121
142
|
if (!filename) return
|
|
122
143
|
|
|
123
144
|
if (filename.endsWith('.zen')) {
|
|
124
145
|
logger.hmr('Page', filename)
|
|
125
|
-
//
|
|
146
|
+
// Recompile CSS for new Tailwind classes in .zen files
|
|
147
|
+
if (globalsCssPath) {
|
|
148
|
+
const cssResult = await compileCssAsync({ input: globalsCssPath, output: ':memory:' })
|
|
149
|
+
if (cssResult.success) {
|
|
150
|
+
compiledCss = cssResult.css
|
|
151
|
+
// Notify clients of CSS update
|
|
152
|
+
for (const client of clients) {
|
|
153
|
+
client.send(JSON.stringify({ type: 'style-update', url: '/assets/styles.css' }))
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Broadcast page reload
|
|
126
158
|
for (const client of clients) {
|
|
127
159
|
client.send(JSON.stringify({ type: 'reload' }))
|
|
128
160
|
}
|
|
129
161
|
} else if (filename.endsWith('.css')) {
|
|
130
162
|
logger.hmr('CSS', filename)
|
|
163
|
+
// Recompile CSS
|
|
164
|
+
if (globalsCssPath) {
|
|
165
|
+
const cssResult = await compileCssAsync({ input: globalsCssPath, output: ':memory:' })
|
|
166
|
+
if (cssResult.success) {
|
|
167
|
+
compiledCss = cssResult.css
|
|
168
|
+
}
|
|
169
|
+
}
|
|
131
170
|
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
|
-
}))
|
|
171
|
+
client.send(JSON.stringify({ type: 'style-update', url: '/assets/styles.css' }))
|
|
136
172
|
}
|
|
137
173
|
} else if (filename.startsWith('content') || filename.includes('zenith-docs')) {
|
|
138
174
|
logger.hmr('Content', filename)
|
|
@@ -173,14 +209,22 @@ export async function dev(options: DevOptions = {}): Promise<void> {
|
|
|
173
209
|
return response
|
|
174
210
|
}
|
|
175
211
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
212
|
+
// Serve compiler-owned CSS (Tailwind compiled)
|
|
213
|
+
if (pathname === '/assets/styles.css') {
|
|
214
|
+
const response = new Response(compiledCss, {
|
|
215
|
+
headers: { 'Content-Type': 'text/css; charset=utf-8' }
|
|
216
|
+
})
|
|
217
|
+
logger.route('GET', pathname, 200, Math.round(performance.now() - startTime), 0, Math.round(performance.now() - startTime))
|
|
218
|
+
return response
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Legacy: also support /styles/globals.css or /styles/global.css for backwards compat
|
|
222
|
+
if (pathname === '/styles/globals.css' || pathname === '/styles/global.css') {
|
|
223
|
+
const response = new Response(compiledCss, {
|
|
224
|
+
headers: { 'Content-Type': 'text/css; charset=utf-8' }
|
|
225
|
+
})
|
|
226
|
+
logger.route('GET', pathname, 200, Math.round(performance.now() - startTime), 0, Math.round(performance.now() - startTime))
|
|
227
|
+
return response
|
|
184
228
|
}
|
|
185
229
|
|
|
186
230
|
// Static files
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zenith CSS Compiler Module
|
|
3
|
+
*
|
|
4
|
+
* Compiler-owned CSS processing that integrates Tailwind CSS v4 JIT
|
|
5
|
+
* at compile time. This module ensures:
|
|
6
|
+
*
|
|
7
|
+
* 1. All CSS is processed at build time (no runtime generation)
|
|
8
|
+
* 2. Tailwind sees all .zen templates for class scanning
|
|
9
|
+
* 3. HMR support for instant CSS updates in dev mode
|
|
10
|
+
* 4. Deterministic, cacheable output for production
|
|
11
|
+
*
|
|
12
|
+
* Per Zenith CSS Directive: The compiler owns styles.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { spawn, spawnSync } from 'child_process'
|
|
16
|
+
import path from 'path'
|
|
17
|
+
import fs from 'fs'
|
|
18
|
+
|
|
19
|
+
// ============================================
|
|
20
|
+
// Types
|
|
21
|
+
// ============================================
|
|
22
|
+
|
|
23
|
+
export interface CSSCompileOptions {
|
|
24
|
+
/** Input CSS file path (e.g., src/styles/globals.css) */
|
|
25
|
+
input: string
|
|
26
|
+
/** Output CSS file path, or ':memory:' for in-memory result */
|
|
27
|
+
output: string
|
|
28
|
+
/** Enable minification for production */
|
|
29
|
+
minify?: boolean
|
|
30
|
+
/** Watch mode for HMR */
|
|
31
|
+
watch?: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface CSSCompileResult {
|
|
35
|
+
/** Compiled CSS content */
|
|
36
|
+
css: string
|
|
37
|
+
/** Compilation time in milliseconds */
|
|
38
|
+
duration: number
|
|
39
|
+
/** Whether compilation succeeded */
|
|
40
|
+
success: boolean
|
|
41
|
+
/** Error message if failed */
|
|
42
|
+
error?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ============================================
|
|
46
|
+
// CSS Compilation
|
|
47
|
+
// ============================================
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Compile CSS using Tailwind CSS v4 CLI
|
|
51
|
+
*
|
|
52
|
+
* This function synchronously compiles CSS for use in:
|
|
53
|
+
* - Dev server startup
|
|
54
|
+
* - SSG build
|
|
55
|
+
* - On-demand recompilation
|
|
56
|
+
*
|
|
57
|
+
* @param options Compilation options
|
|
58
|
+
* @returns Compiled CSS result
|
|
59
|
+
*/
|
|
60
|
+
export function compileCss(options: CSSCompileOptions): CSSCompileResult {
|
|
61
|
+
const startTime = performance.now()
|
|
62
|
+
const { input, output, minify = false } = options
|
|
63
|
+
|
|
64
|
+
// Validate input exists
|
|
65
|
+
if (!fs.existsSync(input)) {
|
|
66
|
+
return {
|
|
67
|
+
css: '',
|
|
68
|
+
duration: 0,
|
|
69
|
+
success: false,
|
|
70
|
+
error: `CSS input file not found: ${input}`
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
// Build Tailwind CLI arguments
|
|
76
|
+
const args = [
|
|
77
|
+
'@tailwindcss/cli',
|
|
78
|
+
'-i', input
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
// For in-memory compilation, use stdout
|
|
82
|
+
const useStdout = output === ':memory:'
|
|
83
|
+
if (!useStdout) {
|
|
84
|
+
args.push('-o', output)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (minify) {
|
|
88
|
+
args.push('--minify')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Execute Tailwind CLI synchronously
|
|
92
|
+
const result = spawnSync('bunx', args, {
|
|
93
|
+
cwd: path.dirname(input),
|
|
94
|
+
encoding: 'utf-8',
|
|
95
|
+
stdio: useStdout ? ['pipe', 'pipe', 'pipe'] : ['pipe', 'inherit', 'pipe'],
|
|
96
|
+
env: { ...process.env }
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const duration = Math.round(performance.now() - startTime)
|
|
100
|
+
|
|
101
|
+
if (result.status !== 0) {
|
|
102
|
+
const errorMsg = result.stderr?.toString() || 'Unknown compilation error'
|
|
103
|
+
return {
|
|
104
|
+
css: '',
|
|
105
|
+
duration,
|
|
106
|
+
success: false,
|
|
107
|
+
error: `Tailwind compilation failed: ${errorMsg}`
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Get CSS content
|
|
112
|
+
let css = ''
|
|
113
|
+
if (useStdout) {
|
|
114
|
+
css = result.stdout?.toString() || ''
|
|
115
|
+
} else if (fs.existsSync(output)) {
|
|
116
|
+
css = fs.readFileSync(output, 'utf-8')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
css,
|
|
121
|
+
duration,
|
|
122
|
+
success: true
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
} catch (error: any) {
|
|
126
|
+
return {
|
|
127
|
+
css: '',
|
|
128
|
+
duration: Math.round(performance.now() - startTime),
|
|
129
|
+
success: false,
|
|
130
|
+
error: error.message
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Compile CSS asynchronously (non-blocking)
|
|
137
|
+
*
|
|
138
|
+
* Used for HMR updates where we don't want to block the main thread.
|
|
139
|
+
*/
|
|
140
|
+
export async function compileCssAsync(options: CSSCompileOptions): Promise<CSSCompileResult> {
|
|
141
|
+
return new Promise((resolve) => {
|
|
142
|
+
const startTime = performance.now()
|
|
143
|
+
const { input, output, minify = false } = options
|
|
144
|
+
|
|
145
|
+
if (!fs.existsSync(input)) {
|
|
146
|
+
resolve({
|
|
147
|
+
css: '',
|
|
148
|
+
duration: 0,
|
|
149
|
+
success: false,
|
|
150
|
+
error: `CSS input file not found: ${input}`
|
|
151
|
+
})
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const args = ['@tailwindcss/cli', '-i', input]
|
|
156
|
+
const useStdout = output === ':memory:'
|
|
157
|
+
|
|
158
|
+
if (!useStdout) {
|
|
159
|
+
args.push('-o', output)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (minify) {
|
|
163
|
+
args.push('--minify')
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const child = spawn('bunx', args, {
|
|
167
|
+
cwd: path.dirname(input),
|
|
168
|
+
stdio: useStdout ? ['pipe', 'pipe', 'pipe'] : ['pipe', 'inherit', 'pipe'],
|
|
169
|
+
env: { ...process.env }
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
let stdout = ''
|
|
173
|
+
let stderr = ''
|
|
174
|
+
|
|
175
|
+
if (useStdout && child.stdout) {
|
|
176
|
+
child.stdout.on('data', (data) => { stdout += data.toString() })
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (child.stderr) {
|
|
180
|
+
child.stderr.on('data', (data) => { stderr += data.toString() })
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
child.on('close', (code) => {
|
|
184
|
+
const duration = Math.round(performance.now() - startTime)
|
|
185
|
+
|
|
186
|
+
if (code !== 0) {
|
|
187
|
+
resolve({
|
|
188
|
+
css: '',
|
|
189
|
+
duration,
|
|
190
|
+
success: false,
|
|
191
|
+
error: `Tailwind compilation failed: ${stderr}`
|
|
192
|
+
})
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let css = ''
|
|
197
|
+
if (useStdout) {
|
|
198
|
+
css = stdout
|
|
199
|
+
} else if (fs.existsSync(output)) {
|
|
200
|
+
css = fs.readFileSync(output, 'utf-8')
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
resolve({
|
|
204
|
+
css,
|
|
205
|
+
duration,
|
|
206
|
+
success: true
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
child.on('error', (err) => {
|
|
211
|
+
resolve({
|
|
212
|
+
css: '',
|
|
213
|
+
duration: Math.round(performance.now() - startTime),
|
|
214
|
+
success: false,
|
|
215
|
+
error: err.message
|
|
216
|
+
})
|
|
217
|
+
})
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ============================================
|
|
222
|
+
// CSS Watcher for HMR
|
|
223
|
+
// ============================================
|
|
224
|
+
|
|
225
|
+
export interface CSSWatchOptions extends CSSCompileOptions {
|
|
226
|
+
/** Callback when CSS changes */
|
|
227
|
+
onChange: (result: CSSCompileResult) => void
|
|
228
|
+
/** Debounce delay in ms */
|
|
229
|
+
debounce?: number
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Watch CSS and source files for changes, recompile on change
|
|
234
|
+
*
|
|
235
|
+
* This is used by the dev server for HMR support.
|
|
236
|
+
* It watches both the CSS entry point AND all .zen files
|
|
237
|
+
* that Tailwind scans for class names.
|
|
238
|
+
*/
|
|
239
|
+
export function watchCss(options: CSSWatchOptions): () => void {
|
|
240
|
+
const { input, output, minify, onChange, debounce = 100 } = options
|
|
241
|
+
|
|
242
|
+
let timeout: NodeJS.Timeout | null = null
|
|
243
|
+
let isCompiling = false
|
|
244
|
+
|
|
245
|
+
const recompile = async () => {
|
|
246
|
+
if (isCompiling) return
|
|
247
|
+
isCompiling = true
|
|
248
|
+
|
|
249
|
+
const result = await compileCssAsync({ input, output, minify })
|
|
250
|
+
onChange(result)
|
|
251
|
+
|
|
252
|
+
isCompiling = false
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const debouncedRecompile = () => {
|
|
256
|
+
if (timeout) clearTimeout(timeout)
|
|
257
|
+
timeout = setTimeout(recompile, debounce)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Watch the styles directory
|
|
261
|
+
const stylesDir = path.dirname(input)
|
|
262
|
+
const stylesWatcher = fs.watch(stylesDir, { recursive: true }, (event, filename) => {
|
|
263
|
+
if (filename?.endsWith('.css')) {
|
|
264
|
+
debouncedRecompile()
|
|
265
|
+
}
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
// Watch source files that Tailwind scans (for class changes)
|
|
269
|
+
// This assumes standard Zenith structure: src/pages, src/components, src/layouts
|
|
270
|
+
const srcDir = path.resolve(stylesDir, '..')
|
|
271
|
+
let srcWatcher: fs.FSWatcher | null = null
|
|
272
|
+
|
|
273
|
+
if (fs.existsSync(srcDir)) {
|
|
274
|
+
srcWatcher = fs.watch(srcDir, { recursive: true }, (event, filename) => {
|
|
275
|
+
if (filename?.endsWith('.zen') || filename?.endsWith('.tsx') || filename?.endsWith('.jsx')) {
|
|
276
|
+
debouncedRecompile()
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Return cleanup function
|
|
282
|
+
return () => {
|
|
283
|
+
if (timeout) clearTimeout(timeout)
|
|
284
|
+
stylesWatcher.close()
|
|
285
|
+
srcWatcher?.close()
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ============================================
|
|
290
|
+
// Path Utilities
|
|
291
|
+
// ============================================
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Resolve the canonical globals.css path for a Zenith project
|
|
295
|
+
*/
|
|
296
|
+
export function resolveGlobalsCss(projectRoot: string): string | null {
|
|
297
|
+
// Check for globals.css (canonical)
|
|
298
|
+
const globalsPath = path.join(projectRoot, 'src', 'styles', 'globals.css')
|
|
299
|
+
if (fs.existsSync(globalsPath)) return globalsPath
|
|
300
|
+
|
|
301
|
+
// Check for global.css (legacy)
|
|
302
|
+
const globalPath = path.join(projectRoot, 'src', 'styles', 'global.css')
|
|
303
|
+
if (fs.existsSync(globalPath)) return globalPath
|
|
304
|
+
|
|
305
|
+
return null
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Get the output path for compiled CSS
|
|
310
|
+
*/
|
|
311
|
+
export function getCompiledCssPath(projectRoot: string, mode: 'dev' | 'build'): string {
|
|
312
|
+
if (mode === 'build') {
|
|
313
|
+
return path.join(projectRoot, 'dist', 'assets', 'styles.css')
|
|
314
|
+
}
|
|
315
|
+
// In dev mode, we use in-memory compilation
|
|
316
|
+
return ':memory:'
|
|
317
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component Discovery
|
|
3
|
+
*
|
|
4
|
+
* Discovers and catalogs components in a Zenith project
|
|
5
|
+
* Similar to layout discovery but for reusable components
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from 'fs'
|
|
9
|
+
import * as path from 'path'
|
|
10
|
+
import { parseZenFile } from '../parse/parseZenFile'
|
|
11
|
+
import type { TemplateNode } from '../ir/types'
|
|
12
|
+
|
|
13
|
+
export interface SlotDefinition {
|
|
14
|
+
name: string | null // null = default slot, string = named slot
|
|
15
|
+
location: {
|
|
16
|
+
line: number
|
|
17
|
+
column: number
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ComponentMetadata {
|
|
22
|
+
name: string // Component name (e.g., "Card", "Button")
|
|
23
|
+
path: string // Absolute path to .zen file
|
|
24
|
+
template: string // Raw template HTML
|
|
25
|
+
nodes: TemplateNode[] // Parsed template nodes
|
|
26
|
+
slots: SlotDefinition[]
|
|
27
|
+
props: string[] // Declared props
|
|
28
|
+
styles: string[] // Raw CSS from <style> blocks
|
|
29
|
+
hasScript: boolean
|
|
30
|
+
hasStyles: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Discover all components in a directory
|
|
35
|
+
* @param baseDir - Base directory to search (e.g., src/components)
|
|
36
|
+
* @returns Map of component name to metadata
|
|
37
|
+
*/
|
|
38
|
+
export function discoverComponents(baseDir: string): Map<string, ComponentMetadata> {
|
|
39
|
+
const components = new Map<string, ComponentMetadata>()
|
|
40
|
+
|
|
41
|
+
// Check if components directory exists
|
|
42
|
+
if (!fs.existsSync(baseDir)) {
|
|
43
|
+
return components
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Recursively find all .zen files
|
|
47
|
+
const zenFiles = findZenFiles(baseDir)
|
|
48
|
+
|
|
49
|
+
for (const filePath of zenFiles) {
|
|
50
|
+
try {
|
|
51
|
+
const metadata = parseComponentFile(filePath)
|
|
52
|
+
if (metadata) {
|
|
53
|
+
components.set(metadata.name, metadata)
|
|
54
|
+
}
|
|
55
|
+
} catch (error: any) {
|
|
56
|
+
console.warn(`[Zenith] Failed to parse component ${filePath}: ${error.message}`)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return components
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Recursively find all .zen files in a directory
|
|
65
|
+
*/
|
|
66
|
+
function findZenFiles(dir: string): string[] {
|
|
67
|
+
const files: string[] = []
|
|
68
|
+
|
|
69
|
+
if (!fs.existsSync(dir)) {
|
|
70
|
+
return files
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
74
|
+
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
const fullPath = path.join(dir, entry.name)
|
|
77
|
+
|
|
78
|
+
if (entry.isDirectory()) {
|
|
79
|
+
files.push(...findZenFiles(fullPath))
|
|
80
|
+
} else if (entry.isFile() && entry.name.endsWith('.zen')) {
|
|
81
|
+
files.push(fullPath)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return files
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Parse a component file and extract metadata
|
|
90
|
+
*/
|
|
91
|
+
function parseComponentFile(filePath: string): ComponentMetadata | null {
|
|
92
|
+
const ir = parseZenFile(filePath)
|
|
93
|
+
|
|
94
|
+
// Extract component name from filename
|
|
95
|
+
const basename = path.basename(filePath, '.zen')
|
|
96
|
+
const componentName = basename
|
|
97
|
+
|
|
98
|
+
// Extract slots from template
|
|
99
|
+
const slots = extractSlots(ir.template.nodes)
|
|
100
|
+
|
|
101
|
+
// Extract props from script attributes
|
|
102
|
+
const props = ir.script?.attributes['props']?.split(',').map(p => p.trim()) || []
|
|
103
|
+
|
|
104
|
+
// Extract raw CSS from styles
|
|
105
|
+
const styles = ir.styles.map(s => s.raw)
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
name: componentName,
|
|
109
|
+
path: filePath,
|
|
110
|
+
template: ir.template.raw,
|
|
111
|
+
nodes: ir.template.nodes,
|
|
112
|
+
slots,
|
|
113
|
+
props,
|
|
114
|
+
styles,
|
|
115
|
+
hasScript: ir.script !== null,
|
|
116
|
+
hasStyles: ir.styles.length > 0
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Extract slot definitions from template nodes
|
|
122
|
+
*/
|
|
123
|
+
function extractSlots(nodes: TemplateNode[]): SlotDefinition[] {
|
|
124
|
+
const slots: SlotDefinition[] = []
|
|
125
|
+
|
|
126
|
+
function traverse(node: TemplateNode) {
|
|
127
|
+
if (node.type === 'element') {
|
|
128
|
+
// Check if this is a <slot> tag
|
|
129
|
+
if (node.tag === 'slot') {
|
|
130
|
+
// Extract slot name from attributes
|
|
131
|
+
const nameAttr = node.attributes.find(attr => attr.name === 'name')
|
|
132
|
+
const slotName = typeof nameAttr?.value === 'string' ? nameAttr.value : null
|
|
133
|
+
|
|
134
|
+
slots.push({
|
|
135
|
+
name: slotName,
|
|
136
|
+
location: node.location
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Traverse children
|
|
141
|
+
for (const child of node.children) {
|
|
142
|
+
traverse(child)
|
|
143
|
+
}
|
|
144
|
+
} else if (node.type === 'component') {
|
|
145
|
+
// Also traverse component children
|
|
146
|
+
for (const child of node.children) {
|
|
147
|
+
traverse(child)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for (const node of nodes) {
|
|
153
|
+
traverse(node)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return slots
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Check if a tag name represents a component (starts with uppercase)
|
|
161
|
+
*/
|
|
162
|
+
export function isComponentTag(tagName: string): boolean {
|
|
163
|
+
return tagName.length > 0 && tagName[0] !== undefined && tagName[0] === tagName[0].toUpperCase()
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Get component metadata by name
|
|
168
|
+
*/
|
|
169
|
+
export function getComponent(
|
|
170
|
+
components: Map<string, ComponentMetadata>,
|
|
171
|
+
name: string
|
|
172
|
+
): ComponentMetadata | undefined {
|
|
173
|
+
return components.get(name)
|
|
174
|
+
}
|
|
@@ -22,3 +22,35 @@ export class CompilerError extends Error {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Invariant Error
|
|
27
|
+
*
|
|
28
|
+
* Thrown when a Zenith compiler invariant is violated.
|
|
29
|
+
* Invariants are non-negotiable rules that guarantee correct behavior.
|
|
30
|
+
*
|
|
31
|
+
* If an invariant fails, the compiler is at fault — not the user.
|
|
32
|
+
* The user receives a clear explanation of what is forbidden and why.
|
|
33
|
+
*/
|
|
34
|
+
export class InvariantError extends CompilerError {
|
|
35
|
+
invariantId: string
|
|
36
|
+
guarantee: string
|
|
37
|
+
|
|
38
|
+
constructor(
|
|
39
|
+
invariantId: string,
|
|
40
|
+
message: string,
|
|
41
|
+
guarantee: string,
|
|
42
|
+
file: string,
|
|
43
|
+
line: number,
|
|
44
|
+
column: number
|
|
45
|
+
) {
|
|
46
|
+
super(`[${invariantId}] ${message}\n\n Zenith Guarantee: ${guarantee}`, file, line, column)
|
|
47
|
+
this.name = 'InvariantError'
|
|
48
|
+
this.invariantId = invariantId
|
|
49
|
+
this.guarantee = guarantee
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
override toString(): string {
|
|
53
|
+
return `${this.file}:${this.line}:${this.column} [${this.invariantId}] ${this.message}`
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|