@zenithbuild/compiler 1.0.2
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 +21 -0
- package/README.md +30 -0
- package/dist/build-analyzer.d.ts +44 -0
- package/dist/build-analyzer.js +87 -0
- package/dist/bundler.d.ts +31 -0
- package/dist/bundler.js +86 -0
- package/dist/core/components/index.d.ts +9 -0
- package/dist/core/components/index.js +13 -0
- package/dist/core/config/index.d.ts +11 -0
- package/dist/core/config/index.js +10 -0
- package/dist/core/config/loader.d.ts +17 -0
- package/dist/core/config/loader.js +60 -0
- package/dist/core/config/types.d.ts +98 -0
- package/dist/core/config/types.js +32 -0
- package/dist/core/index.d.ts +7 -0
- package/dist/core/index.js +6 -0
- package/dist/core/lifecycle/index.d.ts +16 -0
- package/dist/core/lifecycle/index.js +19 -0
- package/dist/core/lifecycle/zen-mount.d.ts +66 -0
- package/dist/core/lifecycle/zen-mount.js +151 -0
- package/dist/core/lifecycle/zen-unmount.d.ts +54 -0
- package/dist/core/lifecycle/zen-unmount.js +76 -0
- package/dist/core/plugins/bridge.d.ts +116 -0
- package/dist/core/plugins/bridge.js +121 -0
- package/dist/core/plugins/index.d.ts +6 -0
- package/dist/core/plugins/index.js +6 -0
- package/dist/core/plugins/registry.d.ts +67 -0
- package/dist/core/plugins/registry.js +113 -0
- package/dist/core/reactivity/index.d.ts +30 -0
- package/dist/core/reactivity/index.js +33 -0
- package/dist/core/reactivity/tracking.d.ts +74 -0
- package/dist/core/reactivity/tracking.js +136 -0
- package/dist/core/reactivity/zen-batch.d.ts +45 -0
- package/dist/core/reactivity/zen-batch.js +54 -0
- package/dist/core/reactivity/zen-effect.d.ts +48 -0
- package/dist/core/reactivity/zen-effect.js +98 -0
- package/dist/core/reactivity/zen-memo.d.ts +43 -0
- package/dist/core/reactivity/zen-memo.js +100 -0
- package/dist/core/reactivity/zen-ref.d.ts +44 -0
- package/dist/core/reactivity/zen-ref.js +34 -0
- package/dist/core/reactivity/zen-signal.d.ts +48 -0
- package/dist/core/reactivity/zen-signal.js +84 -0
- package/dist/core/reactivity/zen-state.d.ts +35 -0
- package/dist/core/reactivity/zen-state.js +147 -0
- package/dist/core/reactivity/zen-untrack.d.ts +38 -0
- package/dist/core/reactivity/zen-untrack.js +41 -0
- package/dist/css/index.d.ts +73 -0
- package/dist/css/index.js +246 -0
- package/dist/discovery/componentDiscovery.d.ts +42 -0
- package/dist/discovery/componentDiscovery.js +56 -0
- package/dist/discovery/layouts.d.ts +13 -0
- package/dist/discovery/layouts.js +41 -0
- package/dist/errors/compilerError.d.ts +31 -0
- package/dist/errors/compilerError.js +51 -0
- package/dist/finalize/finalizeOutput.d.ts +32 -0
- package/dist/finalize/finalizeOutput.js +62 -0
- package/dist/finalize/generateFinalBundle.d.ts +24 -0
- package/dist/finalize/generateFinalBundle.js +68 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +51 -0
- package/dist/ir/types.d.ts +181 -0
- package/dist/ir/types.js +8 -0
- package/dist/output/types.d.ts +30 -0
- package/dist/output/types.js +6 -0
- package/dist/parse/detectMapExpressions.d.ts +45 -0
- package/dist/parse/detectMapExpressions.js +77 -0
- package/dist/parse/parseScript.d.ts +8 -0
- package/dist/parse/parseScript.js +36 -0
- package/dist/parse/parseTemplate.d.ts +11 -0
- package/dist/parse/parseTemplate.js +487 -0
- package/dist/parse/parseZenFile.d.ts +11 -0
- package/dist/parse/parseZenFile.js +50 -0
- package/dist/parse/scriptAnalysis.d.ts +25 -0
- package/dist/parse/scriptAnalysis.js +60 -0
- package/dist/parse/trackLoopContext.d.ts +20 -0
- package/dist/parse/trackLoopContext.js +62 -0
- package/dist/parseZenFile.d.ts +10 -0
- package/dist/parseZenFile.js +55 -0
- package/dist/runtime/analyzeAndEmit.d.ts +20 -0
- package/dist/runtime/analyzeAndEmit.js +70 -0
- package/dist/runtime/build.d.ts +6 -0
- package/dist/runtime/build.js +13 -0
- package/dist/runtime/bundle-generator.d.ts +27 -0
- package/dist/runtime/bundle-generator.js +1263 -0
- package/dist/runtime/client-runtime.d.ts +41 -0
- package/dist/runtime/client-runtime.js +397 -0
- package/dist/runtime/dataExposure.d.ts +52 -0
- package/dist/runtime/dataExposure.js +227 -0
- package/dist/runtime/generateDOM.d.ts +21 -0
- package/dist/runtime/generateDOM.js +194 -0
- package/dist/runtime/generateHydrationBundle.d.ts +15 -0
- package/dist/runtime/generateHydrationBundle.js +399 -0
- package/dist/runtime/hydration.d.ts +53 -0
- package/dist/runtime/hydration.js +271 -0
- package/dist/runtime/navigation.d.ts +58 -0
- package/dist/runtime/navigation.js +372 -0
- package/dist/runtime/serve.d.ts +13 -0
- package/dist/runtime/serve.js +76 -0
- package/dist/runtime/thinRuntime.d.ts +23 -0
- package/dist/runtime/thinRuntime.js +158 -0
- package/dist/runtime/transformIR.d.ts +19 -0
- package/dist/runtime/transformIR.js +285 -0
- package/dist/runtime/wrapExpression.d.ts +24 -0
- package/dist/runtime/wrapExpression.js +76 -0
- package/dist/runtime/wrapExpressionWithLoop.d.ts +17 -0
- package/dist/runtime/wrapExpressionWithLoop.js +75 -0
- package/dist/spa-build.d.ts +26 -0
- package/dist/spa-build.js +866 -0
- package/dist/ssg-build.d.ts +32 -0
- package/dist/ssg-build.js +408 -0
- package/dist/test/analyze-emit.test.d.ts +1 -0
- package/dist/test/analyze-emit.test.js +88 -0
- package/dist/test/bundler-contract.test.d.ts +1 -0
- package/dist/test/bundler-contract.test.js +137 -0
- package/dist/test/compiler-authority.test.d.ts +1 -0
- package/dist/test/compiler-authority.test.js +90 -0
- package/dist/test/component-instance-test.d.ts +1 -0
- package/dist/test/component-instance-test.js +115 -0
- package/dist/test/error-native-bridge.test.d.ts +1 -0
- package/dist/test/error-native-bridge.test.js +51 -0
- package/dist/test/error-serialization.test.d.ts +1 -0
- package/dist/test/error-serialization.test.js +38 -0
- package/dist/test/macro-inlining.test.d.ts +1 -0
- package/dist/test/macro-inlining.test.js +178 -0
- package/dist/test/validate-test.d.ts +6 -0
- package/dist/test/validate-test.js +95 -0
- package/dist/transform/classifyExpression.d.ts +46 -0
- package/dist/transform/classifyExpression.js +354 -0
- package/dist/transform/componentResolver.d.ts +15 -0
- package/dist/transform/componentResolver.js +30 -0
- package/dist/transform/expressionTransformer.d.ts +19 -0
- package/dist/transform/expressionTransformer.js +333 -0
- package/dist/transform/fragmentLowering.d.ts +25 -0
- package/dist/transform/fragmentLowering.js +468 -0
- package/dist/transform/layoutProcessor.d.ts +5 -0
- package/dist/transform/layoutProcessor.js +34 -0
- package/dist/transform/transformTemplate.d.ts +11 -0
- package/dist/transform/transformTemplate.js +33 -0
- package/dist/validate/invariants.d.ts +23 -0
- package/dist/validate/invariants.js +55 -0
- package/native/compiler-native/compiler-native.node +0 -0
- package/native/compiler-native/index.d.ts +113 -0
- package/native/compiler-native/index.js +19 -0
- package/native/compiler-native/package.json +19 -0
- package/package.json +49 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { expect, test, describe } from "bun:test";
|
|
2
|
+
import { analyzeAndEmit } from "../runtime/analyzeAndEmit";
|
|
3
|
+
function createIR(templateNodes = [], expressions = [], script = "") {
|
|
4
|
+
return {
|
|
5
|
+
filePath: "test.zen",
|
|
6
|
+
template: {
|
|
7
|
+
raw: "",
|
|
8
|
+
nodes: templateNodes,
|
|
9
|
+
expressions: expressions
|
|
10
|
+
},
|
|
11
|
+
script: {
|
|
12
|
+
raw: script,
|
|
13
|
+
attributes: {}
|
|
14
|
+
},
|
|
15
|
+
styles: [],
|
|
16
|
+
componentScripts: []
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
describe("Rust Compiler Authority", () => {
|
|
20
|
+
test("JSX transformation into __zenith.h", async () => {
|
|
21
|
+
const ir = createIR([], [{ id: "expr_jsx", code: "<div><span>Hello</span></div>" }], "state count = 0;");
|
|
22
|
+
const result = await analyzeAndEmit(ir);
|
|
23
|
+
const bundle = result.bundle;
|
|
24
|
+
// Should contain native indicators (like state. prefix or h() calls)
|
|
25
|
+
expect(bundle).toContain("state");
|
|
26
|
+
// Should contain __zenith.h calls in expressions
|
|
27
|
+
expect(result.expressions).toContain('__zenith.h("div"');
|
|
28
|
+
expect(result.expressions).toContain('__zenith.h("span"');
|
|
29
|
+
expect(result.expressions).toContain('"Hello"');
|
|
30
|
+
// Should NOT contain raw HTML tags
|
|
31
|
+
expect(result.expressions).not.toContain("<div>");
|
|
32
|
+
});
|
|
33
|
+
test("State identifier renaming - state. prefix", async () => {
|
|
34
|
+
const ir = createIR([], [{ id: "expr_state", code: "count + 1" }], "state count = 0;");
|
|
35
|
+
const result = await analyzeAndEmit(ir);
|
|
36
|
+
// Should be renamed to state.count
|
|
37
|
+
expect(result.expressions).toContain("state.count");
|
|
38
|
+
// It shouldn't be plain count + 1 (except maybe in some comment if we had one)
|
|
39
|
+
expect(result.expressions).not.toContain("return (count + 1)");
|
|
40
|
+
});
|
|
41
|
+
test("Local variables (loop vars) are NOT prefixed with state.", async () => {
|
|
42
|
+
const ir = createIR([
|
|
43
|
+
{
|
|
44
|
+
type: "loop-fragment",
|
|
45
|
+
itemVar: "item",
|
|
46
|
+
indexVar: "i",
|
|
47
|
+
source: "items",
|
|
48
|
+
body: [],
|
|
49
|
+
location: { line: 1, column: 1 }
|
|
50
|
+
}
|
|
51
|
+
], [{ id: "expr_loop", code: "item.name + i" }], "state items = [];");
|
|
52
|
+
const result = await analyzeAndEmit(ir);
|
|
53
|
+
// item and i should STAY AS IS
|
|
54
|
+
expect(result.expressions).toContain("item.name + i");
|
|
55
|
+
expect(result.expressions).not.toContain("state.item");
|
|
56
|
+
expect(result.expressions).not.toContain("state.i");
|
|
57
|
+
});
|
|
58
|
+
test("Nested JSX with state and local variables", async () => {
|
|
59
|
+
const ir = createIR([
|
|
60
|
+
{
|
|
61
|
+
type: "loop-fragment",
|
|
62
|
+
itemVar: "item",
|
|
63
|
+
indexVar: "i",
|
|
64
|
+
source: "items",
|
|
65
|
+
body: [],
|
|
66
|
+
location: { line: 1, column: 1 }
|
|
67
|
+
}
|
|
68
|
+
], [{ id: "expr_complex", code: "items.map(it => <div class={active ? 'active' : ''}>{it.name}</div>)" }], "state items = []; state active = true;");
|
|
69
|
+
const result = await analyzeAndEmit(ir);
|
|
70
|
+
// 1. JSX lowered
|
|
71
|
+
expect(result.expressions).toContain('__zenith.h("div"');
|
|
72
|
+
// 2. state.active
|
|
73
|
+
expect(result.expressions).toContain('state.active');
|
|
74
|
+
// 2. state.active
|
|
75
|
+
expect(result.expressions).toContain('class: state.active');
|
|
76
|
+
// 3. state.items
|
|
77
|
+
expect(result.expressions).toContain('state.items.map');
|
|
78
|
+
// 4. it.name stays as is (it's local to the map)
|
|
79
|
+
expect(result.expressions).toContain('[it.name]');
|
|
80
|
+
expect(result.expressions).not.toContain(': state.it');
|
|
81
|
+
});
|
|
82
|
+
test("JSX Attributes transformation", async () => {
|
|
83
|
+
const ir = createIR([], [{ id: "expr_attr", code: "<button disabled={loading} onclick={() => count++}>Click</button>" }], "state loading = false; state count = 0;");
|
|
84
|
+
const result = await analyzeAndEmit(ir);
|
|
85
|
+
expect(result.expressions).toContain('__zenith.h("button"');
|
|
86
|
+
// Attributes should be in an object
|
|
87
|
+
expect(result.expressions).toContain('disabled: state.loading');
|
|
88
|
+
expect(result.expressions).toContain('onclick: () => state.count++');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { test, expect } from "bun:test";
|
|
2
|
+
import { resolveComponentsInIR } from "../transform/componentResolver";
|
|
3
|
+
// Helper to create a basic IR
|
|
4
|
+
function createIR(nodes, expressions = []) {
|
|
5
|
+
return {
|
|
6
|
+
filePath: "test.zen",
|
|
7
|
+
template: {
|
|
8
|
+
raw: "",
|
|
9
|
+
nodes,
|
|
10
|
+
expressions
|
|
11
|
+
},
|
|
12
|
+
script: { raw: "", attributes: {} },
|
|
13
|
+
styles: [],
|
|
14
|
+
componentScripts: []
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
// Helper to create component metadata
|
|
18
|
+
function createComponent(name, nodes, expressions) {
|
|
19
|
+
return {
|
|
20
|
+
name,
|
|
21
|
+
path: `${name}.zen`,
|
|
22
|
+
template: "",
|
|
23
|
+
nodes,
|
|
24
|
+
expressions,
|
|
25
|
+
slots: [],
|
|
26
|
+
props: [],
|
|
27
|
+
styles: [],
|
|
28
|
+
script: null,
|
|
29
|
+
scriptAttributes: null,
|
|
30
|
+
hasScript: false,
|
|
31
|
+
hasStyles: false
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
test("Component instantiation generates unique instance expression IDs", () => {
|
|
35
|
+
// Define a component "Card" with one expression {title}
|
|
36
|
+
// usage: <div>{title}</div>
|
|
37
|
+
const cardExpr = { id: "expr_card_1", code: "title", location: { line: 1, column: 1 } };
|
|
38
|
+
const cardNodes = [
|
|
39
|
+
{
|
|
40
|
+
type: "element",
|
|
41
|
+
tag: "div",
|
|
42
|
+
attributes: [],
|
|
43
|
+
children: [
|
|
44
|
+
{
|
|
45
|
+
type: "expression",
|
|
46
|
+
expression: "expr_card_1",
|
|
47
|
+
location: { line: 1, column: 1 }
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
location: { line: 1, column: 1 }
|
|
51
|
+
}
|
|
52
|
+
];
|
|
53
|
+
const components = new Map();
|
|
54
|
+
components.set("Card", createComponent("Card", cardNodes, [cardExpr]));
|
|
55
|
+
// Create a page using <Card /> twice
|
|
56
|
+
// <root>
|
|
57
|
+
// <Card />
|
|
58
|
+
// <Card />
|
|
59
|
+
// </root>
|
|
60
|
+
const pageNodes = [
|
|
61
|
+
{
|
|
62
|
+
type: "element",
|
|
63
|
+
tag: "root",
|
|
64
|
+
attributes: [],
|
|
65
|
+
children: [
|
|
66
|
+
{
|
|
67
|
+
type: "component",
|
|
68
|
+
name: "Card",
|
|
69
|
+
attributes: [],
|
|
70
|
+
children: [],
|
|
71
|
+
location: { line: 1, column: 1 }
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
type: "component",
|
|
75
|
+
name: "Card",
|
|
76
|
+
attributes: [],
|
|
77
|
+
children: [],
|
|
78
|
+
location: { line: 2, column: 1 }
|
|
79
|
+
}
|
|
80
|
+
],
|
|
81
|
+
location: { line: 0, column: 0 }
|
|
82
|
+
}
|
|
83
|
+
];
|
|
84
|
+
const ir = createIR(pageNodes);
|
|
85
|
+
// Resolve components
|
|
86
|
+
const resolvedIR = resolveComponentsInIR(ir, components);
|
|
87
|
+
// Assertions
|
|
88
|
+
const root = resolvedIR.template.nodes[0];
|
|
89
|
+
expect(root.type).toBe("element");
|
|
90
|
+
expect(root.children.length).toBe(2);
|
|
91
|
+
const card1 = root.children[0];
|
|
92
|
+
const card2 = root.children[1];
|
|
93
|
+
// Check structure
|
|
94
|
+
expect(card1.type).toBe("element"); // Resolved to div
|
|
95
|
+
expect(card2.type).toBe("element");
|
|
96
|
+
// Check expression nodes inside
|
|
97
|
+
const exprNode1 = card1.children[0];
|
|
98
|
+
const exprNode2 = card2.children[0];
|
|
99
|
+
expect(exprNode1.type).toBe("expression");
|
|
100
|
+
expect(exprNode2.type).toBe("expression");
|
|
101
|
+
const id1 = exprNode1.expression;
|
|
102
|
+
const id2 = exprNode2.expression;
|
|
103
|
+
console.log(`Card 1 Expression ID: ${id1}`);
|
|
104
|
+
console.log(`Card 2 Expression ID: ${id2}`);
|
|
105
|
+
// Critically, IDs must be unique
|
|
106
|
+
expect(id1).not.toBe(id2);
|
|
107
|
+
expect(id1).not.toBe("expr_card_1"); // Should be rewritten
|
|
108
|
+
expect(id1).toContain("inst");
|
|
109
|
+
// Check registry
|
|
110
|
+
const registryIDs = resolvedIR.template.expressions.map(e => e.id);
|
|
111
|
+
expect(registryIDs).toContain(id1);
|
|
112
|
+
expect(registryIDs).toContain(id2);
|
|
113
|
+
// Ensure both are registered
|
|
114
|
+
expect(registryIDs.length).toBeGreaterThanOrEqual(2);
|
|
115
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { compileZenSource } from '../index';
|
|
2
|
+
import { InvariantError } from '../errors/compilerError';
|
|
3
|
+
/**
|
|
4
|
+
* Integration Test: Native Error Bridge
|
|
5
|
+
* Triggers an INV005 (TEMPLATE_TAG) error and verifies detailed info.
|
|
6
|
+
*/
|
|
7
|
+
async function testNativeErrorBridge() {
|
|
8
|
+
console.log('Testing Native Error Bridge (INV001-INV006)...');
|
|
9
|
+
const source = `
|
|
10
|
+
<script>
|
|
11
|
+
const name = "Zenith";
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<div>This is invalid because of the template tag</div>
|
|
16
|
+
</template>
|
|
17
|
+
`;
|
|
18
|
+
try {
|
|
19
|
+
await compileZenSource(source, 'test-invalid.zen');
|
|
20
|
+
throw new Error('Should have thrown an InvariantError for <template> tag');
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
if (e instanceof InvariantError) {
|
|
24
|
+
console.log('Caught InvariantError:', e.code);
|
|
25
|
+
if (e.code !== 'INV005') {
|
|
26
|
+
throw new Error(`Expected error code INV005, but got ${e.code}`);
|
|
27
|
+
}
|
|
28
|
+
if (e.context !== '<template>') {
|
|
29
|
+
throw new Error(`Expected context "<template>", but got "${e.context}"`);
|
|
30
|
+
}
|
|
31
|
+
if (!e.hints || e.hints.length === 0) {
|
|
32
|
+
throw new Error('Expected hints to be populated');
|
|
33
|
+
}
|
|
34
|
+
if (!e.hints.some(h => h.includes('Zenith component') || h.includes('Card.Header'))) {
|
|
35
|
+
throw new Error('Hints do not contain expected content');
|
|
36
|
+
}
|
|
37
|
+
console.log('✅ Native error bridge test passed');
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
console.error('Unexpected error type:', e);
|
|
41
|
+
throw e;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Run tests
|
|
46
|
+
if (import.meta.main) {
|
|
47
|
+
testNativeErrorBridge().catch(e => {
|
|
48
|
+
console.error('❌ Integration test failed:', e.message);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { InvariantError, CompilerError } from '../errors/compilerError';
|
|
2
|
+
/**
|
|
3
|
+
* Test cases for the enhanced Error System
|
|
4
|
+
*/
|
|
5
|
+
function testErrorStructure() {
|
|
6
|
+
console.log('Testing CompilerError structure...');
|
|
7
|
+
const error = new CompilerError('Test message', 'test.zen', 10, 5, 'COMPILER_ERROR', 'TEST_CONTEXT', ['Hint 1', 'Hint 2']);
|
|
8
|
+
console.assert(error.message.includes('Test message'), 'Message should be preserved');
|
|
9
|
+
console.assert(error.file === 'test.zen', 'File should be preserved');
|
|
10
|
+
console.assert(error.line === 10, 'Line should be preserved');
|
|
11
|
+
console.assert(error.column === 5, 'Column should be preserved');
|
|
12
|
+
console.assert(error.context === 'TEST_CONTEXT', 'Context should be preserved');
|
|
13
|
+
console.assert(error.hints.length === 2, 'Hints should be preserved');
|
|
14
|
+
console.assert(error.hints[0] === 'Hint 1', 'Hint 1 should match');
|
|
15
|
+
console.log('✅ CompilerError structure test passed');
|
|
16
|
+
}
|
|
17
|
+
function testInvariantErrorStructure() {
|
|
18
|
+
console.log('Testing InvariantError structure...');
|
|
19
|
+
const error = new InvariantError('INV001', 'Invariant failed', 'Always be true', 'test.zen', 20, 15, 'INVARIANT_CONTEXT', ['Fix hint']);
|
|
20
|
+
console.assert(error.code === 'INV001', 'Code should be preserved');
|
|
21
|
+
console.assert(error.guarantee === 'Always be true', 'Guarantee should be preserved');
|
|
22
|
+
console.assert(error.errorType === 'InvariantViolation', 'Default errorType should be InvariantViolation');
|
|
23
|
+
console.assert(error.context === 'INVARIANT_CONTEXT', 'Context should be preserved');
|
|
24
|
+
console.assert(error.hints[0] === 'Fix hint', 'Hints should be preserved');
|
|
25
|
+
console.log('✅ InvariantError structure test passed');
|
|
26
|
+
}
|
|
27
|
+
// Run tests
|
|
28
|
+
if (import.meta.main) {
|
|
29
|
+
try {
|
|
30
|
+
testErrorStructure();
|
|
31
|
+
testInvariantErrorStructure();
|
|
32
|
+
console.log('✅ All Error System unit tests passed!');
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
console.error('❌ Tests failed:', e);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { test, expect } from "bun:test";
|
|
2
|
+
import { resolveComponentsInIR } from "../transform/componentResolver";
|
|
3
|
+
// Helper to create a basic ZenIR
|
|
4
|
+
function createIR(templateContent, script = '') {
|
|
5
|
+
return {
|
|
6
|
+
filePath: 'test.zen',
|
|
7
|
+
template: {
|
|
8
|
+
raw: templateContent,
|
|
9
|
+
nodes: [],
|
|
10
|
+
expressions: []
|
|
11
|
+
},
|
|
12
|
+
script: {
|
|
13
|
+
raw: script,
|
|
14
|
+
attributes: {}
|
|
15
|
+
},
|
|
16
|
+
styles: [],
|
|
17
|
+
componentScripts: []
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
test("Selective symbol renaming in component expressions", async () => {
|
|
21
|
+
// 1. Mock component metadata
|
|
22
|
+
const counterMeta = {
|
|
23
|
+
name: "Counter",
|
|
24
|
+
hasScript: true,
|
|
25
|
+
script: "let count = 0; function inc() { count++; }",
|
|
26
|
+
expressions: [
|
|
27
|
+
{ id: "expr_0", code: "count" },
|
|
28
|
+
{ id: "expr_1", code: "Math.floor(count / 2)" }
|
|
29
|
+
],
|
|
30
|
+
nodes: [
|
|
31
|
+
{
|
|
32
|
+
type: 'element',
|
|
33
|
+
tag: 'button',
|
|
34
|
+
attributes: [{ name: 'onclick', value: { id: 'expr_1', code: 'inc()', location: { line: 1, column: 1 } }, location: { line: 1, column: 1 } }],
|
|
35
|
+
children: [{ type: 'expression', expression: 'expr_0', location: { line: 1, column: 1 } }],
|
|
36
|
+
location: { line: 1, column: 1 }
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
props: [],
|
|
40
|
+
styles: []
|
|
41
|
+
};
|
|
42
|
+
const components = new Map();
|
|
43
|
+
components.set("Counter", counterMeta);
|
|
44
|
+
// 2. Mock page IR with two counter instances
|
|
45
|
+
const pageIR = createIR("<Counter /><Counter />");
|
|
46
|
+
pageIR.template.nodes = [
|
|
47
|
+
{ type: 'component', name: 'Counter', attributes: [], children: [], location: { line: 1, column: 1 } },
|
|
48
|
+
{ type: 'component', name: 'Counter', attributes: [], children: [], location: { line: 1, column: 10 } }
|
|
49
|
+
];
|
|
50
|
+
// 3. Resolve components
|
|
51
|
+
const resolvedIR = resolveComponentsInIR(pageIR, components);
|
|
52
|
+
// 4. Verify expressions are renamed
|
|
53
|
+
// We expect 4 expressions (2 per instance)
|
|
54
|
+
expect(resolvedIR.template.expressions.length).toBe(4);
|
|
55
|
+
// Check first instance
|
|
56
|
+
const expr0_inst0 = resolvedIR.template.expressions.find(e => e.id === "expr_0_inst0");
|
|
57
|
+
expect(expr0_inst0?.code).toBe("count_inst0");
|
|
58
|
+
// Check second instance
|
|
59
|
+
const expr0_inst1 = resolvedIR.template.expressions.find(e => e.id === "expr_0_inst1");
|
|
60
|
+
expect(expr0_inst1?.code).toBe("count_inst1");
|
|
61
|
+
// 5. Verify script renaming
|
|
62
|
+
expect(resolvedIR.script?.raw).toContain("let count_inst0 = 0");
|
|
63
|
+
expect(resolvedIR.script?.raw).toContain("function inc_inst0()");
|
|
64
|
+
expect(resolvedIR.script?.raw).toContain("let count_inst1 = 0");
|
|
65
|
+
});
|
|
66
|
+
test("Parent symbols are NOT renamed in component expressions", async () => {
|
|
67
|
+
const compMeta = {
|
|
68
|
+
name: "Hello",
|
|
69
|
+
hasScript: true,
|
|
70
|
+
script: "let local = 'hi';",
|
|
71
|
+
expressions: [
|
|
72
|
+
{ id: "expr_0", code: "local + globalVar" }
|
|
73
|
+
],
|
|
74
|
+
nodes: [],
|
|
75
|
+
props: [],
|
|
76
|
+
styles: []
|
|
77
|
+
};
|
|
78
|
+
const components = new Map();
|
|
79
|
+
components.set("Hello", compMeta);
|
|
80
|
+
const pageIR = createIR("<Hello />");
|
|
81
|
+
pageIR.template.nodes = [{ type: 'component', name: 'Hello', attributes: [], children: [], location: { line: 1, column: 1 } }];
|
|
82
|
+
const resIR = resolveComponentsInIR(pageIR, components);
|
|
83
|
+
const expr = resIR.template.expressions[0];
|
|
84
|
+
// 'local' should be renamed, 'globalVar' should NOT
|
|
85
|
+
expect(expr?.code).toContain("_inst");
|
|
86
|
+
expect(expr?.code).toContain("globalVar");
|
|
87
|
+
expect(expr?.code).not.toContain("globalVar_inst");
|
|
88
|
+
});
|
|
89
|
+
test("Macro Prop Substitution", async () => {
|
|
90
|
+
const compMeta = {
|
|
91
|
+
name: "Welcome",
|
|
92
|
+
hasScript: true,
|
|
93
|
+
script: "console.log(title)",
|
|
94
|
+
expressions: [
|
|
95
|
+
{ id: "expr_0", code: "title" }
|
|
96
|
+
],
|
|
97
|
+
nodes: [],
|
|
98
|
+
props: ["title"],
|
|
99
|
+
styles: []
|
|
100
|
+
};
|
|
101
|
+
const components = new Map();
|
|
102
|
+
components.set("Welcome", compMeta);
|
|
103
|
+
const pageIR = createIR("<Welcome title={pageTitle} />");
|
|
104
|
+
pageIR.template.nodes = [{
|
|
105
|
+
type: 'component',
|
|
106
|
+
name: 'Welcome',
|
|
107
|
+
attributes: [{
|
|
108
|
+
name: 'title',
|
|
109
|
+
value: { id: 'expr_P', code: 'pageTitle', location: { line: 1, column: 1 } },
|
|
110
|
+
location: { line: 1, column: 1 }
|
|
111
|
+
}],
|
|
112
|
+
children: [],
|
|
113
|
+
location: { line: 1, column: 1 }
|
|
114
|
+
}];
|
|
115
|
+
const resolvedIR = resolveComponentsInIR(pageIR, components);
|
|
116
|
+
// Expression should be substituted with parent code
|
|
117
|
+
const expr = resolvedIR.template.expressions.find(e => e.id === "expr_0_inst0");
|
|
118
|
+
expect(expr?.code).toBe("(pageTitle)");
|
|
119
|
+
// Script should also be substituted
|
|
120
|
+
expect(resolvedIR.script?.raw).toContain("console.log((pageTitle))");
|
|
121
|
+
});
|
|
122
|
+
test("Event Handler Renaming in Template", async () => {
|
|
123
|
+
const compMeta = {
|
|
124
|
+
name: "Button",
|
|
125
|
+
hasScript: true,
|
|
126
|
+
script: "function handleClick() { console.log('clicked'); }",
|
|
127
|
+
expressions: [],
|
|
128
|
+
nodes: [
|
|
129
|
+
{
|
|
130
|
+
type: 'element',
|
|
131
|
+
tag: 'button',
|
|
132
|
+
attributes: [
|
|
133
|
+
{ name: 'onclick', value: 'handleClick', location: { line: 1, column: 1 } }
|
|
134
|
+
],
|
|
135
|
+
children: [],
|
|
136
|
+
location: { line: 1, column: 1 }
|
|
137
|
+
}
|
|
138
|
+
],
|
|
139
|
+
props: [],
|
|
140
|
+
styles: []
|
|
141
|
+
};
|
|
142
|
+
const components = new Map();
|
|
143
|
+
components.set("Button", compMeta);
|
|
144
|
+
const pageIR = createIR("<Button />");
|
|
145
|
+
pageIR.template.nodes = [{ type: 'component', name: 'Button', attributes: [], children: [], location: { line: 1, column: 1 } }];
|
|
146
|
+
const resolvedIR = resolveComponentsInIR(pageIR, components);
|
|
147
|
+
// The button's onclick should be renamed
|
|
148
|
+
const button = resolvedIR.template.nodes[0];
|
|
149
|
+
expect(button.attributes[0].value).toBe("handleClick_inst0");
|
|
150
|
+
});
|
|
151
|
+
test("Multi-instance isolation and props.prefix substitution", async () => {
|
|
152
|
+
const compMeta = {
|
|
153
|
+
name: "Box",
|
|
154
|
+
hasScript: true,
|
|
155
|
+
script: "const val = signal(props.initial);",
|
|
156
|
+
expressions: [{ id: "expr_0", code: "val.value + props.label" }],
|
|
157
|
+
nodes: [],
|
|
158
|
+
props: ["initial", "label"],
|
|
159
|
+
styles: []
|
|
160
|
+
};
|
|
161
|
+
const components = new Map();
|
|
162
|
+
components.set("Box", compMeta);
|
|
163
|
+
const pageIR = createIR("<Box initial={1} label='A' /><Box initial={10} label='B' />");
|
|
164
|
+
pageIR.template.nodes = [
|
|
165
|
+
{ type: 'component', name: 'Box', attributes: [{ name: 'initial', value: { id: 'expr_P1', code: '1' } }, { name: 'label', value: 'A' }], children: [], location: { line: 1, column: 1 } },
|
|
166
|
+
{ type: 'component', name: 'Box', attributes: [{ name: 'initial', value: { id: 'expr_P2', code: '10' } }, { name: 'label', value: 'B' }], children: [], location: { line: 1, column: 30 } }
|
|
167
|
+
];
|
|
168
|
+
const resolvedIR = resolveComponentsInIR(pageIR, components);
|
|
169
|
+
// Verify first instance
|
|
170
|
+
const script = resolvedIR.script?.raw || "";
|
|
171
|
+
expect(script).toContain("const val_inst0 = signal((1))");
|
|
172
|
+
const expr0 = resolvedIR.template.expressions.find(e => e.id === "expr_0_inst0");
|
|
173
|
+
expect(expr0?.code).toBe("val_inst0.value + \"A\"");
|
|
174
|
+
// Verify second instance
|
|
175
|
+
expect(script).toContain("const val_inst1 = signal((10))");
|
|
176
|
+
const expr1 = resolvedIR.template.expressions.find(e => e.id === "expr_0_inst1");
|
|
177
|
+
expect(expr1?.code).toBe("val_inst1.value + \"B\"");
|
|
178
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Test Cases for Expression Validation
|
|
4
|
+
*
|
|
5
|
+
* Phase 8/9/10: Tests that invalid expressions fail the build
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
const validateExpressions_1 = require("../validate/validateExpressions");
|
|
9
|
+
/**
|
|
10
|
+
* Test valid expressions
|
|
11
|
+
*/
|
|
12
|
+
function testValidExpressions() {
|
|
13
|
+
const validExpressions = [
|
|
14
|
+
{
|
|
15
|
+
id: 'expr_0',
|
|
16
|
+
code: 'user.name',
|
|
17
|
+
location: { line: 10, column: 5 }
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: 'expr_1',
|
|
21
|
+
code: 'count + 1',
|
|
22
|
+
location: { line: 11, column: 8 }
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 'expr_2',
|
|
26
|
+
code: 'isActive ? "on" : "off"',
|
|
27
|
+
location: { line: 12, column: 12 }
|
|
28
|
+
}
|
|
29
|
+
];
|
|
30
|
+
const result = (0, validateExpressions_1.validateExpressions)(validExpressions, 'test.zen');
|
|
31
|
+
console.assert(result.valid === true, 'Valid expressions should pass validation');
|
|
32
|
+
console.assert(result.errors.length === 0, 'Valid expressions should have no errors');
|
|
33
|
+
console.log('✅ Valid expressions test passed');
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Test invalid expressions
|
|
37
|
+
*/
|
|
38
|
+
function testInvalidExpressions() {
|
|
39
|
+
const invalidExpressions = [
|
|
40
|
+
{
|
|
41
|
+
id: 'expr_0',
|
|
42
|
+
code: 'user.name}', // Mismatched brace
|
|
43
|
+
location: { line: 10, column: 5 }
|
|
44
|
+
}
|
|
45
|
+
];
|
|
46
|
+
const result = (0, validateExpressions_1.validateExpressions)(invalidExpressions, 'test.zen');
|
|
47
|
+
console.assert(result.valid === false, 'Invalid expressions should fail validation');
|
|
48
|
+
console.assert(result.errors.length > 0, 'Invalid expressions should have errors');
|
|
49
|
+
console.log('✅ Invalid expressions test passed');
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Test unsafe code detection
|
|
53
|
+
*/
|
|
54
|
+
function testUnsafeCode() {
|
|
55
|
+
const unsafeExpressions = [
|
|
56
|
+
{
|
|
57
|
+
id: 'expr_0',
|
|
58
|
+
code: 'eval("alert(1)")',
|
|
59
|
+
location: { line: 10, column: 5 }
|
|
60
|
+
}
|
|
61
|
+
];
|
|
62
|
+
const result = (0, validateExpressions_1.validateExpressions)(unsafeExpressions, 'test.zen');
|
|
63
|
+
console.assert(result.valid === false, 'Unsafe code should fail validation');
|
|
64
|
+
console.assert(result.errors.length > 0, 'Unsafe code should have errors');
|
|
65
|
+
console.log('✅ Unsafe code detection test passed');
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Test validateExpressionsOrThrow
|
|
69
|
+
*/
|
|
70
|
+
function testThrowOnInvalid() {
|
|
71
|
+
const invalidExpressions = [
|
|
72
|
+
{
|
|
73
|
+
id: 'expr_0',
|
|
74
|
+
code: 'user.name}', // Mismatched brace
|
|
75
|
+
location: { line: 10, column: 5 }
|
|
76
|
+
}
|
|
77
|
+
];
|
|
78
|
+
try {
|
|
79
|
+
(0, validateExpressions_1.validateExpressionsOrThrow)(invalidExpressions, 'test.zen');
|
|
80
|
+
console.assert(false, 'Should have thrown on invalid expressions');
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
console.assert(error instanceof Error, 'Should throw Error');
|
|
84
|
+
console.log('✅ Throw on invalid expressions test passed');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Run tests
|
|
88
|
+
if (require.main === module) {
|
|
89
|
+
console.log('Running validation tests...');
|
|
90
|
+
testValidExpressions();
|
|
91
|
+
testInvalidExpressions();
|
|
92
|
+
testUnsafeCode();
|
|
93
|
+
testThrowOnInvalid();
|
|
94
|
+
console.log('✅ All validation tests passed!');
|
|
95
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expression Classification
|
|
3
|
+
*
|
|
4
|
+
* Analyzes expression code to determine output type for structural lowering.
|
|
5
|
+
*
|
|
6
|
+
* JSX expressions are allowed if — and only if — the compiler can statically
|
|
7
|
+
* enumerate all possible DOM shapes and lower them at compile time.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Expression output types
|
|
11
|
+
*
|
|
12
|
+
* - primitive: string, number, boolean → text binding
|
|
13
|
+
* - conditional: cond ? <A /> : <B /> → ConditionalFragmentNode
|
|
14
|
+
* - optional: cond && <A /> → OptionalFragmentNode
|
|
15
|
+
* - loop: arr.map(i => <JSX />) → LoopFragmentNode
|
|
16
|
+
* - fragment: <A /> or <><A /><B /></> → inline fragment
|
|
17
|
+
* - unknown: cannot be statically determined → COMPILE ERROR
|
|
18
|
+
*/
|
|
19
|
+
export type ExpressionOutputType = 'primitive' | 'conditional' | 'optional' | 'loop' | 'fragment' | 'unknown';
|
|
20
|
+
/**
|
|
21
|
+
* Classification result with extracted metadata
|
|
22
|
+
*/
|
|
23
|
+
export interface ExpressionClassification {
|
|
24
|
+
type: ExpressionOutputType;
|
|
25
|
+
condition?: string;
|
|
26
|
+
consequent?: string;
|
|
27
|
+
alternate?: string;
|
|
28
|
+
optionalCondition?: string;
|
|
29
|
+
optionalFragment?: string;
|
|
30
|
+
loopSource?: string;
|
|
31
|
+
loopItemVar?: string;
|
|
32
|
+
loopIndexVar?: string;
|
|
33
|
+
loopBody?: string;
|
|
34
|
+
fragmentCode?: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Classify expression output type
|
|
38
|
+
*
|
|
39
|
+
* @param code - The expression code to classify
|
|
40
|
+
* @returns Classification result with metadata
|
|
41
|
+
*/
|
|
42
|
+
export declare function classifyExpression(code: string): ExpressionClassification;
|
|
43
|
+
/**
|
|
44
|
+
* Check if an expression type requires structural lowering
|
|
45
|
+
*/
|
|
46
|
+
export declare function requiresStructuralLowering(type: ExpressionOutputType): boolean;
|