@zenithbuild/cli 0.4.11 → 1.3.6
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/dist/zen-build.js +13553 -6385
- package/dist/zen-dev.js +13553 -6385
- package/dist/zen-preview.js +13553 -6385
- package/dist/zenith.js +13553 -6385
- package/package.json +9 -6
- package/src/commands/build.ts +1 -1
- package/src/commands/dev.ts +44 -91
- package/src/commands/index.ts +12 -0
- package/src/commands/lint.ts +140 -0
- package/src/discovery/componentDiscovery.ts +98 -0
- package/src/discovery/layouts.ts +50 -0
- package/src/serve.ts +92 -0
- package/src/spa-build.ts +931 -0
- package/src/ssg-build.ts +548 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenithbuild/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.3.6",
|
|
4
4
|
"description": "CLI for Zenith framework - dev server, build tools, and plugin management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -50,14 +50,17 @@
|
|
|
50
50
|
},
|
|
51
51
|
"private": false,
|
|
52
52
|
"peerDependencies": {
|
|
53
|
-
"@zenithbuild/core": "^1.
|
|
53
|
+
"@zenithbuild/core": "^1.3.0",
|
|
54
|
+
"@zenithbuild/compiler": "^1.3.0"
|
|
54
55
|
},
|
|
55
56
|
"devDependencies": {
|
|
56
|
-
"@types/bun": "latest"
|
|
57
|
+
"@types/bun": "latest",
|
|
58
|
+
"@zenithbuild/compiler": "workspace:*"
|
|
57
59
|
},
|
|
58
60
|
"dependencies": {
|
|
59
|
-
"@zenithbuild/
|
|
60
|
-
"@zenithbuild/router": "
|
|
61
|
+
"@zenithbuild/bundler": "workspace:*",
|
|
62
|
+
"@zenithbuild/router": "workspace:*",
|
|
63
|
+
"glob": "^13.0.0",
|
|
61
64
|
"picocolors": "^1.0.0"
|
|
62
65
|
}
|
|
63
|
-
}
|
|
66
|
+
}
|
package/src/commands/build.ts
CHANGED
package/src/commands/dev.ts
CHANGED
|
@@ -25,23 +25,32 @@ import { requireProject } from '../utils/project'
|
|
|
25
25
|
import * as logger from '../utils/logger'
|
|
26
26
|
import * as brand from '../utils/branding'
|
|
27
27
|
import {
|
|
28
|
-
|
|
29
|
-
discoverLayouts,
|
|
30
|
-
processLayout,
|
|
28
|
+
generateRuntime,
|
|
31
29
|
generateBundleJS,
|
|
32
|
-
loadZenithConfig,
|
|
33
|
-
PluginRegistry,
|
|
34
|
-
createPluginContext,
|
|
35
|
-
getPluginDataByNamespace,
|
|
36
30
|
compileCssAsync,
|
|
37
31
|
resolveGlobalsCss,
|
|
32
|
+
bundlePageScript
|
|
33
|
+
} from '@zenithbuild/bundler'
|
|
34
|
+
import type { ZenManifest } from '@zenithbuild/bundler'
|
|
35
|
+
import { generateRouteDefinition } from '@zenithbuild/router/manifest'
|
|
36
|
+
import { compile } from '@zenithbuild/compiler'
|
|
37
|
+
import { discoverLayouts } from '../discovery/layouts.ts'
|
|
38
|
+
import { discoverComponents } from '../discovery/componentDiscovery.ts'
|
|
39
|
+
import { processLayout } from '@zenithbuild/compiler/transform'
|
|
40
|
+
import { loadZenithConfig } from '@zenithbuild/compiler/config'
|
|
41
|
+
import {
|
|
42
|
+
PluginRegistry,
|
|
43
|
+
createPluginContext,
|
|
44
|
+
getPluginDataByNamespace
|
|
45
|
+
} from '@zenithbuild/compiler/registry'
|
|
46
|
+
import {
|
|
38
47
|
createBridgeAPI,
|
|
39
48
|
runPluginHooks,
|
|
40
49
|
collectHookReturns,
|
|
41
50
|
buildRuntimeEnvelope,
|
|
42
|
-
clearHooks
|
|
43
|
-
|
|
44
|
-
} from '@zenithbuild/compiler'
|
|
51
|
+
clearHooks
|
|
52
|
+
} from '@zenithbuild/compiler/plugins'
|
|
53
|
+
import type { HookContext } from '@zenithbuild/compiler/plugins'
|
|
45
54
|
|
|
46
55
|
export interface DevOptions {
|
|
47
56
|
port?: number
|
|
@@ -61,7 +70,7 @@ const pageCache = new Map<string, CompiledPage>()
|
|
|
61
70
|
* Bundle page script using Rolldown to resolve npm imports at compile time.
|
|
62
71
|
* Only called when compiler emits a BundlePlan - bundler performs no inference.
|
|
63
72
|
*/
|
|
64
|
-
import {
|
|
73
|
+
import type { BundlePlan } from '@zenithbuild/compiler'
|
|
65
74
|
|
|
66
75
|
export async function dev(options: DevOptions = {}): Promise<void> {
|
|
67
76
|
const project = requireProject()
|
|
@@ -153,88 +162,39 @@ export async function dev(options: DevOptions = {}): Promise<void> {
|
|
|
153
162
|
const layoutsDir = path.join(pagesDir, '../layouts')
|
|
154
163
|
const componentsDir = path.join(pagesDir, '../components')
|
|
155
164
|
const layouts = discoverLayouts(layoutsDir)
|
|
156
|
-
const source = fs.readFileSync(pagePath, 'utf-8')
|
|
157
|
-
|
|
158
|
-
let processedSource = source
|
|
159
|
-
let layoutToUse = layouts.get('DefaultLayout')
|
|
160
|
-
|
|
161
|
-
if (layoutToUse) processedSource = processLayout(source, layoutToUse)
|
|
162
|
-
|
|
163
|
-
const result = await compileZenSource(processedSource, pagePath, {
|
|
164
|
-
componentsDir: fs.existsSync(componentsDir) ? componentsDir : undefined
|
|
165
|
-
})
|
|
166
|
-
if (!result.finalized) throw new Error('Compilation failed')
|
|
167
|
-
|
|
168
|
-
const routeDef = generateRouteDefinition(pagePath, pagesDir)
|
|
169
|
-
|
|
170
|
-
|
|
171
165
|
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
while (jsLines.length > 0 && jsLines[0] !== undefined) {
|
|
179
|
-
const line = jsLines[0].trim()
|
|
180
|
-
if (
|
|
181
|
-
line.startsWith('import ') ||
|
|
182
|
-
line === '' ||
|
|
183
|
-
line.startsWith('//') ||
|
|
184
|
-
line.startsWith('/*') ||
|
|
185
|
-
line.startsWith('*')
|
|
186
|
-
) {
|
|
187
|
-
jsLines.shift()
|
|
188
|
-
} else {
|
|
189
|
-
break
|
|
166
|
+
// Manual component discovery since compiler is pure
|
|
167
|
+
const components = new Map<string, any>([...layouts])
|
|
168
|
+
if (fs.existsSync(componentsDir)) {
|
|
169
|
+
const discovered = discoverComponents(componentsDir)
|
|
170
|
+
for (const [k, v] of discovered) {
|
|
171
|
+
components.set(k, v)
|
|
190
172
|
}
|
|
191
173
|
}
|
|
192
174
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
// PATCH: Fix unquoted keys with dashes (Rust codegen bug in jsx_lowerer)
|
|
196
|
-
// e.g. stroke-width: "1.5" -> "stroke-width": "1.5"
|
|
197
|
-
// We only apply this to the JS portions (Script and Expressions)
|
|
198
|
-
// to avoid corrupting the Styles section.
|
|
199
|
-
const stylesMarker = '// 6. Styles injection'
|
|
200
|
-
const parts = jsWithoutImports.split(stylesMarker)
|
|
201
|
-
|
|
202
|
-
if (parts.length > 1) {
|
|
203
|
-
// Apply patch only to the JS part
|
|
204
|
-
parts[0] = parts[0]!.replace(
|
|
205
|
-
/(^|[{,])\s*([a-zA-Z][a-zA-Z0-9-]*-[a-zA-Z0-9-]*)\s*:/gm,
|
|
206
|
-
'$1"$2":'
|
|
207
|
-
)
|
|
208
|
-
jsWithoutImports = parts.join(stylesMarker)
|
|
209
|
-
} else {
|
|
210
|
-
// Fallback if marker not found
|
|
211
|
-
jsWithoutImports = jsWithoutImports.replace(
|
|
212
|
-
/(^|[{,])\s*([a-zA-Z][a-zA-Z0-9-]*-[a-zA-Z0-9-]*)\s*:/gm,
|
|
213
|
-
'$1"$2":'
|
|
214
|
-
)
|
|
215
|
-
}
|
|
175
|
+
const source = fs.readFileSync(pagePath, 'utf-8')
|
|
216
176
|
|
|
217
|
-
|
|
218
|
-
|
|
177
|
+
const result = await compile(source, pagePath, {
|
|
178
|
+
components: components
|
|
179
|
+
})
|
|
180
|
+
if (!result.finalized || !result.finalized.manifest) throw new Error('Compilation failed')
|
|
219
181
|
|
|
220
182
|
|
|
183
|
+
const routeDef = generateRouteDefinition(pagePath, pagesDir)
|
|
221
184
|
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
entry: fullScript,
|
|
229
|
-
resolveRoots: [path.join(rootDir, 'node_modules'), 'node_modules']
|
|
230
|
-
}
|
|
231
|
-
bundledScript = await bundlePageScript(plan)
|
|
185
|
+
// Use the new bundler to generate the runtime + author script
|
|
186
|
+
// This replaces all the manual regex patching and string concatenation
|
|
187
|
+
const manifest: ZenManifest = {
|
|
188
|
+
routes: [routeDef],
|
|
189
|
+
layouts: {}, // Dev server handles layouts dynamically
|
|
190
|
+
components: {}
|
|
232
191
|
}
|
|
192
|
+
const { code } = generateRuntime(manifest, true)
|
|
233
193
|
|
|
234
194
|
return {
|
|
235
195
|
html: result.finalized.html,
|
|
236
|
-
script:
|
|
237
|
-
styles:
|
|
196
|
+
script: code,
|
|
197
|
+
styles: [], // Styles are now injected via the script
|
|
238
198
|
route: routeDef.path,
|
|
239
199
|
lastModified: Date.now()
|
|
240
200
|
}
|
|
@@ -245,22 +205,15 @@ export async function dev(options: DevOptions = {}): Promise<void> {
|
|
|
245
205
|
}
|
|
246
206
|
|
|
247
207
|
/**
|
|
248
|
-
* Generate dev HTML
|
|
249
|
-
*
|
|
250
|
-
* CLI collects payloads from plugins via 'cli:runtime:collect' hook.
|
|
251
|
-
* It serializes blindly - never inspecting what's inside.
|
|
208
|
+
* Generate dev HTML
|
|
252
209
|
*/
|
|
253
210
|
async function generateDevHTML(page: CompiledPage): Promise<string> {
|
|
254
|
-
// Single neutral injection point
|
|
255
|
-
const runtimeTag = `<script src="/runtime.js"></script>`
|
|
256
211
|
const scriptTag = `<script type="module">\n${page.script}\n</script>`
|
|
257
|
-
const allScripts = `${runtimeTag}\n${scriptTag}`
|
|
258
212
|
|
|
259
213
|
let html = page.html.includes('</body>')
|
|
260
|
-
? page.html.replace('</body>', `${
|
|
261
|
-
: `${page.html}\n${
|
|
214
|
+
? page.html.replace('</body>', `${scriptTag}\n</body>`)
|
|
215
|
+
: `${page.html}\n${scriptTag}`
|
|
262
216
|
|
|
263
|
-
// Ensure DOCTYPE is present to prevent Quirks Mode (critical for SVG namespace)
|
|
264
217
|
if (!html.trimStart().toLowerCase().startsWith('<!doctype')) {
|
|
265
218
|
html = `<!DOCTYPE html>\n${html}`
|
|
266
219
|
}
|
package/src/commands/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { build, type BuildOptions } from './build'
|
|
|
10
10
|
import { add, type AddOptions } from './add'
|
|
11
11
|
import { remove } from './remove'
|
|
12
12
|
import { create } from './create'
|
|
13
|
+
import { lint, type LintOptions } from './lint'
|
|
13
14
|
import * as logger from '../utils/logger'
|
|
14
15
|
|
|
15
16
|
export interface Command {
|
|
@@ -84,6 +85,17 @@ export const commands: Command[] = [
|
|
|
84
85
|
}
|
|
85
86
|
await remove(pluginName)
|
|
86
87
|
}
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'lint',
|
|
91
|
+
description: 'Audit project for Zenith Contract compliance',
|
|
92
|
+
usage: 'zenith lint [--fix]',
|
|
93
|
+
async run(args, options) {
|
|
94
|
+
const opts: LintOptions = {}
|
|
95
|
+
if (options.fix) opts.fix = true
|
|
96
|
+
if (options.incremental) opts.incremental = true
|
|
97
|
+
await lint(args, opts)
|
|
98
|
+
}
|
|
87
99
|
}
|
|
88
100
|
]
|
|
89
101
|
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zenithbuild/cli - Lint Command
|
|
3
|
+
*
|
|
4
|
+
* Scans for .zen files and enforces the Zenith Contract.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { glob } from 'glob'
|
|
8
|
+
import { join } from 'path'
|
|
9
|
+
import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'fs'
|
|
10
|
+
import { compile } from '../../../zenith-compiler/dist/index.js'
|
|
11
|
+
import * as logger from '../utils/logger'
|
|
12
|
+
import picocolors from 'picocolors'
|
|
13
|
+
import { createHash } from 'crypto'
|
|
14
|
+
import { dirname } from 'path'
|
|
15
|
+
|
|
16
|
+
export interface LintOptions {
|
|
17
|
+
fix?: boolean
|
|
18
|
+
incremental?: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface LintCache {
|
|
22
|
+
files: Record<string, string> // path -> hash
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function findProjectRoot(startDir: string): string {
|
|
26
|
+
let current = startDir
|
|
27
|
+
while (current !== dirname(current)) {
|
|
28
|
+
if (existsSync(join(current, 'package.json')) || existsSync(join(current, '.zenith'))) {
|
|
29
|
+
return current
|
|
30
|
+
}
|
|
31
|
+
current = dirname(current)
|
|
32
|
+
}
|
|
33
|
+
return startDir
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function lint(fileArgs: string[] = [], options: LintOptions = {}): Promise<void> {
|
|
37
|
+
logger.header('Zenith Safety Audit')
|
|
38
|
+
|
|
39
|
+
const isIncremental = options.incremental || false
|
|
40
|
+
|
|
41
|
+
// Find files
|
|
42
|
+
// Filter out flags that might have been passed in args
|
|
43
|
+
const filesToScan = fileArgs.filter(a => !a.startsWith('--'))
|
|
44
|
+
let files: string[] = []
|
|
45
|
+
|
|
46
|
+
if (filesToScan.length > 0) {
|
|
47
|
+
files = filesToScan
|
|
48
|
+
} else {
|
|
49
|
+
files = await glob('**/*.zen', {
|
|
50
|
+
ignore: ['**/node_modules/**', '**/dist/**', '.git/**', '**/test/**']
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (files.length === 0) {
|
|
55
|
+
logger.info('No .zen files found to audit.')
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Load lint cache - Optimized for monorepos
|
|
60
|
+
const root = findProjectRoot(process.cwd())
|
|
61
|
+
const cacheDir = join(root, '.zenith/cache')
|
|
62
|
+
const cachePath = join(cacheDir, 'lint-cache.json')
|
|
63
|
+
let lintCache: LintCache = { files: {} }
|
|
64
|
+
|
|
65
|
+
if (isIncremental && existsSync(cachePath)) {
|
|
66
|
+
try {
|
|
67
|
+
lintCache = JSON.parse(readFileSync(cachePath, 'utf-8'))
|
|
68
|
+
} catch (e) {
|
|
69
|
+
logger.warn('Failed to load lint cache, starting fresh.')
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
logger.info(`Auditing ${files.length} components${isIncremental ? ' (incremental)' : ''}...\n`)
|
|
74
|
+
|
|
75
|
+
let errorCount = 0
|
|
76
|
+
let fileCount = 0
|
|
77
|
+
let skippedCount = 0
|
|
78
|
+
const newCache: LintCache = { files: { ...lintCache.files } }
|
|
79
|
+
|
|
80
|
+
for (const file of files) {
|
|
81
|
+
fileCount++
|
|
82
|
+
const source = readFileSync(file, 'utf-8')
|
|
83
|
+
const hash = createHash('sha256').update(source).digest('hex')
|
|
84
|
+
|
|
85
|
+
if (isIncremental && lintCache.files[file] === hash) {
|
|
86
|
+
skippedCount++
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
// Run compilation with cache enabled for components
|
|
92
|
+
process.env.ZENITH_CACHE = '1'
|
|
93
|
+
await compile(source, file)
|
|
94
|
+
newCache.files[file] = hash
|
|
95
|
+
} catch (error: any) {
|
|
96
|
+
errorCount++
|
|
97
|
+
// Clear cache for this file on error
|
|
98
|
+
delete newCache.files[file]
|
|
99
|
+
|
|
100
|
+
if (error.name === 'InvariantError' || error.name === 'CompilerError') {
|
|
101
|
+
console.log(picocolors.red(picocolors.bold(`\n✖ ${file}:${error.line}:${error.column}`)))
|
|
102
|
+
console.log(picocolors.red(` [${error.code || 'ERROR'}] ${error.message}`))
|
|
103
|
+
|
|
104
|
+
if (error.guarantee) {
|
|
105
|
+
console.log(picocolors.yellow(` Guarantee: ${error.guarantee}`))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (error.context) {
|
|
109
|
+
console.log(picocolors.dim(` Context: ${error.context}`))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (error.hints && error.hints.length > 0) {
|
|
113
|
+
console.log(picocolors.cyan(' Hints:'))
|
|
114
|
+
for (const hint of error.hints) {
|
|
115
|
+
console.log(picocolors.cyan(` - ${hint}`))
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
console.log(picocolors.red(`\n✖ ${file}: Unexpected error`))
|
|
120
|
+
console.log(picocolors.red(` ${error.message}`))
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Save updated cache
|
|
126
|
+
if (isIncremental) {
|
|
127
|
+
if (!existsSync(cacheDir)) mkdirSync(cacheDir, { recursive: true })
|
|
128
|
+
writeFileSync(cachePath, JSON.stringify(newCache, null, 2))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log('')
|
|
132
|
+
|
|
133
|
+
if (errorCount > 0) {
|
|
134
|
+
logger.error(`Audit failed. Found ${errorCount} contract violations in ${fileCount} files.`)
|
|
135
|
+
process.exit(1)
|
|
136
|
+
} else {
|
|
137
|
+
const skipMsg = skippedCount > 0 ? ` (${skippedCount} unchanged files skipped)` : ''
|
|
138
|
+
logger.success(`Audit passed. ${fileCount} files checked${skipMsg}, 0 violations found.`)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component Discovery
|
|
3
|
+
*
|
|
4
|
+
* Discovers and catalogs components in a Zenith project using standard
|
|
5
|
+
* file system walking and the unified native "syscall" for metadata.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from 'fs'
|
|
9
|
+
import * as path from 'path'
|
|
10
|
+
import { parseZenFile, type TemplateNode, type ExpressionIR } from '@zenithbuild/compiler'
|
|
11
|
+
|
|
12
|
+
export interface SlotDefinition {
|
|
13
|
+
name: string | null // null = default slot, string = named slot
|
|
14
|
+
location: {
|
|
15
|
+
line: number
|
|
16
|
+
column: number
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ComponentMetadata {
|
|
21
|
+
name: string // Component name (e.g., "Card", "Button")
|
|
22
|
+
path: string // Absolute path to .zen file
|
|
23
|
+
template: string // Raw template HTML
|
|
24
|
+
nodes: TemplateNode[] // Parsed template nodes
|
|
25
|
+
expressions: ExpressionIR[] // Component-level expressions
|
|
26
|
+
slots: SlotDefinition[]
|
|
27
|
+
props: string[] // Declared props
|
|
28
|
+
states: Record<string, string> // Declared state (name -> initializer)
|
|
29
|
+
styles: string[] // Raw CSS from <style> blocks
|
|
30
|
+
script: string | null // Raw script content for bundling
|
|
31
|
+
scriptAttributes: Record<string, string> | null // Script attributes (setup, lang)
|
|
32
|
+
hasScript: boolean
|
|
33
|
+
hasStyles: boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Discover all components in a directory recursively
|
|
38
|
+
*/
|
|
39
|
+
export function discoverComponents(baseDir: string): Map<string, ComponentMetadata> {
|
|
40
|
+
const components = new Map<string, ComponentMetadata>()
|
|
41
|
+
|
|
42
|
+
if (!fs.existsSync(baseDir)) return components;
|
|
43
|
+
|
|
44
|
+
const walk = (dir: string) => {
|
|
45
|
+
const files = fs.readdirSync(dir);
|
|
46
|
+
for (const file of files) {
|
|
47
|
+
const fullPath = path.join(dir, file);
|
|
48
|
+
if (fs.statSync(fullPath).isDirectory()) {
|
|
49
|
+
walk(fullPath);
|
|
50
|
+
} else if (file.endsWith('.zen')) {
|
|
51
|
+
const name = path.basename(file, '.zen');
|
|
52
|
+
try {
|
|
53
|
+
// Call the "One True Bridge" in metadata mode
|
|
54
|
+
const ir = parseZenFile(fullPath, undefined, { mode: 'metadata' });
|
|
55
|
+
|
|
56
|
+
// Map IR to ComponentMetadata format
|
|
57
|
+
components.set(name, {
|
|
58
|
+
name,
|
|
59
|
+
path: fullPath,
|
|
60
|
+
template: ir.template.raw,
|
|
61
|
+
nodes: ir.template.nodes,
|
|
62
|
+
expressions: ir.template.expressions,
|
|
63
|
+
slots: [], // Native bridge needs to return slot info in IR if used
|
|
64
|
+
props: ir.props || [],
|
|
65
|
+
states: ir.script?.states || {},
|
|
66
|
+
styles: ir.styles?.map((s: any) => s.raw) || [],
|
|
67
|
+
script: ir.script?.raw || null,
|
|
68
|
+
scriptAttributes: ir.script?.attributes || null,
|
|
69
|
+
hasScript: !!ir.script,
|
|
70
|
+
hasStyles: ir.styles?.length > 0
|
|
71
|
+
});
|
|
72
|
+
} catch (e) {
|
|
73
|
+
console.error(`[Zenith Discovery] Failed to parse component ${file}:`, e);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
walk(baseDir);
|
|
80
|
+
return components;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Universal Zenith Component Tag Rule: PascalCase
|
|
85
|
+
*/
|
|
86
|
+
export function isComponentTag(tagName: string): boolean {
|
|
87
|
+
return tagName.length > 0 && tagName[0] === tagName[0]?.toUpperCase()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get component metadata by name
|
|
92
|
+
*/
|
|
93
|
+
export function getComponent(
|
|
94
|
+
components: Map<string, ComponentMetadata>,
|
|
95
|
+
name: string
|
|
96
|
+
): ComponentMetadata | undefined {
|
|
97
|
+
return components.get(name)
|
|
98
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as fs from 'fs'
|
|
2
|
+
import * as path from 'path'
|
|
3
|
+
import { parseZenFile } from '@zenithbuild/compiler'
|
|
4
|
+
|
|
5
|
+
export interface LayoutMetadata {
|
|
6
|
+
name: string
|
|
7
|
+
filePath: string
|
|
8
|
+
props: string[]
|
|
9
|
+
states: Map<string, any>
|
|
10
|
+
html: string
|
|
11
|
+
scripts: string[]
|
|
12
|
+
styles: string[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Discover layouts in a directory using standard file system walking
|
|
17
|
+
* and the unified native bridge for metadata.
|
|
18
|
+
*/
|
|
19
|
+
export function discoverLayouts(layoutsDir: string): Map<string, LayoutMetadata> {
|
|
20
|
+
const layouts = new Map<string, LayoutMetadata>()
|
|
21
|
+
|
|
22
|
+
if (!fs.existsSync(layoutsDir)) return layouts
|
|
23
|
+
|
|
24
|
+
const files = fs.readdirSync(layoutsDir)
|
|
25
|
+
for (const file of files) {
|
|
26
|
+
if (file.endsWith('.zen')) {
|
|
27
|
+
const fullPath = path.join(layoutsDir, file)
|
|
28
|
+
const name = path.basename(file, '.zen')
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// Call the "One True Bridge" in metadata mode
|
|
32
|
+
const ir = parseZenFile(fullPath, undefined, { mode: 'metadata' })
|
|
33
|
+
|
|
34
|
+
layouts.set(name, {
|
|
35
|
+
name,
|
|
36
|
+
filePath: fullPath,
|
|
37
|
+
props: ir.props || [],
|
|
38
|
+
states: new Map(),
|
|
39
|
+
html: ir.template.raw,
|
|
40
|
+
scripts: ir.script ? [ir.script.content] : [],
|
|
41
|
+
styles: ir.styles?.map((s: any) => s.raw) || []
|
|
42
|
+
})
|
|
43
|
+
} catch (e) {
|
|
44
|
+
console.error(`[Zenith Layout Discovery] Failed to parse layout ${file}:`, e)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return layouts
|
|
50
|
+
}
|
package/src/serve.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zenith Development Server
|
|
3
|
+
*
|
|
4
|
+
* SPA-compatible server that:
|
|
5
|
+
* - Serves static assets directly (js, css, ico, images)
|
|
6
|
+
* - Serves index.html for all other routes (SPA fallback)
|
|
7
|
+
*
|
|
8
|
+
* This enables client-side routing to work on:
|
|
9
|
+
* - Direct URL entry
|
|
10
|
+
* - Hard refresh
|
|
11
|
+
* - Back/forward navigation
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { serve } from "bun"
|
|
15
|
+
import path from "path"
|
|
16
|
+
|
|
17
|
+
const distDir = path.resolve(process.cwd(), "dist")
|
|
18
|
+
|
|
19
|
+
// File extensions that should be served as static assets
|
|
20
|
+
const STATIC_EXTENSIONS = new Set([
|
|
21
|
+
".js",
|
|
22
|
+
".css",
|
|
23
|
+
".ico",
|
|
24
|
+
".png",
|
|
25
|
+
".jpg",
|
|
26
|
+
".jpeg",
|
|
27
|
+
".gif",
|
|
28
|
+
".svg",
|
|
29
|
+
".webp",
|
|
30
|
+
".woff",
|
|
31
|
+
".woff2",
|
|
32
|
+
".ttf",
|
|
33
|
+
".eot",
|
|
34
|
+
".json",
|
|
35
|
+
".map"
|
|
36
|
+
])
|
|
37
|
+
|
|
38
|
+
serve({
|
|
39
|
+
port: 3000,
|
|
40
|
+
|
|
41
|
+
async fetch(req) {
|
|
42
|
+
const url = new URL(req.url)
|
|
43
|
+
const pathname = url.pathname
|
|
44
|
+
|
|
45
|
+
// Get file extension
|
|
46
|
+
const ext = path.extname(pathname).toLowerCase()
|
|
47
|
+
|
|
48
|
+
// Check if this is a static asset request
|
|
49
|
+
if (STATIC_EXTENSIONS.has(ext)) {
|
|
50
|
+
const filePath = path.join(distDir, pathname)
|
|
51
|
+
const file = Bun.file(filePath)
|
|
52
|
+
|
|
53
|
+
// Check if file exists
|
|
54
|
+
if (await file.exists()) {
|
|
55
|
+
return new Response(file)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Static file not found
|
|
59
|
+
return new Response("Not found", { status: 404 })
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// For all other routes, serve index.html (SPA fallback)
|
|
63
|
+
const indexPath = path.join(distDir, "index.html")
|
|
64
|
+
const indexFile = Bun.file(indexPath)
|
|
65
|
+
|
|
66
|
+
if (await indexFile.exists()) {
|
|
67
|
+
return new Response(indexFile, {
|
|
68
|
+
headers: {
|
|
69
|
+
"Content-Type": "text/html; charset=utf-8"
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// No index.html found - likely need to run build first
|
|
75
|
+
return new Response(
|
|
76
|
+
`<html>
|
|
77
|
+
<head><title>Zenith - Build Required</title></head>
|
|
78
|
+
<body style="font-family: system-ui; padding: 2rem; text-align: center;">
|
|
79
|
+
<h1>Build Required</h1>
|
|
80
|
+
<p>Run <code>zenith build</code> first to compile the pages.</p>
|
|
81
|
+
</body>
|
|
82
|
+
</html>`,
|
|
83
|
+
{
|
|
84
|
+
status: 500,
|
|
85
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
console.log("🚀 Zenith dev server running at http://localhost:3000")
|
|
92
|
+
console.log(" SPA mode: All routes serve index.html")
|