@zenithbuild/core 0.1.0 → 0.3.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.
Files changed (90) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +24 -40
  3. package/bin/zen-build.ts +2 -0
  4. package/bin/zen-dev.ts +2 -0
  5. package/bin/zen-preview.ts +2 -0
  6. package/bin/zenith.ts +2 -0
  7. package/cli/commands/add.ts +37 -0
  8. package/cli/commands/build.ts +37 -0
  9. package/cli/commands/create.ts +702 -0
  10. package/cli/commands/dev.ts +197 -0
  11. package/cli/commands/index.ts +112 -0
  12. package/cli/commands/preview.ts +62 -0
  13. package/cli/commands/remove.ts +33 -0
  14. package/cli/index.ts +10 -0
  15. package/cli/main.ts +101 -0
  16. package/cli/utils/branding.ts +153 -0
  17. package/cli/utils/logger.ts +40 -0
  18. package/cli/utils/plugin-manager.ts +114 -0
  19. package/cli/utils/project.ts +71 -0
  20. package/compiler/build-analyzer.ts +122 -0
  21. package/compiler/discovery/layouts.ts +61 -0
  22. package/compiler/index.ts +40 -24
  23. package/compiler/ir/types.ts +1 -0
  24. package/compiler/parse/parseScript.ts +29 -5
  25. package/compiler/parse/parseTemplate.ts +96 -58
  26. package/compiler/parse/scriptAnalysis.ts +77 -0
  27. package/compiler/runtime/dataExposure.ts +49 -31
  28. package/compiler/runtime/generateDOM.ts +18 -17
  29. package/compiler/runtime/generateHydrationBundle.ts +24 -5
  30. package/compiler/runtime/transformIR.ts +140 -49
  31. package/compiler/runtime/wrapExpressionWithLoop.ts +11 -11
  32. package/compiler/spa-build.ts +70 -153
  33. package/compiler/ssg-build.ts +412 -0
  34. package/compiler/transform/layoutProcessor.ts +132 -0
  35. package/compiler/transform/transformNode.ts +19 -19
  36. package/dist/cli.js +11648 -0
  37. package/dist/zen-build.js +11659 -0
  38. package/dist/zen-dev.js +11659 -0
  39. package/dist/zen-preview.js +11659 -0
  40. package/dist/zenith.js +11659 -0
  41. package/package.json +22 -2
  42. package/runtime/bundle-generator.ts +416 -0
  43. package/runtime/client-runtime.ts +532 -0
  44. package/.eslintignore +0 -15
  45. package/.gitattributes +0 -2
  46. package/.github/ISSUE_TEMPLATE/compiler-errors-for-invalid-state-declarations.md +0 -25
  47. package/.github/ISSUE_TEMPLATE/new_ticket.yaml +0 -34
  48. package/.github/pull_request_template.md +0 -15
  49. package/.github/workflows/discord-changelog.yml +0 -141
  50. package/.github/workflows/discord-notify.yml +0 -242
  51. package/.github/workflows/discord-version.yml +0 -195
  52. package/.prettierignore +0 -13
  53. package/.prettierrc +0 -21
  54. package/.zen.d.ts +0 -15
  55. package/app/components/Button.zen +0 -46
  56. package/app/components/Link.zen +0 -11
  57. package/app/favicon.ico +0 -0
  58. package/app/layouts/Main.zen +0 -59
  59. package/app/pages/about.zen +0 -23
  60. package/app/pages/blog/[id].zen +0 -53
  61. package/app/pages/blog/index.zen +0 -32
  62. package/app/pages/dynamic-dx.zen +0 -712
  63. package/app/pages/dynamic-primitives.zen +0 -453
  64. package/app/pages/index.zen +0 -154
  65. package/app/pages/navigation-demo.zen +0 -229
  66. package/app/pages/posts/[...slug].zen +0 -61
  67. package/app/pages/primitives-demo.zen +0 -273
  68. package/assets/logos/0E3B5DDD-605C-4839-BB2E-DFCA8ADC9604.PNG +0 -0
  69. package/assets/logos/760971E5-79A1-44F9-90B9-925DF30F4278.PNG +0 -0
  70. package/assets/logos/8A06ED80-9ED2-4689-BCBD-13B2E95EE8E4.JPG +0 -0
  71. package/assets/logos/C691FF58-ED13-4E8D-B6A3-02E835849340.PNG +0 -0
  72. package/assets/logos/C691FF58-ED13-4E8D-B6A3-02E835849340.svg +0 -601
  73. package/assets/logos/README.md +0 -54
  74. package/assets/logos/zen.icns +0 -0
  75. package/bun.lock +0 -39
  76. package/compiler/legacy/binding.ts +0 -254
  77. package/compiler/legacy/bindings.ts +0 -338
  78. package/compiler/legacy/component-process.ts +0 -1208
  79. package/compiler/legacy/component.ts +0 -301
  80. package/compiler/legacy/event.ts +0 -50
  81. package/compiler/legacy/expression.ts +0 -1149
  82. package/compiler/legacy/mutation.ts +0 -280
  83. package/compiler/legacy/parse.ts +0 -299
  84. package/compiler/legacy/split.ts +0 -608
  85. package/compiler/legacy/types.ts +0 -32
  86. package/docs/COMMENTS.md +0 -111
  87. package/docs/COMMITS.md +0 -36
  88. package/docs/CONTRIBUTING.md +0 -116
  89. package/docs/STYLEGUIDE.md +0 -62
  90. package/scripts/webhook-proxy.ts +0 -213
