@zenithbuild/core 0.4.6 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/commands/dev.ts +58 -6
- package/compiler/discovery/componentDiscovery.ts +4 -0
- package/compiler/discovery/layouts.ts +11 -2
- package/compiler/finalize/finalizeOutput.ts +6 -6
- package/compiler/index.ts +9 -7
- package/compiler/ir/types.ts +24 -0
- package/compiler/parse/parseTemplate.ts +10 -2
- package/compiler/parse/scriptAnalysis.ts +8 -0
- package/compiler/runtime/transformIR.ts +32 -5
- package/compiler/spa-build.ts +5 -5
- package/compiler/ssg-build.ts +6 -6
- package/compiler/transform/componentResolver.ts +36 -13
- package/compiler/transform/componentScriptTransformer.ts +283 -0
- package/dist/cli.js +10 -0
- package/dist/zen-build.js +474 -50
- package/dist/zen-dev.js +474 -50
- package/dist/zen-preview.js +474 -50
- package/dist/zenith.js +474 -50
- package/package.json +4 -2
- package/runtime/bundle-generator.ts +144 -1
package/cli/commands/dev.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'path'
|
|
2
2
|
import fs from 'fs'
|
|
3
|
+
import os from 'os'
|
|
3
4
|
import { serve, type ServerWebSocket } from 'bun'
|
|
4
5
|
import { requireProject } from '../utils/project'
|
|
5
6
|
import * as logger from '../utils/logger'
|
|
@@ -29,6 +30,53 @@ interface CompiledPage {
|
|
|
29
30
|
|
|
30
31
|
const pageCache = new Map<string, CompiledPage>()
|
|
31
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Bundle page script using Bun's bundler to resolve npm imports at compile time.
|
|
35
|
+
* This allows ES module imports like `import { gsap } from 'gsap'` to work.
|
|
36
|
+
*/
|
|
37
|
+
async function bundlePageScript(script: string, projectRoot: string): Promise<string> {
|
|
38
|
+
// If no import statements, return as-is
|
|
39
|
+
if (!script.includes('import ')) {
|
|
40
|
+
return script
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Create a temporary file for bundling
|
|
44
|
+
const tempDir = os.tmpdir()
|
|
45
|
+
const tempFile = path.join(tempDir, `zenith-bundle-${Date.now()}.js`)
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
// Write script to temp file
|
|
49
|
+
fs.writeFileSync(tempFile, script, 'utf-8')
|
|
50
|
+
|
|
51
|
+
// Use Bun.build to bundle with npm resolution
|
|
52
|
+
const result = await Bun.build({
|
|
53
|
+
entrypoints: [tempFile],
|
|
54
|
+
target: 'browser',
|
|
55
|
+
format: 'esm',
|
|
56
|
+
minify: false,
|
|
57
|
+
// Resolve modules from the project's node_modules
|
|
58
|
+
external: [], // Bundle everything
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
if (!result.success || !result.outputs[0]) {
|
|
62
|
+
console.error('[Zenith] Bundle errors:', result.logs)
|
|
63
|
+
return script // Fall back to original
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Get the bundled output
|
|
67
|
+
const bundledCode = await result.outputs[0].text()
|
|
68
|
+
return bundledCode
|
|
69
|
+
} catch (error: any) {
|
|
70
|
+
console.error('[Zenith] Failed to bundle page script:', error.message)
|
|
71
|
+
return script // Fall back to original
|
|
72
|
+
} finally {
|
|
73
|
+
// Clean up temp file
|
|
74
|
+
try {
|
|
75
|
+
fs.unlinkSync(tempFile)
|
|
76
|
+
} catch { }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
32
80
|
export async function dev(options: DevOptions = {}): Promise<void> {
|
|
33
81
|
const project = requireProject()
|
|
34
82
|
const port = options.port || parseInt(process.env.PORT || '3000', 10)
|
|
@@ -105,7 +153,7 @@ export async function dev(options: DevOptions = {}): Promise<void> {
|
|
|
105
153
|
/**
|
|
106
154
|
* Compile a .zen page in memory
|
|
107
155
|
*/
|
|
108
|
-
function compilePageInMemory(pagePath: string): CompiledPage | null {
|
|
156
|
+
async function compilePageInMemory(pagePath: string): Promise<CompiledPage | null> {
|
|
109
157
|
try {
|
|
110
158
|
const layoutsDir = path.join(pagesDir, '../layouts')
|
|
111
159
|
const componentsDir = path.join(pagesDir, '../components')
|
|
@@ -117,16 +165,19 @@ export async function dev(options: DevOptions = {}): Promise<void> {
|
|
|
117
165
|
|
|
118
166
|
if (layoutToUse) processedSource = processLayout(source, layoutToUse)
|
|
119
167
|
|
|
120
|
-
const result = compileZenSource(processedSource, pagePath, {
|
|
168
|
+
const result = await compileZenSource(processedSource, pagePath, {
|
|
121
169
|
componentsDir: fs.existsSync(componentsDir) ? componentsDir : undefined
|
|
122
170
|
})
|
|
123
171
|
if (!result.finalized) throw new Error('Compilation failed')
|
|
124
172
|
|
|
125
173
|
const routeDef = generateRouteDefinition(pagePath, pagesDir)
|
|
126
174
|
|
|
175
|
+
// Bundle the script to resolve npm imports at compile time
|
|
176
|
+
const bundledScript = await bundlePageScript(result.finalized.js, rootDir)
|
|
177
|
+
|
|
127
178
|
return {
|
|
128
179
|
html: result.finalized.html,
|
|
129
|
-
script:
|
|
180
|
+
script: bundledScript,
|
|
130
181
|
styles: result.finalized.styles,
|
|
131
182
|
route: routeDef.path,
|
|
132
183
|
lastModified: Date.now()
|
|
@@ -189,7 +240,7 @@ export async function dev(options: DevOptions = {}): Promise<void> {
|
|
|
189
240
|
|
|
190
241
|
const server = serve({
|
|
191
242
|
port,
|
|
192
|
-
fetch(req, server) {
|
|
243
|
+
async fetch(req, server) {
|
|
193
244
|
const startTime = performance.now()
|
|
194
245
|
const url = new URL(req.url)
|
|
195
246
|
const pathname = url.pathname
|
|
@@ -246,7 +297,7 @@ export async function dev(options: DevOptions = {}): Promise<void> {
|
|
|
246
297
|
const stat = fs.statSync(pagePath)
|
|
247
298
|
|
|
248
299
|
if (!cached || stat.mtimeMs > cached.lastModified) {
|
|
249
|
-
cached = compilePageInMemory(pagePath) || undefined
|
|
300
|
+
cached = await compilePageInMemory(pagePath) || undefined
|
|
250
301
|
if (cached) pageCache.set(pagePath, cached)
|
|
251
302
|
}
|
|
252
303
|
const compileEnd = performance.now()
|
|
@@ -327,7 +378,8 @@ function generateDevHTML(page: CompiledPage, contentData: any = {}): string {
|
|
|
327
378
|
// Escape </script> sequences in JSON content to prevent breaking the script tag
|
|
328
379
|
const contentJson = JSON.stringify(contentData).replace(/<\//g, '<\\/')
|
|
329
380
|
const contentTag = `<script>window.__ZENITH_CONTENT__ = ${contentJson};</script>`
|
|
330
|
-
|
|
381
|
+
// Use type="module" to support ES6 imports from npm packages
|
|
382
|
+
const scriptTag = `<script type="module">\n${page.script}\n</script>`
|
|
331
383
|
const allScripts = `${runtimeTag}\n${contentTag}\n${scriptTag}`
|
|
332
384
|
return page.html.includes('</body>')
|
|
333
385
|
? page.html.replace('</body>', `${allScripts}\n</body>`)
|
|
@@ -26,6 +26,8 @@ export interface ComponentMetadata {
|
|
|
26
26
|
slots: SlotDefinition[]
|
|
27
27
|
props: string[] // Declared props
|
|
28
28
|
styles: string[] // Raw CSS from <style> blocks
|
|
29
|
+
script: string | null // Raw script content for bundling
|
|
30
|
+
scriptAttributes: Record<string, string> | null // Script attributes (setup, lang)
|
|
29
31
|
hasScript: boolean
|
|
30
32
|
hasStyles: boolean
|
|
31
33
|
}
|
|
@@ -112,6 +114,8 @@ function parseComponentFile(filePath: string): ComponentMetadata | null {
|
|
|
112
114
|
slots,
|
|
113
115
|
props,
|
|
114
116
|
styles,
|
|
117
|
+
script: ir.script?.raw || null, // Store raw script content
|
|
118
|
+
scriptAttributes: ir.script?.attributes || null, // Store script attributes
|
|
115
119
|
hasScript: ir.script !== null,
|
|
116
120
|
hasStyles: ir.styles.length > 0
|
|
117
121
|
}
|
|
@@ -41,8 +41,17 @@ export function discoverLayouts(layoutsDir: string): Map<string, LayoutMetadata>
|
|
|
41
41
|
if (match[1]) styles.push(match[1].trim())
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
// Extract HTML (everything except
|
|
45
|
-
|
|
44
|
+
// Extract HTML (everything except inline scripts/style)
|
|
45
|
+
// Preserve external script tags (<script src="...">) but remove inline <script setup> blocks
|
|
46
|
+
// Use a function-based replace to check for src attribute
|
|
47
|
+
let html = source.replace(/<script([^>]*)>([\s\S]*?)<\/script>/gi, (match, attrs, content) => {
|
|
48
|
+
// Keep script tags with src attribute (external scripts)
|
|
49
|
+
if (attrs.includes('src=')) {
|
|
50
|
+
return match;
|
|
51
|
+
}
|
|
52
|
+
// Remove inline scripts (those without src)
|
|
53
|
+
return '';
|
|
54
|
+
})
|
|
46
55
|
html = html.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '').trim()
|
|
47
56
|
|
|
48
57
|
layouts.set(name, {
|
|
@@ -39,10 +39,10 @@ export interface FinalizedOutput {
|
|
|
39
39
|
* @param compiled - Compiled template from Phase 2
|
|
40
40
|
* @returns Finalized output
|
|
41
41
|
*/
|
|
42
|
-
export function finalizeOutput(
|
|
42
|
+
export async function finalizeOutput(
|
|
43
43
|
ir: ZenIR,
|
|
44
44
|
compiled: CompiledTemplate
|
|
45
|
-
): FinalizedOutput {
|
|
45
|
+
): Promise<FinalizedOutput> {
|
|
46
46
|
const errors: string[] = []
|
|
47
47
|
|
|
48
48
|
// 1. Validate all expressions (Phase 8/9/10 requirement)
|
|
@@ -77,7 +77,7 @@ export function finalizeOutput(
|
|
|
77
77
|
// 3. Generate runtime code
|
|
78
78
|
let runtimeCode: RuntimeCode
|
|
79
79
|
try {
|
|
80
|
-
runtimeCode = transformIR(ir)
|
|
80
|
+
runtimeCode = await transformIR(ir)
|
|
81
81
|
} catch (error: any) {
|
|
82
82
|
errors.push(`Runtime generation failed: ${error.message}`)
|
|
83
83
|
return {
|
|
@@ -176,11 +176,11 @@ function verifyNoRawExpressions(html: string, filePath: string): string[] {
|
|
|
176
176
|
*
|
|
177
177
|
* Throws if validation fails (build must fail on errors)
|
|
178
178
|
*/
|
|
179
|
-
export function finalizeOutputOrThrow(
|
|
179
|
+
export async function finalizeOutputOrThrow(
|
|
180
180
|
ir: ZenIR,
|
|
181
181
|
compiled: CompiledTemplate
|
|
182
|
-
): FinalizedOutput {
|
|
183
|
-
const output = finalizeOutput(ir, compiled)
|
|
182
|
+
): Promise<FinalizedOutput> {
|
|
183
|
+
const output = await finalizeOutput(ir, compiled)
|
|
184
184
|
|
|
185
185
|
if (output.hasErrors) {
|
|
186
186
|
const errorMessage = output.errors.join('\n\n')
|
package/compiler/index.ts
CHANGED
|
@@ -12,11 +12,11 @@ import type { FinalizedOutput } from './finalize/finalizeOutput'
|
|
|
12
12
|
/**
|
|
13
13
|
* Compile a .zen file into IR and CompiledTemplate
|
|
14
14
|
*/
|
|
15
|
-
export function compileZen(filePath: string): {
|
|
15
|
+
export async function compileZen(filePath: string): Promise<{
|
|
16
16
|
ir: ZenIR
|
|
17
17
|
compiled: CompiledTemplate
|
|
18
18
|
finalized?: FinalizedOutput
|
|
19
|
-
} {
|
|
19
|
+
}> {
|
|
20
20
|
const source = readFileSync(filePath, 'utf-8')
|
|
21
21
|
return compileZenSource(source, filePath)
|
|
22
22
|
}
|
|
@@ -24,17 +24,17 @@ export function compileZen(filePath: string): {
|
|
|
24
24
|
/**
|
|
25
25
|
* Compile Zen source string into IR and CompiledTemplate
|
|
26
26
|
*/
|
|
27
|
-
export function compileZenSource(
|
|
27
|
+
export async function compileZenSource(
|
|
28
28
|
source: string,
|
|
29
29
|
filePath: string,
|
|
30
30
|
options?: {
|
|
31
31
|
componentsDir?: string
|
|
32
32
|
}
|
|
33
|
-
): {
|
|
33
|
+
): Promise<{
|
|
34
34
|
ir: ZenIR
|
|
35
35
|
compiled: CompiledTemplate
|
|
36
36
|
finalized?: FinalizedOutput
|
|
37
|
-
} {
|
|
37
|
+
}> {
|
|
38
38
|
// Parse template
|
|
39
39
|
const template = parseTemplate(source, filePath)
|
|
40
40
|
|
|
@@ -42,7 +42,7 @@ export function compileZenSource(
|
|
|
42
42
|
const script = parseScript(source)
|
|
43
43
|
|
|
44
44
|
// Parse styles
|
|
45
|
-
const styleRegex =
|
|
45
|
+
const styleRegex = /<style[^>]*>([\s\S]*?)<\/style>/gi
|
|
46
46
|
const styles: StyleIR[] = []
|
|
47
47
|
let match
|
|
48
48
|
while ((match = styleRegex.exec(source)) !== null) {
|
|
@@ -53,6 +53,7 @@ export function compileZenSource(
|
|
|
53
53
|
filePath,
|
|
54
54
|
template,
|
|
55
55
|
script,
|
|
56
|
+
// componentScripts: [],
|
|
56
57
|
styles
|
|
57
58
|
}
|
|
58
59
|
|
|
@@ -73,9 +74,10 @@ export function compileZenSource(
|
|
|
73
74
|
const compiled = transformTemplate(ir)
|
|
74
75
|
|
|
75
76
|
try {
|
|
76
|
-
const finalized = finalizeOutputOrThrow(ir, compiled)
|
|
77
|
+
const finalized = await finalizeOutputOrThrow(ir, compiled)
|
|
77
78
|
return { ir, compiled, finalized }
|
|
78
79
|
} catch (error: any) {
|
|
79
80
|
throw new Error(`Failed to finalize output for ${filePath}:\\n${error.message}`)
|
|
80
81
|
}
|
|
81
82
|
}
|
|
83
|
+
|
package/compiler/ir/types.ts
CHANGED
|
@@ -6,11 +6,35 @@
|
|
|
6
6
|
* without any runtime execution or transformation.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Structured ES module import metadata
|
|
11
|
+
* Parsed from component scripts, used for deterministic bundling
|
|
12
|
+
*/
|
|
13
|
+
export interface ScriptImport {
|
|
14
|
+
source: string // Module specifier, e.g. 'gsap'
|
|
15
|
+
specifiers: string // Import clause, e.g. '{ gsap }' or 'gsap' or ''
|
|
16
|
+
typeOnly: boolean // TypeScript type-only import
|
|
17
|
+
sideEffect: boolean // Side-effect import (no specifiers)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Component Script IR - represents a component's script block
|
|
22
|
+
* Used for collecting and bundling component scripts
|
|
23
|
+
*/
|
|
24
|
+
export type ComponentScriptIR = {
|
|
25
|
+
name: string // Component name (e.g., 'HeroSection')
|
|
26
|
+
script: string // Raw script content
|
|
27
|
+
props: string[] // Declared props
|
|
28
|
+
scriptAttributes: Record<string, string> // Script attributes (setup, lang)
|
|
29
|
+
imports: ScriptImport[] // Parsed npm imports for bundling
|
|
30
|
+
}
|
|
31
|
+
|
|
9
32
|
export type ZenIR = {
|
|
10
33
|
filePath: string
|
|
11
34
|
template: TemplateIR
|
|
12
35
|
script: ScriptIR | null
|
|
13
36
|
styles: StyleIR[]
|
|
37
|
+
componentScripts?: ComponentScriptIR[] // Scripts from used components
|
|
14
38
|
}
|
|
15
39
|
|
|
16
40
|
export type TemplateIR = {
|
|
@@ -22,10 +22,18 @@ function generateExpressionId(): string {
|
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Strip script and style blocks from HTML before parsing
|
|
25
|
+
* Preserves external script tags (<script src="...">) but removes inline scripts
|
|
25
26
|
*/
|
|
26
27
|
function stripBlocks(html: string): string {
|
|
27
|
-
// Remove script blocks
|
|
28
|
-
let stripped = html.replace(/<script[^>]
|
|
28
|
+
// Remove only inline script blocks (those WITHOUT src attribute), preserve external scripts
|
|
29
|
+
let stripped = html.replace(/<script([^>]*)>([\s\S]*?)<\/script>/gi, (match, attrs, content) => {
|
|
30
|
+
// Keep script tags with src attribute (external scripts)
|
|
31
|
+
if (attrs.includes('src=')) {
|
|
32
|
+
return match;
|
|
33
|
+
}
|
|
34
|
+
// Remove inline scripts (those without src)
|
|
35
|
+
return '';
|
|
36
|
+
})
|
|
29
37
|
// Remove style blocks
|
|
30
38
|
stripped = stripped.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
|
31
39
|
return stripped
|
|
@@ -45,6 +45,7 @@ export function extractProps(script: string): string[] {
|
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
47
|
* Transform script by removing state and prop declarations
|
|
48
|
+
* Also strips .zen imports (resolved at compile time) and other compile-time-only imports
|
|
48
49
|
*/
|
|
49
50
|
export function transformStateDeclarations(script: string): string {
|
|
50
51
|
let transformed = script
|
|
@@ -62,6 +63,13 @@ export function transformStateDeclarations(script: string): string {
|
|
|
62
63
|
// Remove zenith/runtime imports
|
|
63
64
|
transformed = transformed.replace(/import\s+{[^}]+}\s+from\s+['"]zenith\/runtime['"]\s*;?[ \t]*/g, '')
|
|
64
65
|
|
|
66
|
+
// Remove .zen file imports (resolved at compile time)
|
|
67
|
+
// Matches: import Name from '.../file.zen'; or import Name from '.../file.zen'
|
|
68
|
+
transformed = transformed.replace(/import\s+\w+\s+from\s+['"][^'"]*\.zen['"];?\s*/g, '')
|
|
69
|
+
|
|
70
|
+
// Remove relative imports with destructuring (components are inlined)
|
|
71
|
+
transformed = transformed.replace(/import\s+{[^}]*}\s+from\s+['"][^'"]+\.zen['"];?\s*/g, '')
|
|
72
|
+
|
|
65
73
|
// Transform zenith:content imports to global lookups
|
|
66
74
|
transformed = transformed.replace(
|
|
67
75
|
/import\s*{\s*([^}]+)\s*}\s*from\s*['"]zenith:content['"]\s*;?/g,
|
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
* Phase 4: Transform ZenIR into runtime-ready JavaScript code with full reactivity
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { ZenIR } from '../ir/types'
|
|
7
|
+
import type { ZenIR, ScriptImport } from '../ir/types'
|
|
8
8
|
import { generateExpressionWrappers } from './wrapExpression'
|
|
9
9
|
import { generateDOMFunction } from './generateDOM'
|
|
10
10
|
import { generateHydrationRuntime, generateExpressionRegistry } from './generateHydrationBundle'
|
|
11
11
|
import { analyzeAllExpressions } from './dataExposure'
|
|
12
12
|
import { generateNavigationRuntime } from './navigation'
|
|
13
13
|
import { extractStateDeclarations, extractProps, transformStateDeclarations } from '../parse/scriptAnalysis'
|
|
14
|
+
import { transformAllComponentScripts, emitImports } from '../transform/componentScriptTransformer'
|
|
14
15
|
|
|
15
16
|
export interface RuntimeCode {
|
|
16
17
|
expressions: string // Expression wrapper functions
|
|
@@ -25,7 +26,7 @@ export interface RuntimeCode {
|
|
|
25
26
|
/**
|
|
26
27
|
* Transform ZenIR into runtime JavaScript code
|
|
27
28
|
*/
|
|
28
|
-
export function transformIR(ir: ZenIR): RuntimeCode {
|
|
29
|
+
export async function transformIR(ir: ZenIR): Promise<RuntimeCode> {
|
|
29
30
|
// Phase 6: Analyze expression dependencies for explicit data exposure
|
|
30
31
|
const expressionDependencies = analyzeAllExpressions(
|
|
31
32
|
ir.template.expressions,
|
|
@@ -70,6 +71,9 @@ export function transformIR(ir: ZenIR): RuntimeCode {
|
|
|
70
71
|
// Transform script (remove state and prop declarations, they're handled by runtime)
|
|
71
72
|
const scriptCode = transformStateDeclarations(scriptContent)
|
|
72
73
|
|
|
74
|
+
// Transform component scripts for instance-scoped execution (async)
|
|
75
|
+
const componentScriptResult = await transformAllComponentScripts(ir.componentScripts || [])
|
|
76
|
+
|
|
73
77
|
// Generate complete runtime bundle
|
|
74
78
|
const bundle = generateRuntimeBundle({
|
|
75
79
|
expressions,
|
|
@@ -78,7 +82,9 @@ export function transformIR(ir: ZenIR): RuntimeCode {
|
|
|
78
82
|
navigationRuntime,
|
|
79
83
|
stylesCode,
|
|
80
84
|
scriptCode,
|
|
81
|
-
stateInitCode
|
|
85
|
+
stateInitCode,
|
|
86
|
+
componentScriptCode: componentScriptResult.code,
|
|
87
|
+
npmImports: componentScriptResult.imports
|
|
82
88
|
})
|
|
83
89
|
|
|
84
90
|
return {
|
|
@@ -103,14 +109,21 @@ function generateRuntimeBundle(parts: {
|
|
|
103
109
|
stylesCode: string
|
|
104
110
|
scriptCode: string
|
|
105
111
|
stateInitCode: string
|
|
112
|
+
componentScriptCode: string // Component factories
|
|
113
|
+
npmImports: ScriptImport[] // Structured npm imports from component scripts
|
|
106
114
|
}): string {
|
|
107
115
|
// Extract function declarations from script code to register on window
|
|
108
116
|
const functionRegistrations = extractFunctionRegistrations(parts.scriptCode)
|
|
109
117
|
|
|
118
|
+
// Generate npm imports header (hoisted, deduplicated, deterministic)
|
|
119
|
+
const npmImportsHeader = parts.npmImports.length > 0
|
|
120
|
+
? `// NPM Imports (hoisted from component scripts)\n${emitImports(parts.npmImports)}\n\n`
|
|
121
|
+
: ''
|
|
122
|
+
|
|
110
123
|
return `// Zenith Runtime Bundle (Phase 5)
|
|
111
124
|
// Generated at compile time - no .zen parsing in browser
|
|
112
125
|
|
|
113
|
-
${parts.expressions}
|
|
126
|
+
${npmImportsHeader}${parts.expressions}
|
|
114
127
|
|
|
115
128
|
${parts.expressionRegistry}
|
|
116
129
|
|
|
@@ -129,6 +142,9 @@ ${functionRegistrations}
|
|
|
129
142
|
${parts.stateInitCode ? `// State initialization
|
|
130
143
|
${parts.stateInitCode}` : ''}
|
|
131
144
|
|
|
145
|
+
${parts.componentScriptCode ? `// Component factories (instance-scoped)
|
|
146
|
+
${parts.componentScriptCode}` : ''}
|
|
147
|
+
|
|
132
148
|
// Export hydration functions
|
|
133
149
|
if (typeof window !== 'undefined') {
|
|
134
150
|
window.zenithHydrate = window.__zenith_hydrate || function(state, container) {
|
|
@@ -187,10 +203,21 @@ if (typeof window !== 'undefined') {
|
|
|
187
203
|
// Get the router outlet or body
|
|
188
204
|
const container = document.querySelector('#app') || document.body;
|
|
189
205
|
|
|
190
|
-
// Hydrate with state
|
|
206
|
+
// Hydrate with state (expressions, bindings)
|
|
191
207
|
if (window.__zenith_hydrate) {
|
|
192
208
|
window.__zenith_hydrate(state, {}, {}, {}, container);
|
|
193
209
|
}
|
|
210
|
+
|
|
211
|
+
// Hydrate components by discovering data-zen-component markers
|
|
212
|
+
// This is the ONLY place component instantiation happens - driven by DOM markers
|
|
213
|
+
if (window.__zenith && window.__zenith.hydrateComponents) {
|
|
214
|
+
window.__zenith.hydrateComponents(container);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Trigger page-level mount lifecycle
|
|
218
|
+
if (window.__zenith && window.__zenith.triggerMount) {
|
|
219
|
+
window.__zenith.triggerMount();
|
|
220
|
+
}
|
|
194
221
|
}
|
|
195
222
|
|
|
196
223
|
// Run on DOM ready
|
package/compiler/spa-build.ts
CHANGED
|
@@ -43,11 +43,11 @@ interface SPABuildOptions {
|
|
|
43
43
|
/**
|
|
44
44
|
* Compile a single page file
|
|
45
45
|
*/
|
|
46
|
-
function compilePage(
|
|
46
|
+
async function compilePage(
|
|
47
47
|
pagePath: string,
|
|
48
48
|
pagesDir: string,
|
|
49
49
|
baseDir: string = process.cwd()
|
|
50
|
-
): CompiledPage {
|
|
50
|
+
): Promise<CompiledPage> {
|
|
51
51
|
try {
|
|
52
52
|
const layoutsDir = path.join(baseDir, 'app', 'layouts')
|
|
53
53
|
const layouts = discoverLayouts(layoutsDir)
|
|
@@ -63,7 +63,7 @@ function compilePage(
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
// Use new compiler pipeline on the processed source
|
|
66
|
-
const result = compileZenSource(processedSource, pagePath)
|
|
66
|
+
const result = await compileZenSource(processedSource, pagePath)
|
|
67
67
|
|
|
68
68
|
if (!result.finalized) {
|
|
69
69
|
throw new Error(`Compilation failed: No finalized output`)
|
|
@@ -843,7 +843,7 @@ function generateHTMLShell(
|
|
|
843
843
|
/**
|
|
844
844
|
* Build SPA from pages directory
|
|
845
845
|
*/
|
|
846
|
-
export function buildSPA(options: SPABuildOptions): void {
|
|
846
|
+
export async function buildSPA(options: SPABuildOptions): Promise<void> {
|
|
847
847
|
const { pagesDir, outDir, baseDir } = options
|
|
848
848
|
|
|
849
849
|
// Clean output directory
|
|
@@ -870,7 +870,7 @@ export function buildSPA(options: SPABuildOptions): void {
|
|
|
870
870
|
console.log(`[Zenith Build] Compiling: ${path.relative(pagesDir, pageFile)}`)
|
|
871
871
|
|
|
872
872
|
try {
|
|
873
|
-
const compiled = compilePage(pageFile, pagesDir)
|
|
873
|
+
const compiled = await compilePage(pageFile, pagesDir)
|
|
874
874
|
compiledPages.push(compiled)
|
|
875
875
|
} catch (error) {
|
|
876
876
|
console.error(`[Zenith Build] Error compiling ${pageFile}:`, error)
|
package/compiler/ssg-build.ts
CHANGED
|
@@ -65,11 +65,11 @@ export interface SSGBuildOptions {
|
|
|
65
65
|
/**
|
|
66
66
|
* Compile a single page file for SSG output
|
|
67
67
|
*/
|
|
68
|
-
function compilePage(
|
|
68
|
+
async function compilePage(
|
|
69
69
|
pagePath: string,
|
|
70
70
|
pagesDir: string,
|
|
71
71
|
baseDir: string = process.cwd()
|
|
72
|
-
): CompiledPage {
|
|
72
|
+
): Promise<CompiledPage> {
|
|
73
73
|
const source = fs.readFileSync(pagePath, 'utf-8')
|
|
74
74
|
|
|
75
75
|
// Analyze page requirements
|
|
@@ -88,7 +88,7 @@ function compilePage(
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
// Compile with new pipeline
|
|
91
|
-
const result = compileZenSource(processedSource, pagePath)
|
|
91
|
+
const result = await compileZenSource(processedSource, pagePath)
|
|
92
92
|
|
|
93
93
|
if (!result.finalized) {
|
|
94
94
|
throw new Error(`Compilation failed for ${pagePath}: No finalized output`)
|
|
@@ -239,7 +239,7 @@ ${page.pageScript}
|
|
|
239
239
|
/**
|
|
240
240
|
* Build all pages using SSG approach
|
|
241
241
|
*/
|
|
242
|
-
export function buildSSG(options: SSGBuildOptions): void {
|
|
242
|
+
export async function buildSSG(options: SSGBuildOptions): Promise<void> {
|
|
243
243
|
const { pagesDir, outDir, baseDir = path.dirname(pagesDir) } = options
|
|
244
244
|
const contentDir = path.join(baseDir, 'content')
|
|
245
245
|
const contentData = loadContent(contentDir)
|
|
@@ -275,7 +275,7 @@ export function buildSSG(options: SSGBuildOptions): void {
|
|
|
275
275
|
console.log(` Compiling: ${relativePath}`)
|
|
276
276
|
|
|
277
277
|
try {
|
|
278
|
-
const compiled = compilePage(pageFile, pagesDir, baseDir)
|
|
278
|
+
const compiled = await compilePage(pageFile, pagesDir, baseDir)
|
|
279
279
|
compiledPages.push(compiled)
|
|
280
280
|
|
|
281
281
|
if (compiled.analysis.needsHydration) {
|
|
@@ -356,7 +356,7 @@ export function buildSSG(options: SSGBuildOptions): void {
|
|
|
356
356
|
const custom404Path = path.join(pagesDir, candidate)
|
|
357
357
|
if (fs.existsSync(custom404Path)) {
|
|
358
358
|
try {
|
|
359
|
-
const compiled = compilePage(custom404Path, pagesDir, baseDir)
|
|
359
|
+
const compiled = await compilePage(custom404Path, pagesDir, baseDir)
|
|
360
360
|
const html = generatePageHTML(compiled, globalStyles, contentData)
|
|
361
361
|
fs.writeFileSync(path.join(outDir, '404.html'), html)
|
|
362
362
|
console.log('📦 Generated 404.html (custom)')
|
|
@@ -5,19 +5,19 @@
|
|
|
5
5
|
* Uses compound component pattern for named slots (Card.Header, Card.Footer).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { TemplateNode, ComponentNode, ElementNode, ZenIR, LoopContext } from '../ir/types'
|
|
8
|
+
import type { TemplateNode, ComponentNode, ElementNode, ZenIR, LoopContext, ComponentScriptIR } from '../ir/types'
|
|
9
9
|
import type { ComponentMetadata } from '../discovery/componentDiscovery'
|
|
10
10
|
import { extractSlotsFromChildren, resolveSlots } from './slotResolver'
|
|
11
11
|
import { throwOrphanCompoundError, throwUnresolvedComponentError } from '../validate/invariants'
|
|
12
12
|
|
|
13
|
-
// Track which components have been used (for style collection)
|
|
13
|
+
// Track which components have been used (for style and script collection)
|
|
14
14
|
const usedComponents = new Set<string>()
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Resolve all component nodes in a template IR
|
|
18
18
|
*
|
|
19
19
|
* Recursively replaces ComponentNode instances with their resolved templates
|
|
20
|
-
* Also collects styles from used components and adds them to the IR
|
|
20
|
+
* Also collects styles AND scripts from used components and adds them to the IR
|
|
21
21
|
*/
|
|
22
22
|
export function resolveComponentsInIR(
|
|
23
23
|
ir: ZenIR,
|
|
@@ -35,6 +35,17 @@ export function resolveComponentsInIR(
|
|
|
35
35
|
.filter((meta): meta is ComponentMetadata => meta !== undefined && meta.styles.length > 0)
|
|
36
36
|
.flatMap(meta => meta.styles.map(raw => ({ raw })))
|
|
37
37
|
|
|
38
|
+
// Collect scripts from all used components (for bundling)
|
|
39
|
+
const componentScripts: ComponentScriptIR[] = Array.from(usedComponents)
|
|
40
|
+
.map(name => components.get(name))
|
|
41
|
+
.filter((meta): meta is ComponentMetadata => meta !== undefined && meta.script !== null)
|
|
42
|
+
.map(meta => ({
|
|
43
|
+
name: meta.name,
|
|
44
|
+
script: meta.script!,
|
|
45
|
+
props: meta.props,
|
|
46
|
+
scriptAttributes: meta.scriptAttributes || {}
|
|
47
|
+
}))
|
|
48
|
+
|
|
38
49
|
return {
|
|
39
50
|
...ir,
|
|
40
51
|
template: {
|
|
@@ -42,7 +53,9 @@ export function resolveComponentsInIR(
|
|
|
42
53
|
nodes: resolvedNodes
|
|
43
54
|
},
|
|
44
55
|
// Merge component styles with existing page styles
|
|
45
|
-
styles: [...ir.styles, ...componentStyles]
|
|
56
|
+
styles: [...ir.styles, ...componentStyles],
|
|
57
|
+
// Add component scripts for bundling
|
|
58
|
+
componentScripts: [...(ir.componentScripts || []), ...componentScripts]
|
|
46
59
|
}
|
|
47
60
|
}
|
|
48
61
|
|
|
@@ -192,11 +205,12 @@ function resolveComponent(
|
|
|
192
205
|
const resolvedTemplate = resolveSlots(templateNodes, resolvedSlots)
|
|
193
206
|
|
|
194
207
|
// Forward attributes from component usage to the root element
|
|
195
|
-
//
|
|
208
|
+
// Also adds data-zen-component marker for hydration-driven instantiation
|
|
196
209
|
const forwardedTemplate = forwardAttributesToRoot(
|
|
197
210
|
resolvedTemplate,
|
|
198
211
|
componentNode.attributes,
|
|
199
|
-
componentNode.loopContext
|
|
212
|
+
componentNode.loopContext,
|
|
213
|
+
componentMeta.hasScript ? componentName : undefined // Only mark if component has script
|
|
200
214
|
)
|
|
201
215
|
|
|
202
216
|
// Recursively resolve any nested components in the resolved template
|
|
@@ -210,16 +224,16 @@ function resolveComponent(
|
|
|
210
224
|
*
|
|
211
225
|
* When using <Button onclick="increment">Text</Button>,
|
|
212
226
|
* the onclick should be applied to the <button> element in Button.zen template.
|
|
227
|
+
*
|
|
228
|
+
* Also adds data-zen-component marker if componentName is provided,
|
|
229
|
+
* enabling hydration-driven instantiation.
|
|
213
230
|
*/
|
|
214
231
|
function forwardAttributesToRoot(
|
|
215
232
|
nodes: TemplateNode[],
|
|
216
233
|
attributes: ComponentNode['attributes'],
|
|
217
|
-
loopContext?: LoopContext
|
|
234
|
+
loopContext?: LoopContext,
|
|
235
|
+
componentName?: string // If provided, adds hydration marker
|
|
218
236
|
): TemplateNode[] {
|
|
219
|
-
if (attributes.length === 0) {
|
|
220
|
-
return nodes
|
|
221
|
-
}
|
|
222
|
-
|
|
223
237
|
// Find the first non-text element (the root element)
|
|
224
238
|
const rootIndex = nodes.findIndex(n => n.type === 'element')
|
|
225
239
|
if (rootIndex === -1) {
|
|
@@ -228,10 +242,19 @@ function forwardAttributesToRoot(
|
|
|
228
242
|
|
|
229
243
|
const root = nodes[rootIndex] as ElementNode
|
|
230
244
|
|
|
231
|
-
//
|
|
232
|
-
// Also preserve the parent's loopContext on forwarded attributes
|
|
245
|
+
// Start with existing attributes
|
|
233
246
|
const mergedAttributes = [...root.attributes]
|
|
234
247
|
|
|
248
|
+
// Add component hydration marker if this component has a script
|
|
249
|
+
if (componentName) {
|
|
250
|
+
mergedAttributes.push({
|
|
251
|
+
name: 'data-zen-component',
|
|
252
|
+
value: componentName,
|
|
253
|
+
location: { line: 0, column: 0 }
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Forward attributes from component usage
|
|
235
258
|
for (const attr of attributes) {
|
|
236
259
|
const existingIndex = mergedAttributes.findIndex(a => a.name === attr.name)
|
|
237
260
|
|