@zenithbuild/core 0.1.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/.eslintignore +15 -0
- package/.gitattributes +2 -0
- package/.github/ISSUE_TEMPLATE/compiler-errors-for-invalid-state-declarations.md +25 -0
- package/.github/ISSUE_TEMPLATE/new_ticket.yaml +34 -0
- package/.github/pull_request_template.md +15 -0
- package/.github/workflows/discord-changelog.yml +141 -0
- package/.github/workflows/discord-notify.yml +242 -0
- package/.github/workflows/discord-version.yml +195 -0
- package/.prettierignore +13 -0
- package/.prettierrc +21 -0
- package/.zen.d.ts +15 -0
- package/LICENSE +21 -0
- package/README.md +55 -0
- package/app/components/Button.zen +46 -0
- package/app/components/Link.zen +11 -0
- package/app/favicon.ico +0 -0
- package/app/layouts/Main.zen +59 -0
- package/app/pages/about.zen +23 -0
- package/app/pages/blog/[id].zen +53 -0
- package/app/pages/blog/index.zen +32 -0
- package/app/pages/dynamic-dx.zen +712 -0
- package/app/pages/dynamic-primitives.zen +453 -0
- package/app/pages/index.zen +154 -0
- package/app/pages/navigation-demo.zen +229 -0
- package/app/pages/posts/[...slug].zen +61 -0
- package/app/pages/primitives-demo.zen +273 -0
- 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 +601 -0
- package/assets/logos/README.md +54 -0
- package/assets/logos/zen.icns +0 -0
- package/bun.lock +39 -0
- package/compiler/README.md +380 -0
- package/compiler/errors/compilerError.ts +24 -0
- package/compiler/finalize/finalizeOutput.ts +163 -0
- package/compiler/finalize/generateFinalBundle.ts +82 -0
- package/compiler/index.ts +44 -0
- package/compiler/ir/types.ts +83 -0
- package/compiler/legacy/binding.ts +254 -0
- package/compiler/legacy/bindings.ts +338 -0
- package/compiler/legacy/component-process.ts +1208 -0
- package/compiler/legacy/component.ts +301 -0
- package/compiler/legacy/event.ts +50 -0
- package/compiler/legacy/expression.ts +1149 -0
- package/compiler/legacy/mutation.ts +280 -0
- package/compiler/legacy/parse.ts +299 -0
- package/compiler/legacy/split.ts +608 -0
- package/compiler/legacy/types.ts +32 -0
- package/compiler/output/types.ts +34 -0
- package/compiler/parse/detectMapExpressions.ts +102 -0
- package/compiler/parse/parseScript.ts +22 -0
- package/compiler/parse/parseTemplate.ts +425 -0
- package/compiler/parse/parseZenFile.ts +66 -0
- package/compiler/parse/trackLoopContext.ts +82 -0
- package/compiler/runtime/dataExposure.ts +291 -0
- package/compiler/runtime/generateDOM.ts +144 -0
- package/compiler/runtime/generateHydrationBundle.ts +383 -0
- package/compiler/runtime/hydration.ts +309 -0
- package/compiler/runtime/navigation.ts +432 -0
- package/compiler/runtime/thinRuntime.ts +160 -0
- package/compiler/runtime/transformIR.ts +256 -0
- package/compiler/runtime/wrapExpression.ts +84 -0
- package/compiler/runtime/wrapExpressionWithLoop.ts +77 -0
- package/compiler/spa-build.ts +1000 -0
- package/compiler/test/validate-test.ts +104 -0
- package/compiler/transform/generateBindings.ts +47 -0
- package/compiler/transform/generateHTML.ts +28 -0
- package/compiler/transform/transformNode.ts +126 -0
- package/compiler/transform/transformTemplate.ts +38 -0
- package/compiler/validate/validateExpressions.ts +168 -0
- package/core/index.ts +135 -0
- package/core/lifecycle/index.ts +49 -0
- package/core/lifecycle/zen-mount.ts +182 -0
- package/core/lifecycle/zen-unmount.ts +88 -0
- package/core/reactivity/index.ts +54 -0
- package/core/reactivity/tracking.ts +167 -0
- package/core/reactivity/zen-batch.ts +57 -0
- package/core/reactivity/zen-effect.ts +139 -0
- package/core/reactivity/zen-memo.ts +146 -0
- package/core/reactivity/zen-ref.ts +52 -0
- package/core/reactivity/zen-signal.ts +121 -0
- package/core/reactivity/zen-state.ts +180 -0
- package/core/reactivity/zen-untrack.ts +44 -0
- package/docs/COMMENTS.md +111 -0
- package/docs/COMMITS.md +36 -0
- package/docs/CONTRIBUTING.md +116 -0
- package/docs/STYLEGUIDE.md +62 -0
- package/package.json +44 -0
- package/router/index.ts +76 -0
- package/router/manifest.ts +314 -0
- package/router/navigation/ZenLink.zen +231 -0
- package/router/navigation/index.ts +78 -0
- package/router/navigation/zen-link.ts +584 -0
- package/router/runtime.ts +458 -0
- package/router/types.ts +168 -0
- package/runtime/build.ts +17 -0
- package/runtime/serve.ts +93 -0
- package/scripts/webhook-proxy.ts +213 -0
- package/tsconfig.json +28 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Cases for Expression Validation
|
|
3
|
+
*
|
|
4
|
+
* Phase 8/9/10: Tests that invalid expressions fail the build
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { validateExpressions, validateExpressionsOrThrow } from '../validate/validateExpressions'
|
|
8
|
+
import type { ExpressionIR } from '../ir/types'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Test valid expressions
|
|
12
|
+
*/
|
|
13
|
+
function testValidExpressions() {
|
|
14
|
+
const validExpressions: ExpressionIR[] = [
|
|
15
|
+
{
|
|
16
|
+
id: 'expr_0',
|
|
17
|
+
code: 'user.name',
|
|
18
|
+
location: { line: 10, column: 5 }
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'expr_1',
|
|
22
|
+
code: 'count + 1',
|
|
23
|
+
location: { line: 11, column: 8 }
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'expr_2',
|
|
27
|
+
code: 'isActive ? "on" : "off"',
|
|
28
|
+
location: { line: 12, column: 12 }
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
const result = validateExpressions(validExpressions, 'test.zen')
|
|
33
|
+
console.assert(result.valid === true, 'Valid expressions should pass validation')
|
|
34
|
+
console.assert(result.errors.length === 0, 'Valid expressions should have no errors')
|
|
35
|
+
console.log('✅ Valid expressions test passed')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Test invalid expressions
|
|
40
|
+
*/
|
|
41
|
+
function testInvalidExpressions() {
|
|
42
|
+
const invalidExpressions: ExpressionIR[] = [
|
|
43
|
+
{
|
|
44
|
+
id: 'expr_0',
|
|
45
|
+
code: 'user.name}', // Mismatched brace
|
|
46
|
+
location: { line: 10, column: 5 }
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
const result = validateExpressions(invalidExpressions, 'test.zen')
|
|
51
|
+
console.assert(result.valid === false, 'Invalid expressions should fail validation')
|
|
52
|
+
console.assert(result.errors.length > 0, 'Invalid expressions should have errors')
|
|
53
|
+
console.log('✅ Invalid expressions test passed')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Test unsafe code detection
|
|
58
|
+
*/
|
|
59
|
+
function testUnsafeCode() {
|
|
60
|
+
const unsafeExpressions: ExpressionIR[] = [
|
|
61
|
+
{
|
|
62
|
+
id: 'expr_0',
|
|
63
|
+
code: 'eval("alert(1)")',
|
|
64
|
+
location: { line: 10, column: 5 }
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
const result = validateExpressions(unsafeExpressions, 'test.zen')
|
|
69
|
+
console.assert(result.valid === false, 'Unsafe code should fail validation')
|
|
70
|
+
console.assert(result.errors.length > 0, 'Unsafe code should have errors')
|
|
71
|
+
console.log('✅ Unsafe code detection test passed')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Test validateExpressionsOrThrow
|
|
76
|
+
*/
|
|
77
|
+
function testThrowOnInvalid() {
|
|
78
|
+
const invalidExpressions: ExpressionIR[] = [
|
|
79
|
+
{
|
|
80
|
+
id: 'expr_0',
|
|
81
|
+
code: 'user.name}', // Mismatched brace
|
|
82
|
+
location: { line: 10, column: 5 }
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
validateExpressionsOrThrow(invalidExpressions, 'test.zen')
|
|
88
|
+
console.assert(false, 'Should have thrown on invalid expressions')
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.assert(error instanceof Error, 'Should throw Error')
|
|
91
|
+
console.log('✅ Throw on invalid expressions test passed')
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Run tests
|
|
96
|
+
if (require.main === module) {
|
|
97
|
+
console.log('Running validation tests...')
|
|
98
|
+
testValidExpressions()
|
|
99
|
+
testInvalidExpressions()
|
|
100
|
+
testUnsafeCode()
|
|
101
|
+
testThrowOnInvalid()
|
|
102
|
+
console.log('✅ All validation tests passed!')
|
|
103
|
+
}
|
|
104
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate Bindings
|
|
3
|
+
*
|
|
4
|
+
* This module is handled by transformNode, but kept here for future extensibility
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Binding } from '../output/types'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Validate bindings structure
|
|
11
|
+
*/
|
|
12
|
+
export function validateBindings(bindings: Binding[]): void {
|
|
13
|
+
for (const binding of bindings) {
|
|
14
|
+
if (!binding.id || !binding.type || !binding.target || !binding.expression) {
|
|
15
|
+
throw new Error(`Invalid binding: ${JSON.stringify(binding)}`)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (binding.type !== 'text' && binding.type !== 'attribute') {
|
|
19
|
+
throw new Error(`Invalid binding type: ${binding.type}`)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (binding.type === 'text' && binding.target !== 'data-zen-text') {
|
|
23
|
+
throw new Error(`Text binding must have target 'data-zen-text', got: ${binding.target}`)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (binding.type === 'attribute' && !binding.target.startsWith('data-zen-attr-')) {
|
|
27
|
+
// This is handled in transformNode, but validate here too
|
|
28
|
+
// Actually, the target should be the attribute name (e.g., "class")
|
|
29
|
+
// and we prepend "data-zen-attr-" when generating HTML
|
|
30
|
+
// So this validation is correct
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Sort bindings by location for deterministic output
|
|
37
|
+
*/
|
|
38
|
+
export function sortBindings(bindings: Binding[]): Binding[] {
|
|
39
|
+
return [...bindings].sort((a, b) => {
|
|
40
|
+
if (!a.location || !b.location) return 0
|
|
41
|
+
if (a.location.line !== b.location.line) {
|
|
42
|
+
return a.location.line - b.location.line
|
|
43
|
+
}
|
|
44
|
+
return a.location.column - b.location.column
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate Static HTML from Transformed Nodes
|
|
3
|
+
*
|
|
4
|
+
* This generates pure HTML with no expressions or runtime code
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { TemplateNode } from '../ir/types'
|
|
8
|
+
import { transformNode } from './transformNode'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generate HTML string from template nodes
|
|
12
|
+
*/
|
|
13
|
+
export function generateHTML(
|
|
14
|
+
nodes: TemplateNode[],
|
|
15
|
+
expressions: any[]
|
|
16
|
+
): { html: string; bindings: any[] } {
|
|
17
|
+
let html = ''
|
|
18
|
+
const allBindings: any[] = []
|
|
19
|
+
|
|
20
|
+
for (const node of nodes) {
|
|
21
|
+
const { html: nodeHtml, bindings } = transformNode(node, expressions)
|
|
22
|
+
html += nodeHtml
|
|
23
|
+
allBindings.push(...bindings)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return { html, bindings: allBindings }
|
|
27
|
+
}
|
|
28
|
+
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transform Template Nodes
|
|
3
|
+
*
|
|
4
|
+
* Transforms IR nodes into HTML strings and collects bindings
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { TemplateNode, ElementNode, TextNode, ExpressionNode, ExpressionIR, LoopContext } from '../ir/types'
|
|
8
|
+
import type { Binding } from '../output/types'
|
|
9
|
+
|
|
10
|
+
let bindingIdCounter = 0
|
|
11
|
+
|
|
12
|
+
function generateBindingId(): string {
|
|
13
|
+
return `exp_${bindingIdCounter++}`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Transform a template node to HTML and collect bindings
|
|
18
|
+
* Phase 7: Supports loop context propagation for map expressions
|
|
19
|
+
*/
|
|
20
|
+
export function transformNode(
|
|
21
|
+
node: TemplateNode,
|
|
22
|
+
expressions: ExpressionIR[],
|
|
23
|
+
parentLoopContext?: LoopContext // Phase 7: Loop context from parent map expressions
|
|
24
|
+
): { html: string; bindings: Binding[] } {
|
|
25
|
+
const bindings: Binding[] = []
|
|
26
|
+
|
|
27
|
+
function transform(node: TemplateNode, loopContext?: LoopContext): string {
|
|
28
|
+
switch (node.type) {
|
|
29
|
+
case 'text':
|
|
30
|
+
return escapeHtml((node as TextNode).value)
|
|
31
|
+
|
|
32
|
+
case 'expression': {
|
|
33
|
+
const exprNode = node as ExpressionNode
|
|
34
|
+
// Find the expression in the expressions array
|
|
35
|
+
const expr = expressions.find(e => e.id === exprNode.expression)
|
|
36
|
+
if (!expr) {
|
|
37
|
+
throw new Error(`Expression ${exprNode.expression} not found`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const bindingId = generateBindingId()
|
|
41
|
+
// Phase 7: Use loop context from ExpressionNode if available, otherwise use passed context
|
|
42
|
+
const activeLoopContext = exprNode.loopContext || loopContext
|
|
43
|
+
|
|
44
|
+
bindings.push({
|
|
45
|
+
id: bindingId,
|
|
46
|
+
type: 'text',
|
|
47
|
+
target: 'data-zen-text',
|
|
48
|
+
expression: expr.code,
|
|
49
|
+
location: expr.location,
|
|
50
|
+
loopContext: activeLoopContext // Phase 7: Attach loop context to binding
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
return `<span data-zen-text="${bindingId}"></span>`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
case 'element': {
|
|
57
|
+
const elNode = node as ElementNode
|
|
58
|
+
const tag = elNode.tag
|
|
59
|
+
|
|
60
|
+
// Build attributes
|
|
61
|
+
const attrs: string[] = []
|
|
62
|
+
for (const attr of elNode.attributes) {
|
|
63
|
+
if (typeof attr.value === 'string') {
|
|
64
|
+
// Static attribute
|
|
65
|
+
const value = escapeHtml(attr.value)
|
|
66
|
+
attrs.push(`${attr.name}="${value}"`)
|
|
67
|
+
} else {
|
|
68
|
+
// Expression attribute
|
|
69
|
+
const expr = attr.value as ExpressionIR
|
|
70
|
+
const bindingId = generateBindingId()
|
|
71
|
+
// Phase 7: Use loop context from AttributeIR if available, otherwise use element's loop context
|
|
72
|
+
const activeLoopContext = attr.loopContext || loopContext
|
|
73
|
+
|
|
74
|
+
bindings.push({
|
|
75
|
+
id: bindingId,
|
|
76
|
+
type: 'attribute',
|
|
77
|
+
target: attr.name, // e.g., "class", "style"
|
|
78
|
+
expression: expr.code,
|
|
79
|
+
location: expr.location,
|
|
80
|
+
loopContext: activeLoopContext // Phase 7: Attach loop context to binding
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
// Use data-zen-attr-{name} for attribute expressions
|
|
84
|
+
attrs.push(`data-zen-attr-${attr.name}="${bindingId}"`)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const attrStr = attrs.length > 0 ? ' ' + attrs.join(' ') : ''
|
|
89
|
+
|
|
90
|
+
// Phase 7: Use loop context from ElementNode if available, otherwise use passed context
|
|
91
|
+
const activeLoopContext = elNode.loopContext || loopContext
|
|
92
|
+
|
|
93
|
+
// Transform children
|
|
94
|
+
const childrenHtml = elNode.children.map(child => transform(child, activeLoopContext)).join('')
|
|
95
|
+
|
|
96
|
+
// Self-closing tags
|
|
97
|
+
const voidElements = new Set([
|
|
98
|
+
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
|
99
|
+
'link', 'meta', 'param', 'source', 'track', 'wbr'
|
|
100
|
+
])
|
|
101
|
+
|
|
102
|
+
if (voidElements.has(tag.toLowerCase()) && childrenHtml === '') {
|
|
103
|
+
return `<${tag}${attrStr} />`
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return `<${tag}${attrStr}>${childrenHtml}</${tag}>`
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const html = transform(node, parentLoopContext)
|
|
112
|
+
return { html, bindings }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Escape HTML special characters
|
|
117
|
+
*/
|
|
118
|
+
function escapeHtml(text: string): string {
|
|
119
|
+
return text
|
|
120
|
+
.replace(/&/g, '&')
|
|
121
|
+
.replace(/</g, '<')
|
|
122
|
+
.replace(/>/g, '>')
|
|
123
|
+
.replace(/"/g, '"')
|
|
124
|
+
.replace(/'/g, ''')
|
|
125
|
+
}
|
|
126
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transform Template IR to Compiled Template
|
|
3
|
+
*
|
|
4
|
+
* Phase 2: Transform IR → Static HTML + Runtime Bindings
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ZenIR } from '../ir/types'
|
|
8
|
+
import type { CompiledTemplate } from '../output/types'
|
|
9
|
+
import { generateHTML } from './generateHTML'
|
|
10
|
+
import { validateBindings, sortBindings } from './generateBindings'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Transform a ZenIR into CompiledTemplate
|
|
14
|
+
*/
|
|
15
|
+
export function transformTemplate(ir: ZenIR): CompiledTemplate {
|
|
16
|
+
// Generate HTML and collect bindings
|
|
17
|
+
const { html, bindings } = generateHTML(ir.template.nodes, ir.template.expressions)
|
|
18
|
+
|
|
19
|
+
// Validate bindings
|
|
20
|
+
validateBindings(bindings)
|
|
21
|
+
|
|
22
|
+
// Sort bindings by location for deterministic output
|
|
23
|
+
const sortedBindings = sortBindings(bindings)
|
|
24
|
+
|
|
25
|
+
// Extract scripts (raw content, pass through)
|
|
26
|
+
const scripts = ir.script ? ir.script.raw : null
|
|
27
|
+
|
|
28
|
+
// Extract styles (raw content, pass through)
|
|
29
|
+
const styles = ir.styles.map(s => s.raw)
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
html,
|
|
33
|
+
bindings: sortedBindings,
|
|
34
|
+
scripts,
|
|
35
|
+
styles
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expression Validation
|
|
3
|
+
*
|
|
4
|
+
* Phase 8/9/10: Compile-time validation of all expressions
|
|
5
|
+
*
|
|
6
|
+
* Ensures all expressions are valid JavaScript and will not cause runtime errors.
|
|
7
|
+
* Build fails immediately if any expression is invalid.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ExpressionIR } from '../ir/types'
|
|
11
|
+
import { CompilerError } from '../errors/compilerError'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Validation result
|
|
15
|
+
*/
|
|
16
|
+
export interface ValidationResult {
|
|
17
|
+
valid: boolean
|
|
18
|
+
errors: CompilerError[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Validate all expressions in the IR
|
|
23
|
+
*
|
|
24
|
+
* @param expressions - Array of expressions to validate
|
|
25
|
+
* @param filePath - Source file path for error reporting
|
|
26
|
+
* @returns Validation result with errors
|
|
27
|
+
*/
|
|
28
|
+
export function validateExpressions(
|
|
29
|
+
expressions: ExpressionIR[],
|
|
30
|
+
filePath: string
|
|
31
|
+
): ValidationResult {
|
|
32
|
+
const errors: CompilerError[] = []
|
|
33
|
+
|
|
34
|
+
for (const expr of expressions) {
|
|
35
|
+
const exprErrors = validateSingleExpression(expr, filePath)
|
|
36
|
+
errors.push(...exprErrors)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
valid: errors.length === 0,
|
|
41
|
+
errors
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Validate a single expression
|
|
47
|
+
*/
|
|
48
|
+
function validateSingleExpression(
|
|
49
|
+
expr: ExpressionIR,
|
|
50
|
+
filePath: string
|
|
51
|
+
): CompilerError[] {
|
|
52
|
+
const errors: CompilerError[] = []
|
|
53
|
+
const { id, code, location } = expr
|
|
54
|
+
|
|
55
|
+
// Basic syntax validation using a safe approach
|
|
56
|
+
// We don't execute the code, just validate syntax
|
|
57
|
+
// Note: Expressions may contain JSX/HTML syntax (e.g., condition && <element>)
|
|
58
|
+
// which is not valid JavaScript but is valid in our expression language.
|
|
59
|
+
// We skip strict JavaScript validation for expressions that contain JSX.
|
|
60
|
+
|
|
61
|
+
const hasJSX = /<[a-zA-Z]/.test(code) || /\/>/.test(code)
|
|
62
|
+
|
|
63
|
+
if (!hasJSX) {
|
|
64
|
+
// Only validate JavaScript syntax if there's no JSX
|
|
65
|
+
// Note: Using Function constructor here is for syntax validation only (compile-time)
|
|
66
|
+
// This is safe because:
|
|
67
|
+
// 1. It's only called at compile time, not runtime
|
|
68
|
+
// 2. We're only checking syntax, not executing the code
|
|
69
|
+
// 3. The actual runtime uses pre-compiled functions, not Function constructor
|
|
70
|
+
try {
|
|
71
|
+
// Use Function constructor to validate syntax (doesn't execute)
|
|
72
|
+
// This is compile-time only - runtime never uses Function constructor
|
|
73
|
+
new Function('state', 'loaderData', 'props', 'stores', `return ${code}`)
|
|
74
|
+
} catch (error: any) {
|
|
75
|
+
errors.push(
|
|
76
|
+
new CompilerError(
|
|
77
|
+
`Invalid expression syntax: ${code}\n${error.message}`,
|
|
78
|
+
filePath,
|
|
79
|
+
location.line,
|
|
80
|
+
location.column
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// If hasJSX, we skip JavaScript validation - JSX syntax is handled by the parser/runtime
|
|
86
|
+
|
|
87
|
+
// Check for dangerous patterns
|
|
88
|
+
if (code.includes('eval(') || code.includes('Function(') || code.includes('with (')) {
|
|
89
|
+
errors.push(
|
|
90
|
+
new CompilerError(
|
|
91
|
+
`Expression contains unsafe code: ${code}`,
|
|
92
|
+
filePath,
|
|
93
|
+
location.line,
|
|
94
|
+
location.column
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Check for undefined global references (basic heuristic)
|
|
100
|
+
// This is a simple check - can be enhanced with AST parsing
|
|
101
|
+
const globalPattern = /\b(window|document|console|globalThis)\./g
|
|
102
|
+
const matches = code.match(globalPattern)
|
|
103
|
+
if (matches && matches.length > 0) {
|
|
104
|
+
// Warn but don't fail - some global access might be intentional
|
|
105
|
+
// In a stricter mode, we could fail here
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Check for common syntax errors
|
|
109
|
+
const openBraces = (code.match(/\{/g) || []).length
|
|
110
|
+
const closeBraces = (code.match(/\}/g) || []).length
|
|
111
|
+
const openParens = (code.match(/\(/g) || []).length
|
|
112
|
+
const closeParens = (code.match(/\)/g) || []).length
|
|
113
|
+
const openBrackets = (code.match(/\[/g) || []).length
|
|
114
|
+
const closeBrackets = (code.match(/\]/g) || []).length
|
|
115
|
+
|
|
116
|
+
if (openBraces !== closeBraces) {
|
|
117
|
+
errors.push(
|
|
118
|
+
new CompilerError(
|
|
119
|
+
`Mismatched braces in expression: ${code}`,
|
|
120
|
+
filePath,
|
|
121
|
+
location.line,
|
|
122
|
+
location.column
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (openParens !== closeParens) {
|
|
128
|
+
errors.push(
|
|
129
|
+
new CompilerError(
|
|
130
|
+
`Mismatched parentheses in expression: ${code}`,
|
|
131
|
+
filePath,
|
|
132
|
+
location.line,
|
|
133
|
+
location.column
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (openBrackets !== closeBrackets) {
|
|
139
|
+
errors.push(
|
|
140
|
+
new CompilerError(
|
|
141
|
+
`Mismatched brackets in expression: ${code}`,
|
|
142
|
+
filePath,
|
|
143
|
+
location.line,
|
|
144
|
+
location.column
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return errors
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Validate and throw if invalid
|
|
154
|
+
*
|
|
155
|
+
* @throws CompilerError if any expression is invalid
|
|
156
|
+
*/
|
|
157
|
+
export function validateExpressionsOrThrow(
|
|
158
|
+
expressions: ExpressionIR[],
|
|
159
|
+
filePath: string
|
|
160
|
+
): void {
|
|
161
|
+
const result = validateExpressions(expressions, filePath)
|
|
162
|
+
|
|
163
|
+
if (!result.valid && result.errors.length > 0) {
|
|
164
|
+
// Throw the first error (can be enhanced to collect all)
|
|
165
|
+
throw result.errors[0]
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
package/core/index.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zenith Core Runtime
|
|
3
|
+
*
|
|
4
|
+
* This is the foundational layer of the Zenith framework, providing:
|
|
5
|
+
* - Reactive primitives (signals, state, effects, memos)
|
|
6
|
+
* - Lifecycle hooks (onMount, onUnmount)
|
|
7
|
+
*
|
|
8
|
+
* Design principles:
|
|
9
|
+
* - Auto-tracked reactivity (no dependency arrays)
|
|
10
|
+
* - No VDOM or render loops
|
|
11
|
+
* - Runtime-agnostic (works in browser, SSR, tests)
|
|
12
|
+
* - Hybrid naming: internal `zen*` + public clean names
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* // Using clean names (recommended for application code)
|
|
17
|
+
* import { signal, effect, onMount } from 'zenith/core'
|
|
18
|
+
*
|
|
19
|
+
* const count = signal(0)
|
|
20
|
+
*
|
|
21
|
+
* effect(() => {
|
|
22
|
+
* console.log('Count:', count())
|
|
23
|
+
* })
|
|
24
|
+
*
|
|
25
|
+
* onMount(() => {
|
|
26
|
+
* console.log('Mounted!')
|
|
27
|
+
* })
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* // Using explicit zen* names (for library/internal code)
|
|
33
|
+
* import { zenSignal, zenEffect, zenOnMount } from 'zenith/core'
|
|
34
|
+
*
|
|
35
|
+
* const count = zenSignal(0)
|
|
36
|
+
* zenEffect(() => console.log(count()))
|
|
37
|
+
* zenOnMount(() => console.log('Ready'))
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* // For navigation, import from router
|
|
43
|
+
* import { navigate, isActive } from 'zenith/router'
|
|
44
|
+
*
|
|
45
|
+
* navigate('/about')
|
|
46
|
+
* if (isActive('/blog')) {
|
|
47
|
+
* // Handle active state
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
// ============================================
|
|
53
|
+
// Reactivity Primitives
|
|
54
|
+
// ============================================
|
|
55
|
+
|
|
56
|
+
// Explicit zen* exports (internal naming)
|
|
57
|
+
export {
|
|
58
|
+
zenSignal,
|
|
59
|
+
zenState,
|
|
60
|
+
zenEffect,
|
|
61
|
+
zenMemo,
|
|
62
|
+
zenRef,
|
|
63
|
+
zenBatch,
|
|
64
|
+
zenUntrack
|
|
65
|
+
} from './reactivity'
|
|
66
|
+
|
|
67
|
+
// Types
|
|
68
|
+
export type {
|
|
69
|
+
Signal,
|
|
70
|
+
Memo,
|
|
71
|
+
Ref,
|
|
72
|
+
EffectFn,
|
|
73
|
+
DisposeFn,
|
|
74
|
+
Subscriber,
|
|
75
|
+
TrackingContext
|
|
76
|
+
} from './reactivity'
|
|
77
|
+
|
|
78
|
+
// Clean name exports (public DX)
|
|
79
|
+
export {
|
|
80
|
+
signal,
|
|
81
|
+
state,
|
|
82
|
+
effect,
|
|
83
|
+
memo,
|
|
84
|
+
ref,
|
|
85
|
+
batch,
|
|
86
|
+
untrack
|
|
87
|
+
} from './reactivity'
|
|
88
|
+
|
|
89
|
+
// Internal tracking utilities (advanced use)
|
|
90
|
+
export {
|
|
91
|
+
trackDependency,
|
|
92
|
+
notifySubscribers,
|
|
93
|
+
getCurrentContext,
|
|
94
|
+
pushContext,
|
|
95
|
+
popContext,
|
|
96
|
+
cleanupContext,
|
|
97
|
+
runUntracked,
|
|
98
|
+
startBatch,
|
|
99
|
+
endBatch,
|
|
100
|
+
isBatching
|
|
101
|
+
} from './reactivity'
|
|
102
|
+
|
|
103
|
+
// ============================================
|
|
104
|
+
// Lifecycle Hooks
|
|
105
|
+
// ============================================
|
|
106
|
+
|
|
107
|
+
// Explicit zen* exports (internal naming)
|
|
108
|
+
export {
|
|
109
|
+
zenOnMount,
|
|
110
|
+
zenOnUnmount
|
|
111
|
+
} from './lifecycle'
|
|
112
|
+
|
|
113
|
+
// Clean name exports (public DX)
|
|
114
|
+
export {
|
|
115
|
+
onMount,
|
|
116
|
+
onUnmount
|
|
117
|
+
} from './lifecycle'
|
|
118
|
+
|
|
119
|
+
// Types
|
|
120
|
+
export type {
|
|
121
|
+
MountCallback,
|
|
122
|
+
UnmountCallback
|
|
123
|
+
} from './lifecycle'
|
|
124
|
+
|
|
125
|
+
// Internal lifecycle utilities (for component system)
|
|
126
|
+
export {
|
|
127
|
+
triggerMount,
|
|
128
|
+
triggerUnmount,
|
|
129
|
+
executeUnmountCallbacks,
|
|
130
|
+
getIsMounted,
|
|
131
|
+
getUnmountCallbackCount,
|
|
132
|
+
resetMountState,
|
|
133
|
+
resetUnmountState
|
|
134
|
+
} from './lifecycle'
|
|
135
|
+
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zenith Lifecycle Hooks
|
|
3
|
+
*
|
|
4
|
+
* This module exports lifecycle hooks for component mount/unmount events.
|
|
5
|
+
* These are effect wrappers that integrate with the component lifecycle system.
|
|
6
|
+
*
|
|
7
|
+
* Exports both explicit `zen*` names (internal) and clean aliases (public DX).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Import lifecycle hooks
|
|
11
|
+
import {
|
|
12
|
+
zenOnMount as _zenOnMount,
|
|
13
|
+
triggerMount,
|
|
14
|
+
triggerUnmount,
|
|
15
|
+
getIsMounted,
|
|
16
|
+
resetMountState,
|
|
17
|
+
type MountCallback
|
|
18
|
+
} from './zen-mount'
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
zenOnUnmount as _zenOnUnmount,
|
|
22
|
+
executeUnmountCallbacks,
|
|
23
|
+
getUnmountCallbackCount,
|
|
24
|
+
resetUnmountState,
|
|
25
|
+
type UnmountCallback
|
|
26
|
+
} from './zen-unmount'
|
|
27
|
+
|
|
28
|
+
// Re-export with explicit names
|
|
29
|
+
export const zenOnMount = _zenOnMount
|
|
30
|
+
export const zenOnUnmount = _zenOnUnmount
|
|
31
|
+
|
|
32
|
+
// Re-export utilities
|
|
33
|
+
export {
|
|
34
|
+
triggerMount,
|
|
35
|
+
triggerUnmount,
|
|
36
|
+
getIsMounted,
|
|
37
|
+
resetMountState,
|
|
38
|
+
executeUnmountCallbacks,
|
|
39
|
+
getUnmountCallbackCount,
|
|
40
|
+
resetUnmountState
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Re-export types
|
|
44
|
+
export type { MountCallback, UnmountCallback }
|
|
45
|
+
|
|
46
|
+
// Public DX aliases - clean names
|
|
47
|
+
export const onMount = _zenOnMount
|
|
48
|
+
export const onUnmount = _zenOnUnmount
|
|
49
|
+
|