@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,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zenith/cli - Logger Utility
|
|
3
|
+
*
|
|
4
|
+
* Colored console output for CLI feedback
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const colors = {
|
|
8
|
+
reset: '\x1b[0m',
|
|
9
|
+
bright: '\x1b[1m',
|
|
10
|
+
dim: '\x1b[2m',
|
|
11
|
+
red: '\x1b[31m',
|
|
12
|
+
green: '\x1b[32m',
|
|
13
|
+
yellow: '\x1b[33m',
|
|
14
|
+
blue: '\x1b[34m',
|
|
15
|
+
cyan: '\x1b[36m'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function log(message: string): void {
|
|
19
|
+
console.log(`${colors.cyan}[zenith]${colors.reset} ${message}`)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function success(message: string): void {
|
|
23
|
+
console.log(`${colors.green}✓${colors.reset} ${message}`)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function warn(message: string): void {
|
|
27
|
+
console.log(`${colors.yellow}⚠${colors.reset} ${message}`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function error(message: string): void {
|
|
31
|
+
console.error(`${colors.red}✗${colors.reset} ${message}`)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function info(message: string): void {
|
|
35
|
+
console.log(`${colors.blue}ℹ${colors.reset} ${message}`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function header(title: string): void {
|
|
39
|
+
console.log(`\n${colors.bright}${colors.cyan}${title}${colors.reset}\n`)
|
|
40
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zenith/cli - Plugin Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages zenith.plugins.json for plugin registration
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'fs'
|
|
8
|
+
import path from 'path'
|
|
9
|
+
import { findProjectRoot } from './project'
|
|
10
|
+
import * as logger from './logger'
|
|
11
|
+
|
|
12
|
+
export interface PluginConfig {
|
|
13
|
+
name: string
|
|
14
|
+
installedAt: string
|
|
15
|
+
options?: Record<string, unknown>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PluginsFile {
|
|
19
|
+
plugins: PluginConfig[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const PLUGINS_FILE = 'zenith.plugins.json'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get path to plugins file
|
|
26
|
+
*/
|
|
27
|
+
function getPluginsPath(): string {
|
|
28
|
+
const root = findProjectRoot()
|
|
29
|
+
if (!root) {
|
|
30
|
+
throw new Error('Not in a Zenith project')
|
|
31
|
+
}
|
|
32
|
+
return path.join(root, PLUGINS_FILE)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Read plugins file
|
|
37
|
+
*/
|
|
38
|
+
export function readPlugins(): PluginsFile {
|
|
39
|
+
const pluginsPath = getPluginsPath()
|
|
40
|
+
|
|
41
|
+
if (!fs.existsSync(pluginsPath)) {
|
|
42
|
+
return { plugins: [] }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(fs.readFileSync(pluginsPath, 'utf-8'))
|
|
47
|
+
} catch {
|
|
48
|
+
return { plugins: [] }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Write plugins file
|
|
54
|
+
*/
|
|
55
|
+
function writePlugins(data: PluginsFile): void {
|
|
56
|
+
const pluginsPath = getPluginsPath()
|
|
57
|
+
fs.writeFileSync(pluginsPath, JSON.stringify(data, null, 2))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Add a plugin to the registry
|
|
62
|
+
*/
|
|
63
|
+
export function addPlugin(name: string, options?: Record<string, unknown>): boolean {
|
|
64
|
+
const data = readPlugins()
|
|
65
|
+
|
|
66
|
+
// Check if already installed
|
|
67
|
+
if (data.plugins.some(p => p.name === name)) {
|
|
68
|
+
logger.warn(`Plugin "${name}" is already registered`)
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
data.plugins.push({
|
|
73
|
+
name,
|
|
74
|
+
installedAt: new Date().toISOString(),
|
|
75
|
+
options
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
writePlugins(data)
|
|
79
|
+
logger.success(`Added plugin "${name}"`)
|
|
80
|
+
return true
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Remove a plugin from the registry
|
|
85
|
+
*/
|
|
86
|
+
export function removePlugin(name: string): boolean {
|
|
87
|
+
const data = readPlugins()
|
|
88
|
+
const initialLength = data.plugins.length
|
|
89
|
+
|
|
90
|
+
data.plugins = data.plugins.filter(p => p.name !== name)
|
|
91
|
+
|
|
92
|
+
if (data.plugins.length === initialLength) {
|
|
93
|
+
logger.warn(`Plugin "${name}" is not registered`)
|
|
94
|
+
return false
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
writePlugins(data)
|
|
98
|
+
logger.success(`Removed plugin "${name}"`)
|
|
99
|
+
return true
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* List all registered plugins
|
|
104
|
+
*/
|
|
105
|
+
export function listPlugins(): PluginConfig[] {
|
|
106
|
+
return readPlugins().plugins
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Check if a plugin is registered
|
|
111
|
+
*/
|
|
112
|
+
export function hasPlugin(name: string): boolean {
|
|
113
|
+
return readPlugins().plugins.some(p => p.name === name)
|
|
114
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zenith/cli - Project Utility
|
|
3
|
+
*
|
|
4
|
+
* Detects Zenith project root and configuration
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'fs'
|
|
8
|
+
import path from 'path'
|
|
9
|
+
|
|
10
|
+
export interface ZenithProject {
|
|
11
|
+
root: string
|
|
12
|
+
pagesDir: string
|
|
13
|
+
distDir: string
|
|
14
|
+
hasZenithDeps: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Find the project root by looking for package.json with @zenith dependencies
|
|
19
|
+
*/
|
|
20
|
+
export function findProjectRoot(startDir: string = process.cwd()): string | null {
|
|
21
|
+
let current = startDir
|
|
22
|
+
|
|
23
|
+
while (current !== path.dirname(current)) {
|
|
24
|
+
const pkgPath = path.join(current, 'package.json')
|
|
25
|
+
|
|
26
|
+
if (fs.existsSync(pkgPath)) {
|
|
27
|
+
try {
|
|
28
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
|
|
29
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies }
|
|
30
|
+
|
|
31
|
+
// Check for any @zenith/* dependency
|
|
32
|
+
const hasZenith = Object.keys(deps).some(d => d.startsWith('@zenith/'))
|
|
33
|
+
if (hasZenith) {
|
|
34
|
+
return current
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
// Invalid JSON, skip
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
current = path.dirname(current)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get project configuration
|
|
49
|
+
*/
|
|
50
|
+
export function getProject(cwd: string = process.cwd()): ZenithProject | null {
|
|
51
|
+
const root = findProjectRoot(cwd)
|
|
52
|
+
if (!root) return null
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
root,
|
|
56
|
+
pagesDir: path.join(root, 'app/pages'),
|
|
57
|
+
distDir: path.join(root, 'app/dist'),
|
|
58
|
+
hasZenithDeps: true
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Ensure we're in a Zenith project
|
|
64
|
+
*/
|
|
65
|
+
export function requireProject(cwd: string = process.cwd()): ZenithProject {
|
|
66
|
+
const project = getProject(cwd)
|
|
67
|
+
if (!project) {
|
|
68
|
+
throw new Error('Not in a Zenith project. Run this command from a directory with @zenith/* dependencies.')
|
|
69
|
+
}
|
|
70
|
+
return project
|
|
71
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zenith Build Analyzer
|
|
3
|
+
*
|
|
4
|
+
* Analyzes .zen page source to determine build strategy:
|
|
5
|
+
* - Static: Pure HTML+CSS, no JS needed
|
|
6
|
+
* - Hydration: Has state/events/hooks, needs page-specific JS
|
|
7
|
+
* - SSR: Uses useFetchServer, needs server rendering
|
|
8
|
+
* - SPA: Uses ZenLink with passHref, needs client router
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface PageAnalysis {
|
|
12
|
+
/** Page has state declarations that need hydration */
|
|
13
|
+
hasState: boolean
|
|
14
|
+
/** Page has event handlers (onclick, etc.) */
|
|
15
|
+
hasEventHandlers: boolean
|
|
16
|
+
/** Page uses lifecycle hooks (zenOnMount, zenOnUnmount) */
|
|
17
|
+
hasLifecycleHooks: boolean
|
|
18
|
+
/** Page uses useFetchServer (requires SSR) */
|
|
19
|
+
usesServerFetch: boolean
|
|
20
|
+
/** Page uses useFetchClient (client-side data) */
|
|
21
|
+
usesClientFetch: boolean
|
|
22
|
+
/** Page uses ZenLink with passHref (SPA navigation) */
|
|
23
|
+
usesZenLink: boolean
|
|
24
|
+
/** Page uses reactive expressions in templates */
|
|
25
|
+
hasReactiveExpressions: boolean
|
|
26
|
+
|
|
27
|
+
/** Computed: page needs any JavaScript */
|
|
28
|
+
needsHydration: boolean
|
|
29
|
+
/** Computed: page is purely static (no JS) */
|
|
30
|
+
isStatic: boolean
|
|
31
|
+
/** Computed: page needs SSR */
|
|
32
|
+
needsSSR: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Analyze a .zen page source to determine build requirements
|
|
37
|
+
*/
|
|
38
|
+
export function analyzePageSource(source: string): PageAnalysis {
|
|
39
|
+
// Extract script content for analysis
|
|
40
|
+
const scriptMatch = source.match(/<script[^>]*>([\s\S]*?)<\/script>/i)
|
|
41
|
+
const scriptContent = scriptMatch?.[1] || ''
|
|
42
|
+
|
|
43
|
+
// Extract template content (everything outside script/style)
|
|
44
|
+
const templateContent = source
|
|
45
|
+
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
46
|
+
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
|
47
|
+
|
|
48
|
+
// Check for state declarations: "state varName = ..."
|
|
49
|
+
const hasState = /\bstate\s+\w+\s*=/.test(scriptContent)
|
|
50
|
+
|
|
51
|
+
// Check for event handlers in template
|
|
52
|
+
const hasEventHandlers = /\bon(click|change|input|submit|focus|blur|keydown|keyup|keypress|mousedown|mouseup|mouseover|mouseout|mouseenter|mouseleave)\s*=\s*["'{]/.test(templateContent)
|
|
53
|
+
|
|
54
|
+
// Check for lifecycle hooks
|
|
55
|
+
const hasLifecycleHooks = /\bzen(OnMount|OnUnmount)\s*\(/.test(scriptContent)
|
|
56
|
+
|
|
57
|
+
// Check for server fetch
|
|
58
|
+
const usesServerFetch = /\buseFetchServer\s*\(/.test(scriptContent)
|
|
59
|
+
|
|
60
|
+
// Check for client fetch
|
|
61
|
+
const usesClientFetch = /\buseFetchClient\s*\(/.test(scriptContent)
|
|
62
|
+
|
|
63
|
+
// Check for ZenLink with passHref
|
|
64
|
+
const usesZenLink = /<ZenLink[^>]*passHref[^>]*>/.test(templateContent)
|
|
65
|
+
|
|
66
|
+
// Check for reactive expressions in template: {expression}
|
|
67
|
+
// Must be actual expressions, not just static text
|
|
68
|
+
// Exclude attribute values like href="/path"
|
|
69
|
+
const hasReactiveExpressions = /{[a-zA-Z_][a-zA-Z0-9_]*}/.test(templateContent)
|
|
70
|
+
|
|
71
|
+
// Compute derived properties
|
|
72
|
+
const needsHydration = hasState || hasEventHandlers || hasLifecycleHooks ||
|
|
73
|
+
usesClientFetch || hasReactiveExpressions
|
|
74
|
+
const isStatic = !needsHydration && !usesServerFetch
|
|
75
|
+
const needsSSR = usesServerFetch
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
hasState,
|
|
79
|
+
hasEventHandlers,
|
|
80
|
+
hasLifecycleHooks,
|
|
81
|
+
usesServerFetch,
|
|
82
|
+
usesClientFetch,
|
|
83
|
+
usesZenLink,
|
|
84
|
+
hasReactiveExpressions,
|
|
85
|
+
needsHydration,
|
|
86
|
+
isStatic,
|
|
87
|
+
needsSSR
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get a human-readable summary of the page analysis
|
|
93
|
+
*/
|
|
94
|
+
export function getAnalysisSummary(analysis: PageAnalysis): string {
|
|
95
|
+
const flags: string[] = []
|
|
96
|
+
|
|
97
|
+
if (analysis.isStatic) {
|
|
98
|
+
flags.push('STATIC (no JS)')
|
|
99
|
+
} else {
|
|
100
|
+
if (analysis.hasState) flags.push('state')
|
|
101
|
+
if (analysis.hasEventHandlers) flags.push('events')
|
|
102
|
+
if (analysis.hasLifecycleHooks) flags.push('lifecycle')
|
|
103
|
+
if (analysis.hasReactiveExpressions) flags.push('reactive')
|
|
104
|
+
if (analysis.usesClientFetch) flags.push('clientFetch')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (analysis.needsSSR) flags.push('SSR')
|
|
108
|
+
if (analysis.usesZenLink) flags.push('SPA')
|
|
109
|
+
|
|
110
|
+
return flags.length > 0 ? flags.join(', ') : 'minimal'
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Determine build output type for a page
|
|
115
|
+
*/
|
|
116
|
+
export type BuildOutputType = 'static' | 'hydrated' | 'ssr'
|
|
117
|
+
|
|
118
|
+
export function getBuildOutputType(analysis: PageAnalysis): BuildOutputType {
|
|
119
|
+
if (analysis.needsSSR) return 'ssr'
|
|
120
|
+
if (analysis.needsHydration) return 'hydrated'
|
|
121
|
+
return 'static'
|
|
122
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { parseTemplate } from '../parse/parseTemplate'
|
|
4
|
+
import { parseScript } from '../parse/parseScript'
|
|
5
|
+
import { extractProps, extractStateDeclarations } from '../parse/scriptAnalysis'
|
|
6
|
+
|
|
7
|
+
export interface LayoutMetadata {
|
|
8
|
+
name: string
|
|
9
|
+
filePath: string
|
|
10
|
+
props: string[]
|
|
11
|
+
states: Map<string, string>
|
|
12
|
+
html: string
|
|
13
|
+
scripts: string[]
|
|
14
|
+
styles: string[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Discover layouts in a directory
|
|
19
|
+
*/
|
|
20
|
+
export function discoverLayouts(layoutsDir: string): Map<string, LayoutMetadata> {
|
|
21
|
+
const layouts = new Map<string, LayoutMetadata>()
|
|
22
|
+
|
|
23
|
+
if (!fs.existsSync(layoutsDir)) return layouts
|
|
24
|
+
|
|
25
|
+
const files = fs.readdirSync(layoutsDir)
|
|
26
|
+
for (const file of files) {
|
|
27
|
+
if (file.endsWith('.zen')) {
|
|
28
|
+
const filePath = path.join(layoutsDir, file)
|
|
29
|
+
const name = path.basename(file, '.zen')
|
|
30
|
+
const source = fs.readFileSync(filePath, 'utf-8')
|
|
31
|
+
|
|
32
|
+
const script = parseScript(source)
|
|
33
|
+
const props = script ? extractProps(script.raw) : []
|
|
34
|
+
const states = script ? extractStateDeclarations(script.raw) : new Map()
|
|
35
|
+
|
|
36
|
+
// Extract styles
|
|
37
|
+
const styles: string[] = []
|
|
38
|
+
const styleRegex = /<style[^>]*>([\s\S]*?)<\/style>/gi
|
|
39
|
+
let match
|
|
40
|
+
while ((match = styleRegex.exec(source)) !== null) {
|
|
41
|
+
if (match[1]) styles.push(match[1].trim())
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Extract HTML (everything except script/style)
|
|
45
|
+
let html = source.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
46
|
+
html = html.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '').trim()
|
|
47
|
+
|
|
48
|
+
layouts.set(name, {
|
|
49
|
+
name,
|
|
50
|
+
filePath,
|
|
51
|
+
props,
|
|
52
|
+
states,
|
|
53
|
+
html,
|
|
54
|
+
scripts: script ? [script.raw] : [],
|
|
55
|
+
styles
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return layouts
|
|
61
|
+
}
|
package/compiler/index.ts
CHANGED
|
@@ -1,44 +1,60 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* Phase 1: Parse & Extract
|
|
5
|
-
* Phase 2: Transform IR → Static HTML + Runtime Bindings
|
|
6
|
-
* Phase 8/9/10: Finalize Output with Validation
|
|
7
|
-
*
|
|
8
|
-
* This compiler observes .zen files, extracts their structure,
|
|
9
|
-
* transforms them into static HTML with explicit bindings,
|
|
10
|
-
* and validates/finalizes output for browser execution.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { parseZenFile } from './parse/parseZenFile'
|
|
1
|
+
import { readFileSync } from 'fs'
|
|
2
|
+
import { parseTemplate } from './parse/parseTemplate'
|
|
3
|
+
import { parseScript } from './parse/parseScript'
|
|
14
4
|
import { transformTemplate } from './transform/transformTemplate'
|
|
15
5
|
import { finalizeOutputOrThrow } from './finalize/finalizeOutput'
|
|
16
|
-
import type { ZenIR } from './ir/types'
|
|
6
|
+
import type { ZenIR, StyleIR } from './ir/types'
|
|
17
7
|
import type { CompiledTemplate } from './output/types'
|
|
18
8
|
import type { FinalizedOutput } from './finalize/finalizeOutput'
|
|
19
9
|
|
|
20
10
|
/**
|
|
21
11
|
* Compile a .zen file into IR and CompiledTemplate
|
|
22
|
-
*
|
|
23
|
-
* Phase 1: Parses and extracts structure
|
|
24
|
-
* Phase 2: Transforms IR into static HTML with bindings
|
|
25
|
-
* Phase 8/9/10: Validates and finalizes output
|
|
26
12
|
*/
|
|
27
|
-
export function compileZen(filePath: string): {
|
|
13
|
+
export function compileZen(filePath: string): {
|
|
28
14
|
ir: ZenIR
|
|
29
15
|
compiled: CompiledTemplate
|
|
30
16
|
finalized?: FinalizedOutput
|
|
31
17
|
} {
|
|
32
|
-
const
|
|
18
|
+
const source = readFileSync(filePath, 'utf-8')
|
|
19
|
+
return compileZenSource(source, filePath)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Compile Zen source string into IR and CompiledTemplate
|
|
24
|
+
*/
|
|
25
|
+
export function compileZenSource(source: string, filePath: string): {
|
|
26
|
+
ir: ZenIR
|
|
27
|
+
compiled: CompiledTemplate
|
|
28
|
+
finalized?: FinalizedOutput
|
|
29
|
+
} {
|
|
30
|
+
// Parse template
|
|
31
|
+
const template = parseTemplate(source, filePath)
|
|
32
|
+
|
|
33
|
+
// Parse script
|
|
34
|
+
const script = parseScript(source)
|
|
35
|
+
|
|
36
|
+
// Parse styles
|
|
37
|
+
const styleRegex = /<style[^>]*>([\s\S]*?)<\/style>/gi
|
|
38
|
+
const styles: StyleIR[] = []
|
|
39
|
+
let match
|
|
40
|
+
while ((match = styleRegex.exec(source)) !== null) {
|
|
41
|
+
if (match[1]) styles.push({ raw: match[1].trim() })
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const ir: ZenIR = {
|
|
45
|
+
filePath,
|
|
46
|
+
template,
|
|
47
|
+
script,
|
|
48
|
+
styles
|
|
49
|
+
}
|
|
50
|
+
|
|
33
51
|
const compiled = transformTemplate(ir)
|
|
34
|
-
|
|
35
|
-
// Phase 8/9/10: Finalize output with validation
|
|
36
|
-
// This ensures build fails on invalid expressions
|
|
52
|
+
|
|
37
53
|
try {
|
|
38
54
|
const finalized = finalizeOutputOrThrow(ir, compiled)
|
|
39
55
|
return { ir, compiled, finalized }
|
|
40
56
|
} catch (error: any) {
|
|
41
|
-
// Re-throw with context
|
|
42
57
|
throw new Error(`Failed to finalize output for ${filePath}:\n${error.message}`)
|
|
43
58
|
}
|
|
44
59
|
}
|
|
60
|
+
|
package/compiler/ir/types.ts
CHANGED
|
@@ -8,15 +8,39 @@
|
|
|
8
8
|
import type { ScriptIR } from '../ir/types'
|
|
9
9
|
|
|
10
10
|
export function parseScript(html: string): ScriptIR | null {
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
const scripts: string[] = []
|
|
12
|
+
const attributes: Record<string, string> = {}
|
|
13
|
+
|
|
14
|
+
const scriptRegex = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi
|
|
15
|
+
let match
|
|
16
|
+
|
|
17
|
+
while ((match = scriptRegex.exec(html)) !== null) {
|
|
18
|
+
const attrString = match[1] || ''
|
|
19
|
+
const content = match[2] || ''
|
|
20
|
+
|
|
21
|
+
// Parse attributes
|
|
22
|
+
const attrRegex = /([a-z0-9-]+)(?:=(?:"([^"]*)"|'([^']*)'|([^>\s]+)))?/gi
|
|
23
|
+
let attrMatch
|
|
24
|
+
while ((attrMatch = attrRegex.exec(attrString)) !== null) {
|
|
25
|
+
const name = attrMatch[1]
|
|
26
|
+
if (name) {
|
|
27
|
+
const value = attrMatch[2] || attrMatch[3] || attrMatch[4] || 'true'
|
|
28
|
+
attributes[name] = value
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (content) {
|
|
33
|
+
scripts.push(content.trim())
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (scripts.length === 0) {
|
|
15
38
|
return null
|
|
16
39
|
}
|
|
17
40
|
|
|
18
41
|
return {
|
|
19
|
-
raw:
|
|
42
|
+
raw: scripts.join('\n\n'),
|
|
43
|
+
attributes
|
|
20
44
|
}
|
|
21
45
|
}
|
|
22
46
|
|