@@ -0,0 +1,412 @@
1
+ /**
2
+ * Zenith SSG Build System
3
+ *
4
+ * SSG-first (Static Site Generation) build system that outputs:
5
+ * - Per-page HTML files: dist/{route}/index.html
6
+ * - Shared runtime: dist/assets/bundle.js
7
+ * - Global styles: dist/assets/styles.css
8
+ * - Page-specific JS only for pages needing hydration: dist/assets/page_{name}.js
9
+ *
10
+ * Static pages get pure HTML+CSS, no JavaScript.
11
+ * Hydrated pages reference the shared bundle.js and their page-specific JS.
12
+ */
13
+
14
+ import fs from "fs"
15
+ import path from "path"
16
+ import { compileZenSource } from "./index"
17
+ import { discoverLayouts } from "./discovery/layouts"
18
+ import { processLayout } from "./transform/layoutProcessor"
19
+ import { discoverPages, generateRouteDefinition } from "../router/manifest"
20
+ import { analyzePageSource, getAnalysisSummary, getBuildOutputType, type PageAnalysis } from "./build-analyzer"
21
+ import { generateBundleJS } from "../runtime/bundle-generator"
22
+
23
+ // ============================================
24
+ // Types
25
+ // ============================================
26
+
27
+ interface CompiledPage {
28
+ /** Route path like "/" or "/about" or "/blog/:id" */
29
+ routePath: string
30
+ /** Original file path */
31
+ filePath: string
32
+ /** Compiled HTML content */
33
+ html: string
34
+ /** Page-specific JavaScript (empty if static) */
35
+ pageScript: string
36
+ /** Page styles */
37
+ styles: string[]
38
+ /** Route score for matching priority */
39
+ score: number
40
+ /** Dynamic route parameter names */
41
+ paramNames: string[]
42
+ /** Build analysis result */
43
+ analysis: PageAnalysis
44
+ /** Output directory relative to dist/ */
45
+ outputDir: string
46
+ }
47
+
48
+ export interface SSGBuildOptions {
49
+ /** Pages directory (e.g., app/pages) */
50
+ pagesDir: string
51
+ /** Output directory (e.g., app/dist) */
52
+ outDir: string
53
+ /** Base directory for components/layouts (e.g., app/) */
54
+ baseDir?: string
55
+ /** Include source maps */
56
+ sourceMaps?: boolean
57
+ }
58
+
59
+ // ============================================
60
+ // Page Compilation
61
+ // ============================================
62
+
63
+ /**
64
+ * Compile a single page file for SSG output
65
+ */
66
+ function compilePage(
67
+ pagePath: string,
68
+ pagesDir: string,
69
+ baseDir: string = process.cwd()
70
+ ): CompiledPage {
71
+ const source = fs.readFileSync(pagePath, 'utf-8')
72
+
73
+ // Analyze page requirements
74
+ const analysis = analyzePageSource(source)
75
+
76
+ // Discover layouts
77
+ const layoutsDir = path.join(baseDir, 'layouts')
78
+ const layouts = discoverLayouts(layoutsDir)
79
+
80
+ // Process with layout if one is used
81
+ let processedSource = source
82
+ const layoutToUse = layouts.get('DefaultLayout')
83
+
84
+ if (layoutToUse) {
85
+ processedSource = processLayout(source, layoutToUse)
86
+ }
87
+
88
+ // Compile with new pipeline
89
+ const result = compileZenSource(processedSource, pagePath)
90
+
91
+ if (!result.finalized) {
92
+ throw new Error(`Compilation failed for ${pagePath}: No finalized output`)
93
+ }
94
+
95
+ // Extract compiled output
96
+ const html = result.finalized.html
97
+ const js = result.finalized.js || ''
98
+ const styles = result.finalized.styles || []
99
+
100
+ // Generate route definition
101
+ const routeDef = generateRouteDefinition(pagePath, pagesDir)
102
+
103
+ // Determine output directory from route path
104
+ // "/" -> "index", "/about" -> "about", "/blog/post" -> "blog/post"
105
+ let outputDir = routeDef.path === '/' ? 'index' : routeDef.path.replace(/^\//, '')
106
+
107
+ // Handle dynamic routes - they'll be placeholders for now
108
+ // [id] segments become _id_ for folder names
109
+ outputDir = outputDir.replace(/\[([^\]]+)\]/g, '_$1_')
110
+
111
+ return {
112
+ routePath: routeDef.path,
113
+ filePath: pagePath,
114
+ html,
115
+ pageScript: analysis.needsHydration ? js : '',
116
+ styles,
117
+ score: routeDef.score,
118
+ paramNames: routeDef.paramNames,
119
+ analysis,
120
+ outputDir
121
+ }
122
+ }
123
+
124
+ // ============================================
125
+ // HTML Generation
126
+ // ============================================
127
+
128
+ /**
129
+ * Generate the final HTML for a page
130
+ * Static pages: no JS references
131
+ * Hydrated pages: bundle.js + page-specific JS
132
+ */
133
+ function generatePageHTML(page: CompiledPage, globalStyles: string): string {
134
+ const { html, styles, analysis, routePath, pageScript } = page
135
+
136
+ // Combine styles
137
+ const pageStyles = styles.join('\n')
138
+ const allStyles = globalStyles + '\n' + pageStyles
139
+
140
+ // Build script tags only if needed
141
+ let scriptTags = ''
142
+ if (analysis.needsHydration) {
143
+ scriptTags = `
144
+ <script src="/assets/bundle.js"></script>`
145
+
146
+ if (pageScript) {
147
+ // Generate a safe filename from route path
148
+ const pageJsName = routePath === '/'
149
+ ? 'page_index.js'
150
+ : `page_${routePath.replace(/^\//, '').replace(/\//g, '_')}.js`
151
+ scriptTags += `
152
+ <script src="/assets/${pageJsName}"></script>`
153
+ }
154
+ }
155
+
156
+ // Check if HTML already has full document structure
157
+ const hasHtmlTag = /<html[^>]*>/i.test(html)
158
+
159
+ if (hasHtmlTag) {
160
+ // HTML already has structure from layout - inject styles and scripts
161
+ let finalHtml = html
162
+
163
+ // Inject styles into <head> if not already there
164
+ if (!/<style[^>]*>/.test(finalHtml)) {
165
+ finalHtml = finalHtml.replace(
166
+ '</head>',
167
+ ` <style>\n${allStyles}\n </style>\n</head>`
168
+ )
169
+ }
170
+
171
+ // Inject scripts before </body>
172
+ if (scriptTags) {
173
+ finalHtml = finalHtml.replace(
174
+ '</body>',
175
+ `${scriptTags}\n</body>`
176
+ )
177
+ }
178
+
179
+ return finalHtml
180
+ }
181
+
182
+ // Generate full HTML document for pages without layout
183
+ return `<!DOCTYPE html>
184
+ <html lang="en">
185
+ <head>
186
+ <meta charset="UTF-8">
187
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
188
+ <title>Zenith App</title>
189
+ <style>
190
+ ${allStyles}
191
+ </style>
192
+ </head>
193
+ <body>
194
+ ${html}${scriptTags}
195
+ </body>
196
+ </html>`
197
+ }
198
+
199
+ // ============================================
200
+ // Asset Generation
201
+ // ============================================
202
+
203
+ /**
204
+ * Generate page-specific JavaScript
205
+ */
206
+ function generatePageJS(page: CompiledPage): string {
207
+ if (!page.pageScript) return ''
208
+
209
+ // Wrap in IIFE for isolation
210
+ return `// Zenith Page: ${page.routePath}
211
+ (function() {
212
+ 'use strict';
213
+
214
+ ${page.pageScript}
215
+
216
+ // Trigger hydration after DOM is ready
217
+ if (document.readyState === 'loading') {
218
+ document.addEventListener('DOMContentLoaded', function() {
219
+ if (window.__zenith && window.__zenith.triggerMount) {
220
+ window.__zenith.triggerMount();
221
+ }
222
+ });
223
+ } else {
224
+ if (window.__zenith && window.__zenith.triggerMount) {
225
+ window.__zenith.triggerMount();
226
+ }
227
+ }
228
+ })();
229
+ `
230
+ }
231
+
232
+ // ============================================
233
+ // Main Build Function
234
+ // ============================================
235
+
236
+ /**
237
+ * Build all pages using SSG approach
238
+ */
239
+ export function buildSSG(options: SSGBuildOptions): void {
240
+ const { pagesDir, outDir, baseDir = path.dirname(pagesDir) } = options
241
+
242
+ console.log('🔨 Zenith SSG Build')
243
+ console.log(` Pages: ${pagesDir}`)
244
+ console.log(` Output: ${outDir}`)
245
+ console.log('')
246
+
247
+ // Clean and create output directory
248
+ if (fs.existsSync(outDir)) {
249
+ fs.rmSync(outDir, { recursive: true, force: true })
250
+ }
251
+ fs.mkdirSync(outDir, { recursive: true })
252
+ fs.mkdirSync(path.join(outDir, 'assets'), { recursive: true })
253
+
254
+ // Discover pages
255
+ const pageFiles = discoverPages(pagesDir)
256
+
257
+ if (pageFiles.length === 0) {
258
+ console.warn('⚠️ No pages found in', pagesDir)
259
+ return
260
+ }
261
+
262
+ console.log(`📄 Found ${pageFiles.length} page(s)`)
263
+
264
+ // Compile all pages
265
+ const compiledPages: CompiledPage[] = []
266
+ let hasHydratedPages = false
267
+
268
+ for (const pageFile of pageFiles) {
269
+ const relativePath = path.relative(pagesDir, pageFile)
270
+ console.log(` Compiling: ${relativePath}`)
271
+
272
+ try {
273
+ const compiled = compilePage(pageFile, pagesDir, baseDir)
274
+ compiledPages.push(compiled)
275
+
276
+ if (compiled.analysis.needsHydration) {
277
+ hasHydratedPages = true
278
+ }
279
+
280
+ const outputType = getBuildOutputType(compiled.analysis)
281
+ const summary = getAnalysisSummary(compiled.analysis)
282
+ console.log(` → ${outputType.toUpperCase()} [${summary}]`)
283
+ } catch (error: any) {
284
+ console.error(` ❌ Error: ${error.message}`)
285
+ throw error
286
+ }
287
+ }
288
+
289
+ console.log('')
290
+
291
+ // Load global styles
292
+ let globalStyles = ''
293
+ const globalCssPath = path.join(baseDir, 'styles', 'global.css')
294
+ if (fs.existsSync(globalCssPath)) {
295
+ globalStyles = fs.readFileSync(globalCssPath, 'utf-8')
296
+ console.log('📦 Loaded global.css')
297
+ }
298
+
299
+ // Write bundle.js if any pages need hydration
300
+ if (hasHydratedPages) {
301
+ const bundleJS = generateBundleJS()
302
+ fs.writeFileSync(path.join(outDir, 'assets', 'bundle.js'), bundleJS)
303
+ console.log('📦 Generated assets/bundle.js')
304
+ }
305
+
306
+ // Write global styles
307
+ if (globalStyles) {
308
+ fs.writeFileSync(path.join(outDir, 'assets', 'styles.css'), globalStyles)
309
+ console.log('📦 Generated assets/styles.css')
310
+ }
311
+
312
+ // Write each page
313
+ for (const page of compiledPages) {
314
+ // Create output directory
315
+ const pageOutDir = path.join(outDir, page.outputDir)
316
+ fs.mkdirSync(pageOutDir, { recursive: true })
317
+
318
+ // Generate and write HTML
319
+ const html = generatePageHTML(page, globalStyles)
320
+ fs.writeFileSync(path.join(pageOutDir, 'index.html'), html)
321
+
322
+ // Write page-specific JS if needed
323
+ if (page.pageScript) {
324
+ const pageJsName = page.routePath === '/'
325
+ ? 'page_index.js'
326
+ : `page_${page.routePath.replace(/^\//, '').replace(/\//g, '_')}.js`
327
+ const pageJS = generatePageJS(page)
328
+ fs.writeFileSync(path.join(outDir, 'assets', pageJsName), pageJS)
329
+ }
330
+
331
+ console.log(`✅ ${page.outputDir}/index.html`)
332
+ }
333
+
334
+ // Copy favicon if exists
335
+ const faviconPath = path.join(baseDir, 'favicon.ico')
336
+ if (fs.existsSync(faviconPath)) {
337
+ fs.copyFileSync(faviconPath, path.join(outDir, 'favicon.ico'))
338
+ console.log('📦 Copied favicon.ico')
339
+ }
340
+
341
+ // Generate 404 page
342
+ const custom404Candidates = ['404.zen', '+404.zen', 'not-found.zen']
343
+ let has404 = false
344
+
345
+ for (const candidate of custom404Candidates) {
346
+ const custom404Path = path.join(pagesDir, candidate)
347
+ if (fs.existsSync(custom404Path)) {
348
+ try {
349
+ const compiled = compilePage(custom404Path, pagesDir, baseDir)
350
+ const html = generatePageHTML(compiled, globalStyles)
351
+ fs.writeFileSync(path.join(outDir, '404.html'), html)
352
+ console.log('📦 Generated 404.html (custom)')
353
+ has404 = true
354
+ if (compiled.pageScript) {
355
+ const pageJS = generatePageJS(compiled)
356
+ fs.writeFileSync(path.join(outDir, 'assets', 'page_404.js'), pageJS)
357
+ }
358
+ } catch (error: any) {
359
+ console.warn(` ⚠️ Could not compile ${candidate}: ${error.message}`)
360
+ }
361
+ break
362
+ }
363
+ }
364
+
365
+ if (!has404) {
366
+ const default404HTML = `<!DOCTYPE html>
367
+ <html lang="en">
368
+ <head>
369
+ <meta charset="UTF-8">
370
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
371
+ <title>Page Not Found | Zenith</title>
372
+ <style>
373
+ * { box-sizing: border-box; margin: 0; padding: 0; }
374
+ body { font-family: system-ui, sans-serif; background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); color: #f1f5f9; min-height: 100vh; display: flex; align-items: center; justify-content: center; }
375
+ .container { text-align: center; padding: 2rem; }
376
+ .error-code { font-size: 8rem; font-weight: 800; background: linear-gradient(135deg, #3b82f6, #06b6d4); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; line-height: 1; margin-bottom: 1rem; }
377
+ h1 { font-size: 1.5rem; font-weight: 600; margin-bottom: 1rem; color: #e2e8f0; }
378
+ .message { color: #94a3b8; margin-bottom: 2rem; }
379
+ a { display: inline-block; background: linear-gradient(135deg, #3b82f6, #2563eb); color: white; text-decoration: none; padding: 0.75rem 1.5rem; border-radius: 8px; font-weight: 500; }
380
+ </style>
381
+ </head>
382
+ <body>
383
+ <div class="container">
384
+ <div class="error-code">404</div>
385
+ <h1>Page Not Found</h1>
386
+ <p class="message">The page you're looking for doesn't exist.</p>
387
+ <a href="/">← Go Home</a>
388
+ </div>
389
+ </body>
390
+ </html>`
391
+ fs.writeFileSync(path.join(outDir, '404.html'), default404HTML)
392
+ console.log('📦 Generated 404.html (default)')
393
+ }
394
+
395
+ // Summary
396
+ console.log('')
397
+ console.log('✨ Build complete!')
398
+ console.log(` Static pages: ${compiledPages.filter(p => p.analysis.isStatic).length}`)
399
+ console.log(` Hydrated pages: ${compiledPages.filter(p => p.analysis.needsHydration).length}`)
400
+ console.log(` SSR pages: ${compiledPages.filter(p => p.analysis.needsSSR).length}`)
401
+ console.log('')
402
+
403
+ // Route manifest
404
+ console.log('📍 Routes:')
405
+ for (const page of compiledPages.sort((a, b) => b.score - a.score)) {
406
+ const type = getBuildOutputType(page.analysis)
407
+ console.log(` ${page.routePath.padEnd(20)} → ${page.outputDir}/index.html (${type})`)
408
+ }
409
+ }
410
+
411
+ // Legacy export for backwards compatibility
412
+ export { buildSSG as buildSPA }
@@ -0,0 +1,132 @@
1
+ import type { LayoutMetadata } from '../discovery/layouts'
2
+
3
+ /**
4
+ * Process a page by inlining a layout
5
+ */
6
+ export function processLayout(
7
+ source: string,
8
+ layout: LayoutMetadata,
9
+ props: Record<string, any> = {}
10
+ ): string {
11
+ // 1. Extract scripts and styles from the page source
12
+ const pageScripts: string[] = []
13
+ const pageStyles: string[] = []
14
+ let isTypeScript = false
15
+
16
+ // Extract script blocks
17
+ const scriptRegex = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi
18
+ let scriptMatch
19
+ while ((scriptMatch = scriptRegex.exec(source)) !== null) {
20
+ const attrString = scriptMatch[1] || ''
21
+ const content = scriptMatch[2] || ''
22
+ if (attrString.includes('lang="ts"') || attrString.includes('setup="ts"')) {
23
+ isTypeScript = true
24
+ }
25
+ if (content) pageScripts.push(content.trim())
26
+ }
27
+
28
+ // Extract style blocks
29
+ const styleRegex = /<style[^>]*>([\s\S]*?)<\/style>/gi
30
+ let styleMatch
31
+ while ((styleMatch = styleRegex.exec(source)) !== null) {
32
+ if (styleMatch[1]) pageStyles.push(styleMatch[1].trim())
33
+ }
34
+
35
+ // 2. Extract content from page source and parse props
36
+ const layoutTag = layout.name
37
+ // Support both <DefaultLayout ...> and <DefaultLayout>...</DefaultLayout>
38
+ const layoutRegex = new RegExp(`<${layoutTag}\\b([^>]*)>(?:([\\s\\S]*?)</${layoutTag}>)?`, 'i')
39
+ const match = source.match(layoutRegex)
40
+
41
+ let pageHtml = ''
42
+ let layoutPropsStr = ''
43
+
44
+ if (match) {
45
+ layoutPropsStr = match[1] || ''
46
+ pageHtml = match[2] || ''
47
+
48
+ // If it's a self-closing tag or empty, it might not have captured content correctly if regex failed
49
+ if (!pageHtml && !source.includes(`</${layoutTag}>`)) {
50
+ // Self-closing check? No, Zenith usually expects explicit tags or the layout to wrap everything.
51
+ }
52
+ } else {
53
+ // If layout tag not found as root, assume everything minus script/style is content
54
+ pageHtml = source.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
55
+ pageHtml = pageHtml.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '').trim()
56
+ }
57
+
58
+ // 3. Parse props from the tag
59
+ const mergedProps = { ...props }
60
+ if (layoutPropsStr) {
61
+ // Support legacy props={{...}}
62
+ const legacyMatch = layoutPropsStr.match(/props=\{\{([^}]+)\}\}/)
63
+ if (legacyMatch && legacyMatch[1]) {
64
+ const propsBody = legacyMatch[1]
65
+ const pairs = propsBody.split(/,(?![^[]*\])(?![^{]*\})/)
66
+ for (const pair of pairs) {
67
+ const [key, ...valParts] = pair.split(':')
68
+ if (key && valParts.length > 0) {
69
+ mergedProps[key.trim()] = valParts.join(':').trim()
70
+ }
71
+ }
72
+ }
73
+
74
+ // Support natural props: title={"Home"} or title="Home" or title={title}
75
+ const attrRegex = /([a-zA-Z0-9-]+)=(?:\{([^}]+)\}|"([^"]*)"|'([^']*)')/g
76
+ let attrMatch
77
+ while ((attrMatch = attrRegex.exec(layoutPropsStr)) !== null) {
78
+ const name = attrMatch[1]
79
+ const value = attrMatch[2] || attrMatch[3] || attrMatch[4]
80
+ if (name && name !== 'props') {
81
+ mergedProps[name] = value
82
+ }
83
+ }
84
+ }
85
+
86
+ // 4. Merge Scripts with Prop Injection
87
+ // Layout scripts come first, then page scripts. Props are injected at the very top.
88
+ const propDeclarations = Object.entries(mergedProps)
89
+ .map(([key, value]) => {
90
+ // If value looks like a string literal, keep it as is, otherwise wrap if needed
91
+ // Actually, if it came from {expression}, we should treat it as code.
92
+ // If it came from "string", we treat it as a string.
93
+ const isExpression = layoutPropsStr.includes(`${key}={${value}}`)
94
+ if (isExpression) {
95
+ return `const ${key} = ${value};`
96
+ }
97
+ return `const ${key} = ${typeof value === 'string' && !value.startsWith("'") && !value.startsWith('"') ? `'${value}'` : value};`
98
+ })
99
+ .join('\n')
100
+
101
+ const mergedScripts = [
102
+ propDeclarations,
103
+ ...layout.scripts,
104
+ ...pageScripts
105
+ ].filter(Boolean).join('\n\n')
106
+
107
+ // 5. Merge Styles
108
+ const mergedStyles = [
109
+ ...layout.styles,
110
+ ...pageStyles
111
+ ].filter(Boolean).join('\n\n')
112
+
113
+ // 6. Inline HTML into layout slot
114
+ let finalizedHtml = layout.html.replace(/<Slot\s*\/>/gi, pageHtml)
115
+ finalizedHtml = finalizedHtml.replace(/<slot\s*>[\s\S]*?<\/slot>/gi, pageHtml)
116
+
117
+ // 7. Reconstruct the full .zen source
118
+ const propNames = Object.keys(mergedProps).join(',')
119
+ const scriptTag = `<script setup${isTypeScript ? '="ts"' : ''}${propNames ? ` props="${propNames}"` : ''}>`
120
+
121
+ return `
122
+ ${scriptTag}
123
+ ${mergedScripts}
124
+ </script>
125
+
126
+ ${finalizedHtml}
127
+
128
+ <style>
129
+ ${mergedStyles}
130
+ </style>
131
+ `.trim()
132
+ }
@@ -10,7 +10,7 @@ import type { Binding } from '../output/types'
10
10
  let bindingIdCounter = 0
