@zenithbuild/core 0.3.3 → 0.4.2
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 +184 -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 +421 -37
- package/runtime/client-runtime.ts +17 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expression JSX Transformer
|
|
3
|
+
*
|
|
4
|
+
* Transforms JSX-like tags inside Zenith expressions into __zenith.h() calls.
|
|
5
|
+
* This allows Zenith to support JSX semantics without a full JSX compiler like Babel.
|
|
6
|
+
*
|
|
7
|
+
* Handles:
|
|
8
|
+
* - Multi-line JSX expressions
|
|
9
|
+
* - Nested elements
|
|
10
|
+
* - Complex event handlers like onclick={() => fn(item)}
|
|
11
|
+
* - Expression attributes {expr}
|
|
12
|
+
* - Text interpolation {item.title}
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Find the end of a balanced brace expression
|
|
17
|
+
*/
|
|
18
|
+
function findBalancedBraceEnd(code: string, startIndex: number): number {
|
|
19
|
+
let braceCount = 1
|
|
20
|
+
let i = startIndex + 1
|
|
21
|
+
let inString = false
|
|
22
|
+
let stringChar = ''
|
|
23
|
+
let inTemplate = false
|
|
24
|
+
|
|
25
|
+
while (i < code.length && braceCount > 0) {
|
|
26
|
+
const char = code[i]
|
|
27
|
+
const prevChar = i > 0 ? code[i - 1] : ''
|
|
28
|
+
|
|
29
|
+
// Handle escape sequences
|
|
30
|
+
if (prevChar === '\\') {
|
|
31
|
+
i++
|
|
32
|
+
continue
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Handle string literals
|
|
36
|
+
if (!inString && !inTemplate && (char === '"' || char === "'")) {
|
|
37
|
+
inString = true
|
|
38
|
+
stringChar = char
|
|
39
|
+
i++
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (inString && char === stringChar) {
|
|
44
|
+
inString = false
|
|
45
|
+
stringChar = ''
|
|
46
|
+
i++
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Handle template literals
|
|
51
|
+
if (!inString && !inTemplate && char === '`') {
|
|
52
|
+
inTemplate = true
|
|
53
|
+
i++
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (inTemplate && char === '`') {
|
|
58
|
+
inTemplate = false
|
|
59
|
+
i++
|
|
60
|
+
continue
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Count braces only when not in strings
|
|
64
|
+
if (!inString && !inTemplate) {
|
|
65
|
+
if (char === '{') braceCount++
|
|
66
|
+
else if (char === '}') braceCount--
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
i++
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return braceCount === 0 ? i : -1
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Parse JSX attributes using balanced parsing for expression values
|
|
77
|
+
*/
|
|
78
|
+
function parseJSXAttributes(code: string, startIndex: number): {
|
|
79
|
+
attrs: string;
|
|
80
|
+
endIndex: number;
|
|
81
|
+
isSelfClosing: boolean
|
|
82
|
+
} {
|
|
83
|
+
const attrPairs: string[] = []
|
|
84
|
+
let i = startIndex
|
|
85
|
+
|
|
86
|
+
// Skip whitespace
|
|
87
|
+
while (i < code.length && /\s/.test(code[i]!)) i++
|
|
88
|
+
|
|
89
|
+
while (i < code.length) {
|
|
90
|
+
const char = code[i]
|
|
91
|
+
|
|
92
|
+
// Check for end of opening tag
|
|
93
|
+
if (char === '>') {
|
|
94
|
+
return { attrs: formatAttrs(attrPairs), endIndex: i + 1, isSelfClosing: false }
|
|
95
|
+
}
|
|
96
|
+
if (char === '/' && code[i + 1] === '>') {
|
|
97
|
+
return { attrs: formatAttrs(attrPairs), endIndex: i + 2, isSelfClosing: true }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Parse attribute name
|
|
101
|
+
const nameMatch = code.slice(i).match(/^([a-zA-Z_][a-zA-Z0-9_-]*)/)
|
|
102
|
+
if (!nameMatch) {
|
|
103
|
+
i++
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const attrName = nameMatch[1]!
|
|
108
|
+
i += attrName.length
|
|
109
|
+
|
|
110
|
+
// Skip whitespace
|
|
111
|
+
while (i < code.length && /\s/.test(code[i]!)) i++
|
|
112
|
+
|
|
113
|
+
// Check for value
|
|
114
|
+
if (code[i] !== '=') {
|
|
115
|
+
attrPairs.push(`"${attrName}": true`)
|
|
116
|
+
continue
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
i++ // Skip '='
|
|
120
|
+
|
|
121
|
+
// Skip whitespace
|
|
122
|
+
while (i < code.length && /\s/.test(code[i]!)) i++
|
|
123
|
+
|
|
124
|
+
// Parse value
|
|
125
|
+
if (code[i] === '"' || code[i] === "'") {
|
|
126
|
+
const quote = code[i]
|
|
127
|
+
let endQuote = i + 1
|
|
128
|
+
while (endQuote < code.length && code[endQuote] !== quote) {
|
|
129
|
+
if (code[endQuote] === '\\') endQuote++ // Skip escaped chars
|
|
130
|
+
endQuote++
|
|
131
|
+
}
|
|
132
|
+
const value = code.slice(i + 1, endQuote)
|
|
133
|
+
attrPairs.push(`"${attrName}": "${value}"`)
|
|
134
|
+
i = endQuote + 1
|
|
135
|
+
} else if (code[i] === '{') {
|
|
136
|
+
// Expression value - find balanced end
|
|
137
|
+
const endBrace = findBalancedBraceEnd(code, i)
|
|
138
|
+
if (endBrace === -1) {
|
|
139
|
+
i++
|
|
140
|
+
continue
|
|
141
|
+
}
|
|
142
|
+
const expr = code.slice(i + 1, endBrace - 1).trim()
|
|
143
|
+
attrPairs.push(`"${attrName}": ${expr}`)
|
|
144
|
+
i = endBrace
|
|
145
|
+
} else {
|
|
146
|
+
// Unquoted value (rare in JSX, but support it)
|
|
147
|
+
const unquotedMatch = code.slice(i).match(/^([^\s/>]+)/)
|
|
148
|
+
if (unquotedMatch) {
|
|
149
|
+
attrPairs.push(`"${attrName}": "${unquotedMatch[1]}"`)
|
|
150
|
+
i += unquotedMatch[1]!.length
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Skip whitespace
|
|
155
|
+
while (i < code.length && /\s/.test(code[i]!)) i++
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { attrs: formatAttrs(attrPairs), endIndex: i, isSelfClosing: false }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function formatAttrs(pairs: string[]): string {
|
|
162
|
+
return pairs.length > 0 ? `{ ${pairs.join(', ')} }` : 'null'
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Find the matching closing tag for an element
|
|
167
|
+
*/
|
|
168
|
+
function findClosingTag(code: string, startIndex: number, tagName: string): number {
|
|
169
|
+
let depth = 1
|
|
170
|
+
let i = startIndex
|
|
171
|
+
const openPattern = new RegExp(`<${tagName}(?:\\s|>|/>)`, 'i')
|
|
172
|
+
const closeTag = `</${tagName}>`
|
|
173
|
+
|
|
174
|
+
while (i < code.length && depth > 0) {
|
|
175
|
+
// Check for closing tag
|
|
176
|
+
if (code.slice(i, i + closeTag.length).toLowerCase() === closeTag.toLowerCase()) {
|
|
177
|
+
depth--
|
|
178
|
+
if (depth === 0) return i
|
|
179
|
+
i += closeTag.length
|
|
180
|
+
continue
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Check for opening tag (same name, nested)
|
|
184
|
+
const openMatch = code.slice(i).match(openPattern)
|
|
185
|
+
if (openMatch && openMatch.index === 0) {
|
|
186
|
+
// Check if it's self-closing
|
|
187
|
+
const selfClosing = code.slice(i).match(new RegExp(`<${tagName}[^>]*/>`, 'i'))
|
|
188
|
+
if (!selfClosing || selfClosing.index !== 0) {
|
|
189
|
+
depth++
|
|
190
|
+
}
|
|
191
|
+
i += openMatch[0].length
|
|
192
|
+
continue
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
i++
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return -1
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Parse JSX children content
|
|
203
|
+
*/
|
|
204
|
+
function parseJSXChildren(code: string, startIndex: number, tagName: string): {
|
|
205
|
+
children: string;
|
|
206
|
+
endIndex: number
|
|
207
|
+
} {
|
|
208
|
+
const closingIndex = findClosingTag(code, startIndex, tagName)
|
|
209
|
+
if (closingIndex === -1) {
|
|
210
|
+
return { children: 'null', endIndex: code.length }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const content = code.slice(startIndex, closingIndex)
|
|
214
|
+
|
|
215
|
+
if (!content.trim()) {
|
|
216
|
+
return { children: 'null', endIndex: closingIndex }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Transform the children content
|
|
220
|
+
const transformedContent = transformChildContent(content)
|
|
221
|
+
|
|
222
|
+
return { children: transformedContent, endIndex: closingIndex }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Transform content that may contain text, expressions, and nested JSX
|
|
227
|
+
*/
|
|
228
|
+
function transformChildContent(content: string): string {
|
|
229
|
+
const parts: string[] = []
|
|
230
|
+
let i = 0
|
|
231
|
+
let currentText = ''
|
|
232
|
+
|
|
233
|
+
while (i < content.length) {
|
|
234
|
+
const char = content[i]
|
|
235
|
+
|
|
236
|
+
// Check for JSX element
|
|
237
|
+
if (char === '<' && /[a-zA-Z]/.test(content[i + 1] || '')) {
|
|
238
|
+
// Save any accumulated text
|
|
239
|
+
if (currentText.trim()) {
|
|
240
|
+
parts.push(`"${escapeString(currentText.trim())}"`)
|
|
241
|
+
currentText = ''
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Try to parse as JSX element
|
|
245
|
+
const parsed = parseJSXElement(content, i)
|
|
246
|
+
if (parsed) {
|
|
247
|
+
parts.push(parsed.hCall)
|
|
248
|
+
i = parsed.endIndex
|
|
249
|
+
continue
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Check for expression {expr}
|
|
254
|
+
if (char === '{') {
|
|
255
|
+
const endBrace = findBalancedBraceEnd(content, i)
|
|
256
|
+
if (endBrace !== -1) {
|
|
257
|
+
// Save any accumulated text
|
|
258
|
+
if (currentText.trim()) {
|
|
259
|
+
parts.push(`"${escapeString(currentText.trim())}"`)
|
|
260
|
+
currentText = ''
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Extract and add expression
|
|
264
|
+
const expr = content.slice(i + 1, endBrace - 1).trim()
|
|
265
|
+
if (expr) {
|
|
266
|
+
// Transform any JSX inside the expression
|
|
267
|
+
const transformedExpr = transformExpressionJSX(expr)
|
|
268
|
+
parts.push(transformedExpr)
|
|
269
|
+
}
|
|
270
|
+
i = endBrace
|
|
271
|
+
continue
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Accumulate text
|
|
276
|
+
currentText += char
|
|
277
|
+
i++
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Add remaining text
|
|
281
|
+
if (currentText.trim()) {
|
|
282
|
+
parts.push(`"${escapeString(currentText.trim())}"`)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (parts.length === 0) return 'null'
|
|
286
|
+
if (parts.length === 1) return parts[0]!
|
|
287
|
+
return `[${parts.join(', ')}]`
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Escape a string for use in JavaScript
|
|
292
|
+
*/
|
|
293
|
+
function escapeString(str: string): string {
|
|
294
|
+
return str
|
|
295
|
+
.replace(/\\/g, '\\\\')
|
|
296
|
+
.replace(/"/g, '\\"')
|
|
297
|
+
.replace(/\n/g, '\\n')
|
|
298
|
+
.replace(/\r/g, '\\r')
|
|
299
|
+
.replace(/\t/g, '\\t')
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Parse a single JSX element starting at the given index
|
|
304
|
+
*/
|
|
305
|
+
function parseJSXElement(code: string, startIndex: number): { hCall: string; endIndex: number } | null {
|
|
306
|
+
// Extract tag name
|
|
307
|
+
const tagMatch = code.slice(startIndex).match(/^<([a-zA-Z][a-zA-Z0-9]*)/)
|
|
308
|
+
if (!tagMatch) return null
|
|
309
|
+
|
|
310
|
+
const tagName = tagMatch[1]!
|
|
311
|
+
let i = startIndex + tagMatch[0].length
|
|
312
|
+
|
|
313
|
+
// Parse attributes
|
|
314
|
+
const { attrs, endIndex: attrEnd, isSelfClosing } = parseJSXAttributes(code, i)
|
|
315
|
+
i = attrEnd
|
|
316
|
+
|
|
317
|
+
if (isSelfClosing) {
|
|
318
|
+
return {
|
|
319
|
+
hCall: `__zenith.h("${tagName}", ${attrs}, null)`,
|
|
320
|
+
endIndex: i
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Parse children until closing tag
|
|
325
|
+
const { children, endIndex: childEnd } = parseJSXChildren(code, i, tagName)
|
|
326
|
+
i = childEnd
|
|
327
|
+
|
|
328
|
+
// Skip closing tag
|
|
329
|
+
const closeTag = `</${tagName}>`
|
|
330
|
+
if (code.slice(i, i + closeTag.length).toLowerCase() === closeTag.toLowerCase()) {
|
|
331
|
+
i += closeTag.length
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
hCall: `__zenith.h("${tagName}", ${attrs}, ${children})`,
|
|
336
|
+
endIndex: i
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Main transformer function
|
|
342
|
+
*
|
|
343
|
+
* Transforms JSX-like tags inside Zenith expressions into __zenith.h() calls.
|
|
344
|
+
*/
|
|
345
|
+
export function transformExpressionJSX(code: string): string {
|
|
346
|
+
// Skip if no JSX-like content (optimization)
|
|
347
|
+
if (!/<[a-zA-Z]/.test(code)) {
|
|
348
|
+
return code
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
let result = ''
|
|
352
|
+
let i = 0
|
|
353
|
+
|
|
354
|
+
while (i < code.length) {
|
|
355
|
+
// Look for potential JSX tag start
|
|
356
|
+
// Only treat as JSX if it follows common JSX contexts: (, return, =, :, ,, [, ?
|
|
357
|
+
if (code[i] === '<' && /[a-zA-Z]/.test(code[i + 1] || '')) {
|
|
358
|
+
// Check if this looks like a JSX context
|
|
359
|
+
const beforeChar = i > 0 ? code[i - 1] : ''
|
|
360
|
+
const beforeTrimmed = code.slice(0, i).trimEnd()
|
|
361
|
+
const lastChar = beforeTrimmed[beforeTrimmed.length - 1] || ''
|
|
362
|
+
|
|
363
|
+
// Common JSX-starting contexts
|
|
364
|
+
const jsxContexts = ['(', '=', ':', ',', '[', '?', '{', 'n'] // 'n' for 'return'
|
|
365
|
+
const isJSXContext = jsxContexts.includes(lastChar) ||
|
|
366
|
+
beforeTrimmed.endsWith('return') ||
|
|
367
|
+
beforeTrimmed === '' ||
|
|
368
|
+
(beforeChar && /\s/.test(beforeChar))
|
|
369
|
+
|
|
370
|
+
if (isJSXContext) {
|
|
371
|
+
const parsed = parseJSXElement(code, i)
|
|
372
|
+
if (parsed) {
|
|
373
|
+
result += parsed.hCall
|
|
374
|
+
i = parsed.endIndex
|
|
375
|
+
continue
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
result += code[i]
|
|
381
|
+
i++
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return result
|
|
385
|
+
}
|
|
@@ -50,7 +50,7 @@ export function transformNode(
|
|
|
50
50
|
loopContext: activeLoopContext // Phase 7: Attach loop context to binding
|
|
51
51
|
})
|
|
52
52
|
|
|
53
|
-
return `<span data-zen-text="${bindingId}"></span>`
|
|
53
|
+
return `<span data-zen-text="${bindingId}" style="display: contents;"></span>`
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
case 'element': {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zenith Config
|
|
3
|
+
*
|
|
4
|
+
* Public exports for zenith/config
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { defineConfig } from './types';
|
|
8
|
+
export type {
|
|
9
|
+
ZenithConfig,
|
|
10
|
+
ZenithPlugin,
|
|
11
|
+
PluginContext,
|
|
12
|
+
ContentSourceConfig,
|
|
13
|
+
ContentPluginOptions,
|
|
14
|
+
ContentItem
|
|
15
|
+
} from './types';
|
|
16
|
+
export { loadZenithConfig, hasZenithConfig } from './loader';
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zenith Config Loader
|
|
3
|
+
*
|
|
4
|
+
* Loads zenith.config.ts from the project root
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import type { ZenithConfig } from './types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Load zenith.config.ts from the project root
|
|
13
|
+
*
|
|
14
|
+
* @param projectRoot - Absolute path to the project root
|
|
15
|
+
* @returns Parsed ZenithConfig or empty config if not found
|
|
16
|
+
*/
|
|
17
|
+
export async function loadZenithConfig(projectRoot: string): Promise<ZenithConfig> {
|
|
18
|
+
// Check for TypeScript config first, then JavaScript
|
|
19
|
+
const configPaths = [
|
|
20
|
+
path.join(projectRoot, 'zenith.config.ts'),
|
|
21
|
+
path.join(projectRoot, 'zenith.config.js'),
|
|
22
|
+
path.join(projectRoot, 'zenith.config.mjs'),
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
let configPath: string | null = null;
|
|
26
|
+
for (const p of configPaths) {
|
|
27
|
+
if (fs.existsSync(p)) {
|
|
28
|
+
configPath = p;
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!configPath) {
|
|
34
|
+
// No config file found, return empty config
|
|
35
|
+
return { plugins: [] };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
// Use dynamic import to load the config
|
|
40
|
+
// Bun supports importing TS files directly
|
|
41
|
+
const configModule = await import(configPath);
|
|
42
|
+
const config = configModule.default || configModule;
|
|
43
|
+
|
|
44
|
+
// Validate basic structure
|
|
45
|
+
if (typeof config !== 'object' || config === null) {
|
|
46
|
+
console.warn(`[Zenith] Invalid config format in ${configPath}`);
|
|
47
|
+
return { plugins: [] };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return config as ZenithConfig;
|
|
51
|
+
} catch (error: unknown) {
|
|
52
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
53
|
+
console.error(`[Zenith] Failed to load config from ${configPath}:`, message);
|
|
54
|
+
return { plugins: [] };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Check if a zenith.config.ts exists in the project
|
|
60
|
+
*/
|
|
61
|
+
export function hasZenithConfig(projectRoot: string): boolean {
|
|
62
|
+
const configPaths = [
|
|
63
|
+
path.join(projectRoot, 'zenith.config.ts'),
|
|
64
|
+
path.join(projectRoot, 'zenith.config.js'),
|
|
65
|
+
path.join(projectRoot, 'zenith.config.mjs'),
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
return configPaths.some(p => fs.existsSync(p));
|
|
69
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zenith Config Types
|
|
3
|
+
*
|
|
4
|
+
* Configuration interfaces for zenith.config.ts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ============================================
|
|
8
|
+
// Content Plugin Types
|
|
9
|
+
// ============================================
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Configuration for a content source
|
|
13
|
+
*/
|
|
14
|
+
export interface ContentSourceConfig {
|
|
15
|
+
/** Root directory relative to project root (e.g., "../zenith-docs" or "content") */
|
|
16
|
+
root: string;
|
|
17
|
+
/** Folders to include from the root (e.g., ["documentation"]). Defaults to all. */
|
|
18
|
+
include?: string[];
|
|
19
|
+
/** Folders to exclude from the root (e.g., ["changelog"]) */
|
|
20
|
+
exclude?: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Options for the content plugin
|
|
25
|
+
*/
|
|
26
|
+
export interface ContentPluginOptions {
|
|
27
|
+
/** Named content sources mapped to their configuration */
|
|
28
|
+
sources?: Record<string, ContentSourceConfig>;
|
|
29
|
+
/** Legacy: Single content directory (deprecated, use sources instead) */
|
|
30
|
+
contentDir?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ============================================
|
|
34
|
+
// Core Plugin Types
|
|
35
|
+
// ============================================
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Context passed to plugins during setup
|
|
39
|
+
*/
|
|
40
|
+
export interface PluginContext {
|
|
41
|
+
/** Absolute path to project root */
|
|
42
|
+
projectRoot: string;
|
|
43
|
+
/** Set content data for the runtime */
|
|
44
|
+
setContentData: (data: Record<string, ContentItem[]>) => void;
|
|
45
|
+
/** Additional options passed from config */
|
|
46
|
+
options?: Record<string, unknown>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A content item loaded from a source
|
|
51
|
+
*/
|
|
52
|
+
export interface ContentItem {
|
|
53
|
+
id?: string | number;
|
|
54
|
+
slug?: string | null;
|
|
55
|
+
collection?: string | null;
|
|
56
|
+
content?: string | null;
|
|
57
|
+
[key: string]: unknown;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A Zenith plugin definition
|
|
62
|
+
*/
|
|
63
|
+
export interface ZenithPlugin {
|
|
64
|
+
/** Unique plugin name */
|
|
65
|
+
name: string;
|
|
66
|
+
/** Setup function called during initialization */
|
|
67
|
+
setup: (ctx: PluginContext) => void | Promise<void>;
|
|
68
|
+
/** Plugin-specific configuration (preserved for reference) */
|
|
69
|
+
config?: unknown;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ============================================
|
|
73
|
+
// Main Config Types
|
|
74
|
+
// ============================================
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Zenith configuration object
|
|
78
|
+
*/
|
|
79
|
+
export interface ZenithConfig {
|
|
80
|
+
/** List of plugins to load */
|
|
81
|
+
plugins?: ZenithPlugin[];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Define a Zenith configuration with full type safety
|
|
86
|
+
*/
|
|
87
|
+
export function defineConfig(config: ZenithConfig): ZenithConfig {
|
|
88
|
+
return config;
|
|
89
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zenith Plugin Registry
|
|
3
|
+
*
|
|
4
|
+
* Manages plugin registration and initialization
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ZenithPlugin, PluginContext, ContentItem } from '../config/types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Plugin registry for managing Zenith plugins
|
|
11
|
+
*/
|
|
12
|
+
export class PluginRegistry {
|
|
13
|
+
private plugins = new Map<string, ZenithPlugin>();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Register a plugin
|
|
17
|
+
*/
|
|
18
|
+
register(plugin: ZenithPlugin): void {
|
|
19
|
+
if (this.plugins.has(plugin.name)) {
|
|
20
|
+
console.warn(`[Zenith] Plugin "${plugin.name}" is already registered. Overwriting.`);
|
|
21
|
+
}
|
|
22
|
+
this.plugins.set(plugin.name, plugin);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get a plugin by name
|
|
27
|
+
*/
|
|
28
|
+
get(name: string): ZenithPlugin | undefined {
|
|
29
|
+
return this.plugins.get(name);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if a plugin is registered
|
|
34
|
+
*/
|
|
35
|
+
has(name: string): boolean {
|
|
36
|
+
return this.plugins.has(name);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get all registered plugins
|
|
41
|
+
*/
|
|
42
|
+
all(): ZenithPlugin[] {
|
|
43
|
+
return Array.from(this.plugins.values());
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Initialize all plugins with the provided context
|
|
48
|
+
*/
|
|
49
|
+
async initAll(ctx: PluginContext): Promise<void> {
|
|
50
|
+
for (const plugin of this.plugins.values()) {
|
|
51
|
+
try {
|
|
52
|
+
await plugin.setup(ctx);
|
|
53
|
+
console.log(`[Zenith] Plugin "${plugin.name}" initialized`);
|
|
54
|
+
} catch (error: unknown) {
|
|
55
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
56
|
+
console.error(`[Zenith] Failed to initialize plugin "${plugin.name}":`, message);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Clear all registered plugins
|
|
63
|
+
*/
|
|
64
|
+
clear(): void {
|
|
65
|
+
this.plugins.clear();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Create a plugin context for initialization
|
|
71
|
+
*/
|
|
72
|
+
export function createPluginContext(
|
|
73
|
+
projectRoot: string,
|
|
74
|
+
contentSetter: (data: Record<string, ContentItem[]>) => void
|
|
75
|
+
): PluginContext {
|
|
76
|
+
return {
|
|
77
|
+
projectRoot,
|
|
78
|
+
setContentData: contentSetter,
|
|
79
|
+
options: {}
|
|
80
|
+
};
|
|
81
|
+
}
|