@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.
- package/LICENSE +1 -1
- package/README.md +24 -40
- package/bin/zen-build.ts +2 -0
- package/bin/zen-dev.ts +2 -0
- package/bin/zen-preview.ts +2 -0
- package/bin/zenith.ts +2 -0
- package/cli/commands/add.ts +37 -0
- package/cli/commands/build.ts +37 -0
- package/cli/commands/create.ts +702 -0
- package/cli/commands/dev.ts +197 -0
- package/cli/commands/index.ts +112 -0
- package/cli/commands/preview.ts +62 -0
- package/cli/commands/remove.ts +33 -0
- package/cli/index.ts +10 -0
- package/cli/main.ts +101 -0
- package/cli/utils/branding.ts +153 -0
- package/cli/utils/logger.ts +40 -0
- package/cli/utils/plugin-manager.ts +114 -0
- package/cli/utils/project.ts +71 -0
- package/compiler/build-analyzer.ts +122 -0
- package/compiler/discovery/layouts.ts +61 -0
- package/compiler/index.ts +40 -24
- package/compiler/ir/types.ts +1 -0
- package/compiler/parse/parseScript.ts +29 -5
- package/compiler/parse/parseTemplate.ts +96 -58
- package/compiler/parse/scriptAnalysis.ts +77 -0
- package/compiler/runtime/dataExposure.ts +49 -31
- package/compiler/runtime/generateDOM.ts +18 -17
- package/compiler/runtime/generateHydrationBundle.ts +24 -5
- package/compiler/runtime/transformIR.ts +140 -49
- package/compiler/runtime/wrapExpressionWithLoop.ts +11 -11
- package/compiler/spa-build.ts +70 -153
- package/compiler/ssg-build.ts +412 -0
- package/compiler/transform/layoutProcessor.ts +132 -0
- package/compiler/transform/transformNode.ts +19 -19
- package/dist/cli.js +11648 -0
- package/dist/zen-build.js +11659 -0
- package/dist/zen-dev.js +11659 -0
- package/dist/zen-preview.js +11659 -0
- package/dist/zenith.js +11659 -0
- package/package.json +22 -2
- package/runtime/bundle-generator.ts +416 -0
- package/runtime/client-runtime.ts +532 -0
- package/.eslintignore +0 -15
- package/.gitattributes +0 -2
- package/.github/ISSUE_TEMPLATE/compiler-errors-for-invalid-state-declarations.md +0 -25
- package/.github/ISSUE_TEMPLATE/new_ticket.yaml +0 -34
- package/.github/pull_request_template.md +0 -15
- package/.github/workflows/discord-changelog.yml +0 -141
- package/.github/workflows/discord-notify.yml +0 -242
- package/.github/workflows/discord-version.yml +0 -195
- package/.prettierignore +0 -13
- package/.prettierrc +0 -21
- package/.zen.d.ts +0 -15
- package/app/components/Button.zen +0 -46
- package/app/components/Link.zen +0 -11
- package/app/favicon.ico +0 -0
- package/app/layouts/Main.zen +0 -59
- package/app/pages/about.zen +0 -23
- package/app/pages/blog/[id].zen +0 -53
- package/app/pages/blog/index.zen +0 -32
- package/app/pages/dynamic-dx.zen +0 -712
- package/app/pages/dynamic-primitives.zen +0 -453
- package/app/pages/index.zen +0 -154
- package/app/pages/navigation-demo.zen +0 -229
- package/app/pages/posts/[...slug].zen +0 -61
- package/app/pages/primitives-demo.zen +0 -273
- package/assets/logos/0E3B5DDD-605C-4839-BB2E-DFCA8ADC9604.PNG +0 -0
- package/assets/logos/760971E5-79A1-44F9-90B9-925DF30F4278.PNG +0 -0
- package/assets/logos/8A06ED80-9ED2-4689-BCBD-13B2E95EE8E4.JPG +0 -0
- package/assets/logos/C691FF58-ED13-4E8D-B6A3-02E835849340.PNG +0 -0
- package/assets/logos/C691FF58-ED13-4E8D-B6A3-02E835849340.svg +0 -601
- package/assets/logos/README.md +0 -54
- package/assets/logos/zen.icns +0 -0
- package/bun.lock +0 -39
- package/compiler/legacy/binding.ts +0 -254
- package/compiler/legacy/bindings.ts +0 -338
- package/compiler/legacy/component-process.ts +0 -1208
- package/compiler/legacy/component.ts +0 -301
- package/compiler/legacy/event.ts +0 -50
- package/compiler/legacy/expression.ts +0 -1149
- package/compiler/legacy/mutation.ts +0 -280
- package/compiler/legacy/parse.ts +0 -299
- package/compiler/legacy/split.ts +0 -608
- package/compiler/legacy/types.ts +0 -32
- package/docs/COMMENTS.md +0 -111
- package/docs/COMMITS.md +0 -36
- package/docs/CONTRIBUTING.md +0 -116
- package/docs/STYLEGUIDE.md +0 -62
- 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 `
|
|
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 =
|
|
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 =
|
|
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
|
}
|