11
11
 
12
12
  function generateBindingId(): string {
13
- return `exp_${bindingIdCounter++}`
13
+ return `expr_${bindingIdCounter++}`
14
14
  }
15
15
 
16
16
  /**
@@ -23,12 +23,12 @@ export function transformNode(
23
23
  parentLoopContext?: LoopContext // Phase 7: Loop context from parent map expressions
24
24
  ): { html: string; bindings: Binding[] } {
25
25
  const bindings: Binding[] = []
26
-
26
+
27
27
  function transform(node: TemplateNode, loopContext?: LoopContext): string {
28
28
  switch (node.type) {
29
29
  case 'text':
30
30
  return escapeHtml((node as TextNode).value)
31
-
31
+
32
32
  case 'expression': {
33
33
  const exprNode = node as ExpressionNode
34
34
  // Find the expression in the expressions array
@@ -36,11 +36,11 @@ export function transformNode(
36
36
  if (!expr) {
37
37
  throw new Error(`Expression ${exprNode.expression} not found`)
38
38
  }
39
-
40
- const bindingId = generateBindingId()
39
+
40
+ const bindingId = expr.id
41
41
  // Phase 7: Use loop context from ExpressionNode if available, otherwise use passed context
42
42
  const activeLoopContext = exprNode.loopContext || loopContext
43
-
43
+
44
44
  bindings.push({
45
45
  id: bindingId,
46
46
  type: 'text',
@@ -49,14 +49,14 @@ export function transformNode(
49
49
  location: expr.location,
50
50
  loopContext: activeLoopContext // Phase 7: Attach loop context to binding
51
51
  })
52
-
52
+
53
53
  return `<span data-zen-text="${bindingId}"></span>`
54
54
  }
55
-
55
+
56
56
  case 'element': {
57
57
  const elNode = node as ElementNode
58
58
  const tag = elNode.tag
59
-
59
+
60
60
  // Build attributes
61
61
  const attrs: string[] = []
62
62
  for (const attr of elNode.attributes) {
@@ -67,10 +67,10 @@ export function transformNode(
67
67
  } else {
68
68
  // Expression attribute
69
69
  const expr = attr.value as ExpressionIR
70
- const bindingId = generateBindingId()
70
+ const bindingId = expr.id
71
71
  // Phase 7: Use loop context from AttributeIR if available, otherwise use element's loop context
72
72
  const activeLoopContext = attr.loopContext || loopContext
73
-
73
+
74
74
  bindings.push({
75
75
  id: bindingId,
76
76
  type: 'attribute',
@@ -79,35 +79,35 @@ export function transformNode(
79
79
  location: expr.location,
80
80
  loopContext: activeLoopContext // Phase 7: Attach loop context to binding
81
81
  })
82
-
82
+
83
83
  // Use data-zen-attr-{name} for attribute expressions
84
84
  attrs.push(`data-zen-attr-${attr.name}="${bindingId}"`)
85
85
  }
86
86
  }
87
-
87
+
88
88
  const attrStr = attrs.length > 0 ? ' ' + attrs.join(' ') : ''
89
-
89
+
90
90
  // Phase 7: Use loop context from ElementNode if available, otherwise use passed context
91
91
  const activeLoopContext = elNode.loopContext || loopContext
92
-
92
+
93
93
  // Transform children
94
94
  const childrenHtml = elNode.children.map(child => transform(child, activeLoopContext)).join('')
95
-
95
+
96
96
  // Self-closing tags
97
97
  const voidElements = new Set([
98
98
  'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
99
99
  'link', 'meta', 'param', 'source', 'track', 'wbr'
100
100
  ])
101
-
101
+
102
102
  if (voidElements.has(tag.toLowerCase()) && childrenHtml === '') {
103
103
  return `<${tag}${attrStr} />`
104
104
  }
105
-
105
+
106
106
  return `<${tag}${attrStr}>${childrenHtml}</${tag}>`
107
107
  }
108
108
  }
109
109
  }
110
-
110
+
111
111
  const html = transform(node, parentLoopContext)
112
112
  return { html, bindings }
113
113
  }