@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,608 @@
|
|
|
1
|
+
import type { ZenFile, StateBinding, ScriptBlock } from "./types"
|
|
2
|
+
import * as parse5 from "parse5"
|
|
3
|
+
import { extractStateDeclarations, extractStateDeclarationsWithLocation, transformStateDeclarations, type StateDeclarationInfo } from "./parse"
|
|
4
|
+
import { extractEventHandlers, detectStateMutations, validateStateMutations, type InlineHandlerInfo } from "./mutation"
|
|
5
|
+
import {
|
|
6
|
+
extractExpressionBlocks,
|
|
7
|
+
transformExpressionBlocks,
|
|
8
|
+
generateExpressionRuntime,
|
|
9
|
+
generateExpressionRuntimeHelpers,
|
|
10
|
+
extractAttributeExpressions,
|
|
11
|
+
generateAttributeExpressionRuntime,
|
|
12
|
+
type ExpressionBlock,
|
|
13
|
+
type AttributeExpressionBinding
|
|
14
|
+
} from "./expression"
|
|
15
|
+
|
|
16
|
+
// ============================================
|
|
17
|
+
// ES6 Import Transform & Auto-Import System
|
|
18
|
+
// ============================================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Zen primitives that can be auto-imported
|
|
22
|
+
* Maps the function name to its internal implementation
|
|
23
|
+
*/
|
|
24
|
+
const ZEN_AUTO_IMPORTS = {
|
|
25
|
+
// Reactivity primitives
|
|
26
|
+
zenSignal: '__zenith.signal',
|
|
27
|
+
zenState: '__zenith.state',
|
|
28
|
+
zenEffect: '__zenith.effect',
|
|
29
|
+
zenMemo: '__zenith.memo',
|
|
30
|
+
zenRef: '__zenith.ref',
|
|
31
|
+
zenBatch: '__zenith.batch',
|
|
32
|
+
zenUntrack: '__zenith.untrack',
|
|
33
|
+
// Clean aliases
|
|
34
|
+
signal: '__zenith.signal',
|
|
35
|
+
state: '__zenith.state',
|
|
36
|
+
effect: '__zenith.effect',
|
|
37
|
+
memo: '__zenith.memo',
|
|
38
|
+
ref: '__zenith.ref',
|
|
39
|
+
batch: '__zenith.batch',
|
|
40
|
+
untrack: '__zenith.untrack',
|
|
41
|
+
// Lifecycle hooks
|
|
42
|
+
zenOnMount: '__zenith.onMount',
|
|
43
|
+
zenOnUnmount: '__zenith.onUnmount',
|
|
44
|
+
onMount: '__zenith.onMount',
|
|
45
|
+
onUnmount: '__zenith.onUnmount',
|
|
46
|
+
// Navigation (from router)
|
|
47
|
+
navigate: '__zenith_router.navigate',
|
|
48
|
+
getRoute: '__zenith_router.getRoute',
|
|
49
|
+
isActive: '__zenith_router.isActive',
|
|
50
|
+
prefetch: '__zenith_router.prefetch',
|
|
51
|
+
} as const;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Parse and transform ES6 imports in script content
|
|
55
|
+
* Converts imports to window/global references
|
|
56
|
+
*/
|
|
57
|
+
function transformImports(script: string): {
|
|
58
|
+
transformed: string;
|
|
59
|
+
imports: Array<{ from: string; names: string[] }>;
|
|
60
|
+
} {
|
|
61
|
+
const imports: Array<{ from: string; names: string[] }> = [];
|
|
62
|
+
|
|
63
|
+
// Match ES6 import statements
|
|
64
|
+
// import { a, b, c } from 'module'
|
|
65
|
+
// import { a as b } from 'module'
|
|
66
|
+
// import defaultExport from 'module'
|
|
67
|
+
// import * as name from 'module'
|
|
68
|
+
const importRegex = /import\s+(?:(\{[^}]+\})|(\*\s+as\s+\w+)|(\w+))\s+from\s+['"]([^'"]+)['"]\s*;?/g;
|
|
69
|
+
|
|
70
|
+
let transformed = script;
|
|
71
|
+
let match;
|
|
72
|
+
|
|
73
|
+
while ((match = importRegex.exec(script)) !== null) {
|
|
74
|
+
const namedImports = match[1]; // { a, b, c }
|
|
75
|
+
const namespaceImport = match[2]; // * as name
|
|
76
|
+
const defaultImport = match[3]; // defaultExport
|
|
77
|
+
const modulePath = match[4]; // 'module'
|
|
78
|
+
|
|
79
|
+
const importedNames: string[] = [];
|
|
80
|
+
|
|
81
|
+
if (namedImports) {
|
|
82
|
+
// Parse { a, b as c, d }
|
|
83
|
+
const names = namedImports
|
|
84
|
+
.replace(/[{}]/g, '')
|
|
85
|
+
.split(',')
|
|
86
|
+
.map(n => n.trim())
|
|
87
|
+
.filter(n => n);
|
|
88
|
+
|
|
89
|
+
for (const name of names) {
|
|
90
|
+
// Handle 'a as b' syntax
|
|
91
|
+
const asMatch = name.match(/(\w+)\s+as\s+(\w+)/);
|
|
92
|
+
if (asMatch) {
|
|
93
|
+
importedNames.push(asMatch[2]); // Use the alias
|
|
94
|
+
} else {
|
|
95
|
+
importedNames.push(name);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (defaultImport) {
|
|
101
|
+
importedNames.push(defaultImport);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (namespaceImport) {
|
|
105
|
+
const nsMatch = namespaceImport.match(/\*\s+as\s+(\w+)/);
|
|
106
|
+
if (nsMatch) {
|
|
107
|
+
importedNames.push(nsMatch[1]);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
imports.push({ from: modulePath, names: importedNames });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Remove import statements from the script
|
|
115
|
+
transformed = transformed.replace(importRegex, '');
|
|
116
|
+
|
|
117
|
+
// Clean up any leftover empty lines from removed imports
|
|
118
|
+
transformed = transformed.replace(/^\s*\n{2,}/gm, '\n');
|
|
119
|
+
|
|
120
|
+
return { transformed, imports };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Detect zen primitives used in script and generate auto-import declarations
|
|
125
|
+
*/
|
|
126
|
+
function generateAutoImports(script: string, existingImports: string[]): string {
|
|
127
|
+
const autoImportDeclarations: string[] = [];
|
|
128
|
+
|
|
129
|
+
for (const [name, globalRef] of Object.entries(ZEN_AUTO_IMPORTS)) {
|
|
130
|
+
// Skip if already imported
|
|
131
|
+
if (existingImports.includes(name)) continue;
|
|
132
|
+
|
|
133
|
+
// Check if the primitive is used in the script (as a word boundary)
|
|
134
|
+
const usageRegex = new RegExp(`\\b${name}\\s*\\(`, 'g');
|
|
135
|
+
if (usageRegex.test(script)) {
|
|
136
|
+
autoImportDeclarations.push(`const ${name} = window.${globalRef};`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (autoImportDeclarations.length > 0) {
|
|
141
|
+
return `// Auto-imported zen primitives\n${autoImportDeclarations.join('\n')}\n\n`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return '';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Process script content: transform imports and add auto-imports
|
|
149
|
+
*/
|
|
150
|
+
export function processScriptImports(script: string): string {
|
|
151
|
+
// First, transform ES6 imports
|
|
152
|
+
const { transformed, imports } = transformImports(script);
|
|
153
|
+
|
|
154
|
+
// Collect all imported names
|
|
155
|
+
const importedNames = imports.flatMap(i => i.names);
|
|
156
|
+
|
|
157
|
+
// Generate auto-imports for zen primitives
|
|
158
|
+
const autoImports = generateAutoImports(transformed, importedNames);
|
|
159
|
+
|
|
160
|
+
// Combine: auto-imports first, then the transformed script
|
|
161
|
+
return autoImports + transformed;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Transform on* attributes to data-zen-* attributes during compilation
|
|
165
|
+
// Also converts inline arrow functions to function names
|
|
166
|
+
// Returns: { transformedHtml, eventTypes, inlineHandlerMap }
|
|
167
|
+
// where inlineHandlerMap maps original arrow function code to generated function names
|
|
168
|
+
function transformEventAttributes(
|
|
169
|
+
html: string,
|
|
170
|
+
inlineHandlerMap: Map<string, string> // maps arrow function code -> generated function name
|
|
171
|
+
): { transformedHtml: string; eventTypes: Set<string> } {
|
|
172
|
+
const document = parse5.parse(html);
|
|
173
|
+
const eventTypes = new Set<string>();
|
|
174
|
+
|
|
175
|
+
function walk(node: any) {
|
|
176
|
+
// Transform attributes on element nodes
|
|
177
|
+
if (node.attrs && Array.isArray(node.attrs)) {
|
|
178
|
+
node.attrs = node.attrs.map((attr: any) => {
|
|
179
|
+
const attrName = attr.name.toLowerCase();
|
|
180
|
+
// Check if attribute starts with "on" (event handler)
|
|
181
|
+
if (attrName.startsWith('on') && attrName.length > 2) {
|
|
182
|
+
// Convert onclick -> data-zen-click, onchange -> data-zen-change, etc.
|
|
183
|
+
const eventType = attrName.slice(2); // Remove "on" prefix
|
|
184
|
+
eventTypes.add(eventType); // Track which event types are used
|
|
185
|
+
|
|
186
|
+
const handlerValue = attr.value;
|
|
187
|
+
// Check if it's an inline arrow function
|
|
188
|
+
const arrowFunctionMatch = handlerValue.match(/^\s*\([^)]*\)\s*=>\s*(.+)$/);
|
|
189
|
+
|
|
190
|
+
if (arrowFunctionMatch) {
|
|
191
|
+
// It's an inline arrow function - look up the generated function name
|
|
192
|
+
const arrowBody = arrowFunctionMatch[1].trim();
|
|
193
|
+
const generatedName = inlineHandlerMap.get(arrowBody);
|
|
194
|
+
if (generatedName) {
|
|
195
|
+
return {
|
|
196
|
+
name: `data-zen-${eventType}`,
|
|
197
|
+
value: generatedName
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
name: `data-zen-${eventType}`,
|
|
204
|
+
value: handlerValue
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
return attr;
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Recursively process child nodes
|
|
212
|
+
if (node.childNodes) {
|
|
213
|
+
node.childNodes.forEach(walk);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
walk(document);
|
|
218
|
+
|
|
219
|
+
// Serialize back to HTML string
|
|
220
|
+
return {
|
|
221
|
+
transformedHtml: parse5.serialize(document),
|
|
222
|
+
eventTypes
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Transform text bindings { stateName } in text nodes to <span data-zen-bind="stateName"></span>
|
|
228
|
+
* Returns transformed HTML and a map of state bindings
|
|
229
|
+
*/
|
|
230
|
+
function transformTextBindings(
|
|
231
|
+
html: string,
|
|
232
|
+
declaredStates: Set<string>
|
|
233
|
+
): { transformedHtml: string; stateBindings: Map<string, StateBinding> } {
|
|
234
|
+
const document = parse5.parse(html);
|
|
235
|
+
const stateBindings = new Map<string, StateBinding>();
|
|
236
|
+
let bindingIndex = 0;
|
|
237
|
+
|
|
238
|
+
function walk(node: any) {
|
|
239
|
+
// Skip script and style nodes - their content is handled separately
|
|
240
|
+
if (node.nodeName === 'script' || node.nodeName === 'style') {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (!node.childNodes || !Array.isArray(node.childNodes)) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const newChildNodes: any[] = [];
|
|
249
|
+
|
|
250
|
+
for (const child of node.childNodes) {
|
|
251
|
+
if (child.nodeName === '#text' && child.value) {
|
|
252
|
+
const text = child.value;
|
|
253
|
+
// Match { identifier } pattern - only simple identifiers, no expressions
|
|
254
|
+
const bindingRegex = /\{\s*(\w+)\s*\}/g;
|
|
255
|
+
const matches = Array.from(text.matchAll(bindingRegex));
|
|
256
|
+
|
|
257
|
+
if (matches.length === 0) {
|
|
258
|
+
// No bindings, keep the text node as-is
|
|
259
|
+
newChildNodes.push(child);
|
|
260
|
+
} else {
|
|
261
|
+
// Validate all bindings reference declared states
|
|
262
|
+
// Skip validation for instance-scoped state names (__zen_comp_*_stateName)
|
|
263
|
+
// These are component states that have already been transformed during inlining
|
|
264
|
+
for (const match of matches) {
|
|
265
|
+
const m = match as RegExpMatchArray;
|
|
266
|
+
const stateName = m[1];
|
|
267
|
+
if (!stateName) continue;
|
|
268
|
+
|
|
269
|
+
// Skip validation for instance-scoped component state names
|
|
270
|
+
// These are generated during component inlining (e.g., __zen_comp_0_clicks)
|
|
271
|
+
if (stateName.startsWith('__zen_')) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (!declaredStates.has(stateName)) {
|
|
276
|
+
throw new Error(
|
|
277
|
+
`Compiler Error: Binding "{ ${stateName} }" references undeclared state. ` +
|
|
278
|
+
`Declared states: ${Array.from(declaredStates).join(', ') || '(none)'}`
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Split text by bindings and create nodes
|
|
284
|
+
let lastIndex = 0;
|
|
285
|
+
|
|
286
|
+
for (const match of matches) {
|
|
287
|
+
const m = match as RegExpMatchArray;
|
|
288
|
+
const stateName = m[1];
|
|
289
|
+
if (!stateName) continue; // Skip if state name is missing (shouldn't happen)
|
|
290
|
+
|
|
291
|
+
const matchStart = m.index!;
|
|
292
|
+
const matchEnd = matchStart + m[0].length;
|
|
293
|
+
|
|
294
|
+
// Add text before the binding
|
|
295
|
+
if (matchStart > lastIndex) {
|
|
296
|
+
const beforeText = text.substring(lastIndex, matchStart);
|
|
297
|
+
if (beforeText) {
|
|
298
|
+
newChildNodes.push({
|
|
299
|
+
nodeName: '#text',
|
|
300
|
+
value: beforeText,
|
|
301
|
+
parentNode: node
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Create span element for binding
|
|
307
|
+
const bindId = `bind-${bindingIndex++}`;
|
|
308
|
+
const spanNode = {
|
|
309
|
+
nodeName: 'span',
|
|
310
|
+
tagName: 'span',
|
|
311
|
+
attrs: [
|
|
312
|
+
{
|
|
313
|
+
name: 'data-zen-bind',
|
|
314
|
+
value: stateName
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
name: 'data-zen-bind-id',
|
|
318
|
+
value: bindId
|
|
319
|
+
}
|
|
320
|
+
],
|
|
321
|
+
childNodes: [],
|
|
322
|
+
parentNode: node
|
|
323
|
+
};
|
|
324
|
+
newChildNodes.push(spanNode);
|
|
325
|
+
|
|
326
|
+
// Track this binding
|
|
327
|
+
if (!stateBindings.has(stateName)) {
|
|
328
|
+
stateBindings.set(stateName, {
|
|
329
|
+
stateName,
|
|
330
|
+
bindings: []
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
const binding = stateBindings.get(stateName)!;
|
|
334
|
+
binding.bindings.push({
|
|
335
|
+
stateName,
|
|
336
|
+
nodeIndex: bindingIndex - 1
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
lastIndex = matchEnd;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Add remaining text after last binding
|
|
343
|
+
if (lastIndex < text.length) {
|
|
344
|
+
const afterText = text.substring(lastIndex);
|
|
345
|
+
if (afterText) {
|
|
346
|
+
newChildNodes.push({
|
|
347
|
+
nodeName: '#text',
|
|
348
|
+
value: afterText,
|
|
349
|
+
parentNode: node
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
} else {
|
|
355
|
+
// Not a text node, recurse into it
|
|
356
|
+
walk(child);
|
|
357
|
+
newChildNodes.push(child);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
node.childNodes = newChildNodes;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
walk(document);
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
transformedHtml: parse5.serialize(document),
|
|
368
|
+
stateBindings
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Transform :class and :value attributes to data-zen-* attributes
|
|
373
|
+
// Returns: { transformedHtml, bindings } where bindings contains attribute binding info
|
|
374
|
+
function transformAttributeBindings(html: string, scripts: ScriptBlock[]): { transformedHtml: string; bindings: Array<{ type: 'class' | 'value'; expression: string }> } {
|
|
375
|
+
const document = parse5.parse(html);
|
|
376
|
+
const bindings: Array<{ type: 'class' | 'value'; expression: string }> = [];
|
|
377
|
+
|
|
378
|
+
function walk(node: any) {
|
|
379
|
+
// Transform attributes on element nodes
|
|
380
|
+
if (node.attrs && Array.isArray(node.attrs)) {
|
|
381
|
+
node.attrs = node.attrs.map((attr: any) => {
|
|
382
|
+
const attrName = attr.name;
|
|
383
|
+
// Check if attribute is :class or :value (colon-prefixed)
|
|
384
|
+
if (attrName === ':class' || attrName === ':value') {
|
|
385
|
+
const bindingType = attrName.slice(1) as 'class' | 'value'; // Remove ":" prefix
|
|
386
|
+
const expression = attr.value.trim(); // Store the quoted expression
|
|
387
|
+
|
|
388
|
+
// Track this binding
|
|
389
|
+
bindings.push({ type: bindingType, expression });
|
|
390
|
+
|
|
391
|
+
// Transform to data-zen-* attribute
|
|
392
|
+
return {
|
|
393
|
+
name: `data-zen-${bindingType}`,
|
|
394
|
+
value: expression // Store the expression string
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
return attr;
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Recursively process child nodes
|
|
402
|
+
if (node.childNodes) {
|
|
403
|
+
node.childNodes.forEach(walk);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
walk(document);
|
|
408
|
+
|
|
409
|
+
// NOTE: Component state transformation is now handled in component-process.ts
|
|
410
|
+
// via transformStateInHtml, which only transforms component INTERNAL state.
|
|
411
|
+
// Props are direct bindings and should NOT be transformed here.
|
|
412
|
+
// This function is kept for backward compatibility but does nothing now.
|
|
413
|
+
|
|
414
|
+
// Serialize back to HTML string
|
|
415
|
+
return {
|
|
416
|
+
transformedHtml: parse5.serialize(document),
|
|
417
|
+
bindings
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Strip script and style tags from HTML since they're extracted to separate files
|
|
422
|
+
function stripScriptAndStyleTags(html: string): string {
|
|
423
|
+
// Remove script tags (including content)
|
|
424
|
+
html = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '');
|
|
425
|
+
// Remove style tags (including content)
|
|
426
|
+
html = html.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '');
|
|
427
|
+
return html;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Compiler error for state redeclaration
|
|
432
|
+
*/
|
|
433
|
+
export class StateRedeclarationError extends Error {
|
|
434
|
+
constructor(
|
|
435
|
+
public stateName: string,
|
|
436
|
+
public firstDeclaration: StateDeclarationInfo,
|
|
437
|
+
public secondDeclaration: StateDeclarationInfo
|
|
438
|
+
) {
|
|
439
|
+
const firstLoc = `script ${firstDeclaration.scriptIndex + 1}, line ${firstDeclaration.line}, column ${firstDeclaration.column}`;
|
|
440
|
+
const secondLoc = `script ${secondDeclaration.scriptIndex + 1}, line ${secondDeclaration.line}, column ${secondDeclaration.column}`;
|
|
441
|
+
super(
|
|
442
|
+
`Compiler Error: State variable "${stateName}" is declared multiple times.\n` +
|
|
443
|
+
` First declaration: ${firstLoc}\n` +
|
|
444
|
+
` Second declaration: ${secondLoc}\n` +
|
|
445
|
+
` State variables must be declared exactly once.`
|
|
446
|
+
);
|
|
447
|
+
this.name = 'StateRedeclarationError';
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// this function splits the props into what we are compiling the them down too
|
|
452
|
+
// html styles and scripts
|
|
453
|
+
export function splitZen(file: ZenFile) {
|
|
454
|
+
// Extract state declarations from all scripts with location information
|
|
455
|
+
const allDeclarations: StateDeclarationInfo[] = [];
|
|
456
|
+
for (let i = 0; i < file.scripts.length; i++) {
|
|
457
|
+
const script = file.scripts[i];
|
|
458
|
+
if (script) {
|
|
459
|
+
const declarations = extractStateDeclarationsWithLocation(script.content, i);
|
|
460
|
+
allDeclarations.push(...declarations);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Check for redeclarations and throw compile-time error
|
|
465
|
+
const declaredStates = new Map<string, StateDeclarationInfo>();
|
|
466
|
+
for (const declaration of allDeclarations) {
|
|
467
|
+
const existing = declaredStates.get(declaration.name);
|
|
468
|
+
if (existing) {
|
|
469
|
+
throw new StateRedeclarationError(declaration.name, existing, declaration);
|
|
470
|
+
}
|
|
471
|
+
declaredStates.set(declaration.name, declaration);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Convert to Map<string, string> for backward compatibility
|
|
475
|
+
const stateMap = new Map<string, string>();
|
|
476
|
+
declaredStates.forEach((info, name) => {
|
|
477
|
+
stateMap.set(name, info.value);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// Extract event handlers from HTML (including inline arrow functions)
|
|
481
|
+
const { eventHandlers, inlineHandlers } = extractEventHandlers(file.html);
|
|
482
|
+
|
|
483
|
+
// Also add function props to event handlers (functions passed as props can mutate state)
|
|
484
|
+
if (file.functionProps) {
|
|
485
|
+
file.functionProps.forEach(funcName => eventHandlers.add(funcName));
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Generate function code for inline handlers and create a map
|
|
489
|
+
// Map: arrow function body -> generated function name
|
|
490
|
+
const inlineHandlerMap = new Map<string, string>(); // arrow body -> function name
|
|
491
|
+
const inlineHandlerFunctions: string[] = []; // generated function code to inject
|
|
492
|
+
|
|
493
|
+
inlineHandlers.forEach((handlerInfo, generatedName) => {
|
|
494
|
+
inlineHandlerMap.set(handlerInfo.body, generatedName);
|
|
495
|
+
|
|
496
|
+
// Replace parameter references in the body (e.g., "e" -> "event")
|
|
497
|
+
let body = handlerInfo.body;
|
|
498
|
+
if (handlerInfo.paramName && handlerInfo.paramName !== 'event' && handlerInfo.paramName !== '') {
|
|
499
|
+
// Replace the parameter name with "event" in the body
|
|
500
|
+
// Use word boundaries to avoid partial matches
|
|
501
|
+
const paramRegex = new RegExp(`\\b${handlerInfo.paramName}\\b`, 'g');
|
|
502
|
+
body = body.replace(paramRegex, 'event');
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Generate function code: function name(event, el) { body }
|
|
506
|
+
const functionCode = `function ${generatedName}(event, el) { ${body} }`;
|
|
507
|
+
inlineHandlerFunctions.push(functionCode);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
// Inject inline handler functions into scripts (before state declaration removal)
|
|
511
|
+
const scriptsWithInlineHandlers = file.scripts.map((script, index) => {
|
|
512
|
+
if (inlineHandlerFunctions.length > 0 && index === 0) {
|
|
513
|
+
// Inject inline handlers at the start of the first script
|
|
514
|
+
return {
|
|
515
|
+
...script,
|
|
516
|
+
content: inlineHandlerFunctions.join('\n\n') + '\n\n' + script.content
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
return script;
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
// Detect and validate state mutations
|
|
523
|
+
for (let i = 0; i < scriptsWithInlineHandlers.length; i++) {
|
|
524
|
+
const script = scriptsWithInlineHandlers[i];
|
|
525
|
+
if (!script) continue;
|
|
526
|
+
const mutations = detectStateMutations(script.content, new Set(stateMap.keys()));
|
|
527
|
+
validateStateMutations(mutations, eventHandlers, i);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Transform event attributes (converting inline arrow functions to function names)
|
|
531
|
+
const { transformedHtml: htmlAfterEvents, eventTypes } = transformEventAttributes(file.html, inlineHandlerMap);
|
|
532
|
+
|
|
533
|
+
// Then transform attribute bindings (:class, :value)
|
|
534
|
+
const { transformedHtml: htmlAfterAttributeBindings, bindings } = transformAttributeBindings(htmlAfterEvents, file.scripts);
|
|
535
|
+
|
|
536
|
+
// Extract and transform attribute expressions (attr={expression})
|
|
537
|
+
const { transformedHtml: htmlAfterAttrExpressions, bindings: attrExprBindings } = extractAttributeExpressions(
|
|
538
|
+
htmlAfterAttributeBindings,
|
|
539
|
+
new Set(stateMap.keys())
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
// Extract and transform dynamic expression blocks ({ condition && <el> }, { a ? b : c }, { arr.map(...) })
|
|
543
|
+
const expressionBlocks = extractExpressionBlocks(htmlAfterAttrExpressions, new Set(stateMap.keys()));
|
|
544
|
+
const htmlAfterExpressions = transformExpressionBlocks(htmlAfterAttrExpressions, expressionBlocks);
|
|
545
|
+
|
|
546
|
+
// Then transform text bindings (this will validate against declared states)
|
|
547
|
+
const { transformedHtml: htmlAfterBindings, stateBindings } = transformTextBindings(
|
|
548
|
+
htmlAfterExpressions,
|
|
549
|
+
new Set(stateMap.keys())
|
|
550
|
+
);
|
|
551
|
+
|
|
552
|
+
// Finally strip script/style tags
|
|
553
|
+
const finalHtml = stripScriptAndStyleTags(htmlAfterBindings);
|
|
554
|
+
|
|
555
|
+
// Transform scripts to:
|
|
556
|
+
// 1. Process ES6 imports (transform to window references)
|
|
557
|
+
// 2. Auto-import zen primitives
|
|
558
|
+
// 3. Remove state declarations (runtime will handle them)
|
|
559
|
+
// 4. Register functions on window for component access
|
|
560
|
+
const transformedScripts = scriptsWithInlineHandlers.map((s, index) => {
|
|
561
|
+
if (!s) return '';
|
|
562
|
+
|
|
563
|
+
// Step 1 & 2: Process imports and auto-imports
|
|
564
|
+
let transformed = processScriptImports(s.content);
|
|
565
|
+
|
|
566
|
+
// Step 3: Transform state declarations
|
|
567
|
+
transformed = transformStateDeclarations(transformed);
|
|
568
|
+
|
|
569
|
+
// Step 4: Extract function declarations and register them on window
|
|
570
|
+
// This allows components to access parent functions via props
|
|
571
|
+
// SKIP if script contains an IIFE with window registration (component scripts are pre-wrapped)
|
|
572
|
+
// Component scripts have pattern: window.__zen_comp_*_funcName = funcName
|
|
573
|
+
const hasComponentRegistration = /window\.__zen_comp_\d+_\w+\s*=/.test(transformed);
|
|
574
|
+
|
|
575
|
+
if (!hasComponentRegistration) {
|
|
576
|
+
const functionRegex = /function\s+(\w+)\s*\([^)]*\)\s*\{/g;
|
|
577
|
+
const functionNames: string[] = [];
|
|
578
|
+
let match;
|
|
579
|
+
while ((match = functionRegex.exec(transformed)) !== null) {
|
|
580
|
+
if (match[1]) {
|
|
581
|
+
functionNames.push(match[1]);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Register functions on window (only if not already registered)
|
|
586
|
+
if (functionNames.length > 0) {
|
|
587
|
+
const registrationCode = functionNames.map(name =>
|
|
588
|
+
` if (typeof window.${name} === 'undefined') { window.${name} = ${name}; }`
|
|
589
|
+
).join('\n');
|
|
590
|
+
transformed += `\n\n// Register functions on window for component access\n${registrationCode}`;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return transformed;
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
return {
|
|
598
|
+
html: finalHtml,
|
|
599
|
+
scripts: transformedScripts,
|
|
600
|
+
styles: file.styles.map(style => style.content),
|
|
601
|
+
eventTypes: Array.from(eventTypes).sort(), // Return sorted array of event types
|
|
602
|
+
stateBindings: Array.from(stateBindings.values()), // Return array of state bindings
|
|
603
|
+
stateDeclarations: stateMap, // Return map of state declarations (name -> value)
|
|
604
|
+
bindings, // Return attribute bindings for :class and :value
|
|
605
|
+
expressionBlocks, // Return expression blocks for dynamic rendering
|
|
606
|
+
attrExprBindings // Return attribute expression bindings
|
|
607
|
+
}
|
|
608
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface ZenFile {
|
|
2
|
+
html: string;
|
|
3
|
+
scripts: ScriptBlock[];
|
|
4
|
+
styles: StyleBlock[];
|
|
5
|
+
functionProps?: Set<string>; // Function names passed as props to components
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ScriptBlock {
|
|
9
|
+
content: string;
|
|
10
|
+
index: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface StyleBlock {
|
|
14
|
+
content: string;
|
|
15
|
+
index: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface Binding {
|
|
19
|
+
stateName: string;
|
|
20
|
+
nodeIndex: number; // For tracking multiple bindings to the same state
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface StateBinding {
|
|
24
|
+
stateName: string;
|
|
25
|
+
bindings: Binding[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface StateDeclaration {
|
|
29
|
+
name: string;
|
|
30
|
+
initialValue: string; // The expression after the = sign
|
|
31
|
+
}
|
|
32
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compiled Template Output Types
|
|
3
|
+
*
|
|
4
|
+
* Phase 2: Transform IR → Static HTML + Runtime Bindings
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type CompiledTemplate = {
|
|
8
|
+
html: string
|
|
9
|
+
bindings: Binding[]
|
|
10
|
+
scripts: string | null
|
|
11
|
+
styles: string[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type Binding = {
|
|
15
|
+
id: string
|
|
16
|
+
type: 'text' | 'attribute'
|
|
17
|
+
target: string // e.g., "data-zen-text" or "class" for attribute bindings
|
|
18
|
+
expression: string // The original expression code
|
|
19
|
+
location?: {
|
|
20
|
+
line: number
|
|
21
|
+
column: number
|
|
22
|
+
}
|
|
23
|
+
loopContext?: LoopContext // Phase 7: Loop context for expressions inside map iterations
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Loop context for expressions inside map iterations
|
|
28
|
+
* Phase 7: Tracks loop variables for runtime setter generation
|
|
29
|
+
*/
|
|
30
|
+
export type LoopContext = {
|
|
31
|
+
variables: string[] // e.g., ['todo', 'index']
|
|
32
|
+
mapSource?: string // The array being mapped, e.g., 'todoItems'
|
|
33
|
+
}
|
|
34
|
+
|