@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.
@@ -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,7 @@
1
+ /**
2
+ * Zenith Plugins
3
+ *
4
+ * Public exports for plugin system
5
+ */
6
+
7
+ export { PluginRegistry, createPluginContext } from './registry';
@@ -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
+ }
package/dist/cli.js CHANGED
@@ -4,6 +4,7 @@
4
4
  #!/usr/bin/env bun
5
5
  #!/usr/bin/env bun
6
6
  #!/usr/bin/env bun
7
+ #!/usr/bin/env bun
7
8
  // @bun
8
9
  var __create = Object.create;
9
10
  var __getProtoOf = Object.getPrototypeOf;