@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.
Files changed (101) hide show
  1. package/.eslintignore +15 -0
  2. package/.gitattributes +2 -0
  3. package/.github/ISSUE_TEMPLATE/compiler-errors-for-invalid-state-declarations.md +25 -0
  4. package/.github/ISSUE_TEMPLATE/new_ticket.yaml +34 -0
  5. package/.github/pull_request_template.md +15 -0
  6. package/.github/workflows/discord-changelog.yml +141 -0
  7. package/.github/workflows/discord-notify.yml +242 -0
  8. package/.github/workflows/discord-version.yml +195 -0
  9. package/.prettierignore +13 -0
  10. package/.prettierrc +21 -0
  11. package/.zen.d.ts +15 -0
  12. package/LICENSE +21 -0
  13. package/README.md +55 -0
  14. package/app/components/Button.zen +46 -0
  15. package/app/components/Link.zen +11 -0
  16. package/app/favicon.ico +0 -0
  17. package/app/layouts/Main.zen +59 -0
  18. package/app/pages/about.zen +23 -0
  19. package/app/pages/blog/[id].zen +53 -0
  20. package/app/pages/blog/index.zen +32 -0
  21. package/app/pages/dynamic-dx.zen +712 -0
  22. package/app/pages/dynamic-primitives.zen +453 -0
  23. package/app/pages/index.zen +154 -0
  24. package/app/pages/navigation-demo.zen +229 -0
  25. package/app/pages/posts/[...slug].zen +61 -0
  26. package/app/pages/primitives-demo.zen +273 -0
  27. package/assets/logos/0E3B5DDD-605C-4839-BB2E-DFCA8ADC9604.PNG +0 -0
  28. package/assets/logos/760971E5-79A1-44F9-90B9-925DF30F4278.PNG +0 -0
  29. package/assets/logos/8A06ED80-9ED2-4689-BCBD-13B2E95EE8E4.JPG +0 -0
  30. package/assets/logos/C691FF58-ED13-4E8D-B6A3-02E835849340.PNG +0 -0
  31. package/assets/logos/C691FF58-ED13-4E8D-B6A3-02E835849340.svg +601 -0
  32. package/assets/logos/README.md +54 -0
  33. package/assets/logos/zen.icns +0 -0
  34. package/bun.lock +39 -0
  35. package/compiler/README.md +380 -0
  36. package/compiler/errors/compilerError.ts +24 -0
  37. package/compiler/finalize/finalizeOutput.ts +163 -0
  38. package/compiler/finalize/generateFinalBundle.ts +82 -0
  39. package/compiler/index.ts +44 -0
  40. package/compiler/ir/types.ts +83 -0
  41. package/compiler/legacy/binding.ts +254 -0
  42. package/compiler/legacy/bindings.ts +338 -0
  43. package/compiler/legacy/component-process.ts +1208 -0
  44. package/compiler/legacy/component.ts +301 -0
  45. package/compiler/legacy/event.ts +50 -0
  46. package/compiler/legacy/expression.ts +1149 -0
  47. package/compiler/legacy/mutation.ts +280 -0
  48. package/compiler/legacy/parse.ts +299 -0
  49. package/compiler/legacy/split.ts +608 -0
  50. package/compiler/legacy/types.ts +32 -0
  51. package/compiler/output/types.ts +34 -0
  52. package/compiler/parse/detectMapExpressions.ts +102 -0
  53. package/compiler/parse/parseScript.ts +22 -0
  54. package/compiler/parse/parseTemplate.ts +425 -0
  55. package/compiler/parse/parseZenFile.ts +66 -0
  56. package/compiler/parse/trackLoopContext.ts +82 -0
  57. package/compiler/runtime/dataExposure.ts +291 -0
  58. package/compiler/runtime/generateDOM.ts +144 -0
  59. package/compiler/runtime/generateHydrationBundle.ts +383 -0
  60. package/compiler/runtime/hydration.ts +309 -0
  61. package/compiler/runtime/navigation.ts +432 -0
  62. package/compiler/runtime/thinRuntime.ts +160 -0
  63. package/compiler/runtime/transformIR.ts +256 -0
  64. package/compiler/runtime/wrapExpression.ts +84 -0
  65. package/compiler/runtime/wrapExpressionWithLoop.ts +77 -0
  66. package/compiler/spa-build.ts +1000 -0
  67. package/compiler/test/validate-test.ts +104 -0
  68. package/compiler/transform/generateBindings.ts +47 -0
  69. package/compiler/transform/generateHTML.ts +28 -0
  70. package/compiler/transform/transformNode.ts +126 -0
  71. package/compiler/transform/transformTemplate.ts +38 -0
  72. package/compiler/validate/validateExpressions.ts +168 -0
  73. package/core/index.ts +135 -0
  74. package/core/lifecycle/index.ts +49 -0
  75. package/core/lifecycle/zen-mount.ts +182 -0
  76. package/core/lifecycle/zen-unmount.ts +88 -0
  77. package/core/reactivity/index.ts +54 -0
  78. package/core/reactivity/tracking.ts +167 -0
  79. package/core/reactivity/zen-batch.ts +57 -0
  80. package/core/reactivity/zen-effect.ts +139 -0
  81. package/core/reactivity/zen-memo.ts +146 -0
  82. package/core/reactivity/zen-ref.ts +52 -0
  83. package/core/reactivity/zen-signal.ts +121 -0
  84. package/core/reactivity/zen-state.ts +180 -0
  85. package/core/reactivity/zen-untrack.ts +44 -0
  86. package/docs/COMMENTS.md +111 -0
  87. package/docs/COMMITS.md +36 -0
  88. package/docs/CONTRIBUTING.md +116 -0
  89. package/docs/STYLEGUIDE.md +62 -0
  90. package/package.json +44 -0
  91. package/router/index.ts +76 -0
  92. package/router/manifest.ts +314 -0
  93. package/router/navigation/ZenLink.zen +231 -0
  94. package/router/navigation/index.ts +78 -0
  95. package/router/navigation/zen-link.ts +584 -0
  96. package/router/runtime.ts +458 -0
  97. package/router/types.ts +168 -0
  98. package/runtime/build.ts +17 -0
  99. package/runtime/serve.ts +93 -0
  100. package/scripts/webhook-proxy.ts +213 -0
  101. package/tsconfig.json +28 -0
@@ -0,0 +1,301 @@
1
+ // compiler/component.ts
2
+ // Phase 3: Component discovery, parsing, and metadata extraction
3
+
4
+ import fs from "fs";
5
+ import path from "path";
6
+ import { parseZen } from "./parse";
7
+ import { extractStateDeclarations } from "./parse";
8
+ import * as parse5 from "parse5";
9
+
10
+ export interface ComponentMetadata {
11
+ name: string; // PascalCase component name (e.g., "Button", "UIButton")
12
+ filePath: string;
13
+ isLayout: boolean; // true for layouts, false for components
14
+ props: Map<string, string>; // prop name -> default value expression (empty string = no default)
15
+ stateDeclarations: Map<string, string>; // state name -> initial value
16
+ hasSlots: boolean; // true if component uses <Slot /> or <Slot name="..."/>
17
+ slotNames: Set<string>; // set of slot names used (includes "default" for default slot)
18
+ html: string; // component HTML (with slots)
19
+ scripts: string[]; // component scripts
20
+ styles: string[]; // component styles
21
+ }
22
+
23
+ /**
24
+ * Convert filename to PascalCase component name
25
+ * Examples:
26
+ * components/Button.zen -> Button
27
+ * components/ui/Button.zen -> UIButton
28
+ * layouts/Main.zen -> Main
29
+ * MyComponent.zen -> MyComponent
30
+ */
31
+ function filenameToComponentName(filePath: string, baseDir: string): string {
32
+ // Get relative path from base directory
33
+ const relativePath = path.relative(baseDir, filePath);
34
+ // Remove .zen extension
35
+ const withoutExt = relativePath.replace(/\.zen$/, "");
36
+ // Split by path separator
37
+ const parts = withoutExt.split(path.sep);
38
+
39
+ // Strip "components" or "layouts" prefix (first part if it's one of these)
40
+ const filteredParts = parts.filter((part, index) => {
41
+ // Keep all parts except the first if it's "components" or "layouts"
42
+ return index > 0 || (part !== "components" && part !== "layouts");
43
+ });
44
+
45
+ // Convert each part to PascalCase
46
+ const pascalParts = filteredParts.map(part => {
47
+ // Handle kebab-case, snake_case, or camelCase
48
+ return part
49
+ .split(/[-_]/)
50
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
51
+ .join("");
52
+ });
53
+
54
+ return pascalParts.join("");
55
+ }
56
+
57
+ /**
58
+ * Extract props from script content
59
+ * Looks for:
60
+ * 1. TypeScript-style: type Props = { propName?: type; ... }
61
+ * 2. props.propName = value;
62
+ * 3. props = { propName: value, ... }
63
+ * Returns Map of prop name -> default value expression (empty string = no default, "?" = optional)
64
+ */
65
+ function extractProps(scriptContent: string): Map<string, string> {
66
+ const props = new Map<string, string>();
67
+
68
+ // Pattern 1: TypeScript-style type Props = { propName?: type; ... }
69
+ // Match: type Props = { ... } (multiline, handles nested braces and function types)
70
+ // This regex handles: type Props = { prop?: type; } with nested braces in function types
71
+ const typePropsRegex = /type\s+Props\s*=\s*\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/s;
72
+ const typeMatch = typePropsRegex.exec(scriptContent);
73
+ if (typeMatch && typeMatch[1]) {
74
+ const propsBody = typeMatch[1];
75
+ // Parse prop definitions: propName?: type; or propName: type;
76
+ // Match prop name, optional ?, type (can include function types like (x: number) => void, generics, etc.)
77
+ // Use a more robust regex that handles function types with nested parentheses
78
+ const propDefRegex = /(\w+)(\?)?\s*:\s*((?:\([^)]*\)\s*=>\s*[^;]+|[^;]+?))(?:\s*;|\s*$)/g;
79
+ let propMatch: RegExpExecArray | null;
80
+ while ((propMatch = propDefRegex.exec(propsBody)) !== null) {
81
+ const propName = propMatch[1];
82
+ if (!propName) continue; // Skip if prop name is missing
83
+ const isOptional = propMatch[2] === '?';
84
+ // For type definitions, we don't store the type, just mark as optional if needed
85
+ // Optional props get "?" as default value indicator
86
+ // This allows the component to know which props are optional
87
+ props.set(propName, isOptional ? "?" : "");
88
+ }
89
+ }
90
+
91
+ // Pattern 2: props.propName = value;
92
+ const dotPropRegex = /props\.(\w+)\s*=\s*([^;]+?)(?:\s*;|\s*$)/gm;
93
+ let match;
94
+ while ((match = dotPropRegex.exec(scriptContent)) !== null) {
95
+ const propName = match[1];
96
+ const defaultValue = match[2]?.trim() || "";
97
+ if (propName) {
98
+ // Only override if not already set from type Props (preserve type info)
99
+ if (!props.has(propName)) {
100
+ props.set(propName, defaultValue);
101
+ }
102
+ }
103
+ }
104
+
105
+ // Pattern 3: props = { propName: value, ... }
106
+ const objPropRegex = /props\s*=\s*\{([^}]+)\}/s;
107
+ const objMatch = objPropRegex.exec(scriptContent);
108
+ if (objMatch) {
109
+ const propsObj = objMatch[1];
110
+ // Parse key: value pairs
111
+ const propPairs = propsObj?.match(/(\w+)\s*:\s*([^,}]+)/g);
112
+ if (propPairs) {
113
+ for (const pair of propPairs) {
114
+ const propMatch = pair.match(/(\w+)\s*:\s*(.+)/);
115
+ if (propMatch) {
116
+ const propName = propMatch[1];
117
+ const defaultValue = propMatch[2]?.trim() || "";
118
+ // Only override if not already set from type Props
119
+ if (propName && typeof propName === 'string' && !props.has(propName)) {
120
+ props.set(propName.trim(), defaultValue.trim());
121
+ }
122
+ }
123
+ }
124
+ }
125
+ }
126
+
127
+ return props;
128
+ }
129
+
130
+ /**
131
+ * Extract slot usage from HTML
132
+ * Returns: { hasSlots: boolean, slotNames: Set<string> }
133
+ */
134
+ function extractSlots(html: string): { hasSlots: boolean; slotNames: Set<string> } {
135
+ const slotNames = new Set<string>();
136
+ const document = parse5.parse(html);
137
+
138
+ function walk(node: any) {
139
+ if (node.tagName === "Slot" || node.tagName === "slot") {
140
+ slotNames.add("default"); // Default slot
141
+
142
+ // Check for name attribute
143
+ const nameAttr = node.attrs?.find((attr: any) => attr.name === "name");
144
+ if (nameAttr?.value) {
145
+ slotNames.add(nameAttr.value);
146
+ }
147
+ }
148
+
149
+ if (node.childNodes) {
150
+ node.childNodes.forEach(walk);
151
+ }
152
+ }
153
+
154
+ walk(document);
155
+
156
+ return {
157
+ hasSlots: slotNames.size > 0,
158
+ slotNames
159
+ };
160
+ }
161
+
162
+ /**
163
+ * Discover and parse all component files in a directory
164
+ */
165
+ export function discoverComponents(
166
+ componentsDir: string,
167
+ baseDir: string
168
+ ): Map<string, ComponentMetadata> {
169
+ const components = new Map<string, ComponentMetadata>();
170
+
171
+ if (!fs.existsSync(componentsDir)) {
172
+ return components; // Return empty map if directory doesn't exist
173
+ }
174
+
175
+ function scanDirectory(dir: string): void {
176
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
177
+
178
+ for (const entry of entries) {
179
+ const fullPath = path.join(dir, entry.name);
180
+
181
+ if (entry.isDirectory()) {
182
+ scanDirectory(fullPath); // Recurse into subdirectories
183
+ } else if (entry.isFile() && entry.name.endsWith(".zen")) {
184
+ try {
185
+ const zenFile = parseZen(fullPath);
186
+ const componentName = filenameToComponentName(fullPath, baseDir);
187
+
188
+ // Extract metadata from all scripts
189
+ const allProps = new Map<string, string>();
190
+ const allStateDeclarations = new Map<string, string>();
191
+
192
+ for (const script of zenFile.scripts) {
193
+ const props = extractProps(script.content);
194
+ const stateDecls = extractStateDeclarations(script.content);
195
+
196
+ // Merge props (later scripts override earlier ones)
197
+ for (const [name, value] of props.entries()) {
198
+ allProps.set(name, value);
199
+ }
200
+
201
+ // Merge state declarations (later scripts override earlier ones)
202
+ for (const [name, value] of stateDecls.entries()) {
203
+ allStateDeclarations.set(name, value);
204
+ }
205
+ }
206
+
207
+ // Extract slots
208
+ const { hasSlots, slotNames } = extractSlots(zenFile.html);
209
+
210
+ const metadata: ComponentMetadata = {
211
+ name: componentName,
212
+ filePath: fullPath,
213
+ isLayout: false,
214
+ props: allProps,
215
+ stateDeclarations: allStateDeclarations,
216
+ hasSlots,
217
+ slotNames,
218
+ html: zenFile.html,
219
+ scripts: zenFile.scripts.map(s => s.content),
220
+ styles: zenFile.styles.map(s => s.content)
221
+ };
222
+
223
+ components.set(componentName, metadata);
224
+ } catch (error) {
225
+ console.warn(`Warning: Failed to parse component ${fullPath}:`, error);
226
+ }
227
+ }
228
+ }
229
+ }
230
+
231
+ scanDirectory(componentsDir);
232
+ return components;
233
+ }
234
+
235
+ /**
236
+ * Discover and parse all layout files in a directory
237
+ */
238
+ export function discoverLayouts(
239
+ layoutsDir: string,
240
+ baseDir: string
241
+ ): Map<string, ComponentMetadata> {
242
+ const layouts = new Map<string, ComponentMetadata>();
243
+
244
+ if (!fs.existsSync(layoutsDir)) {
245
+ return layouts; // Return empty map if directory doesn't exist
246
+ }
247
+
248
+ const entries = fs.readdirSync(layoutsDir, { withFileTypes: true });
249
+
250
+ for (const entry of entries) {
251
+ if (entry.isFile() && entry.name.endsWith(".zen")) {
252
+ const fullPath = path.join(layoutsDir, entry.name);
253
+ try {
254
+ const zenFile = parseZen(fullPath);
255
+ const layoutName = filenameToComponentName(fullPath, baseDir);
256
+
257
+ // Extract metadata from all scripts
258
+ const allProps = new Map<string, string>();
259
+ const allStateDeclarations = new Map<string, string>();
260
+
261
+ for (const script of zenFile.scripts) {
262
+ const props = extractProps(script.content);
263
+ const stateDecls = extractStateDeclarations(script.content);
264
+
265
+ // Merge props (later scripts override earlier ones)
266
+ for (const [name, value] of props.entries()) {
267
+ allProps.set(name, value);
268
+ }
269
+
270
+ // Merge state declarations (later scripts override earlier ones)
271
+ for (const [name, value] of stateDecls.entries()) {
272
+ allStateDeclarations.set(name, value);
273
+ }
274
+ }
275
+
276
+ // Extract slots
277
+ const { hasSlots, slotNames } = extractSlots(zenFile.html);
278
+
279
+ const metadata: ComponentMetadata = {
280
+ name: layoutName,
281
+ filePath: fullPath,
282
+ isLayout: true,
283
+ props: allProps,
284
+ stateDeclarations: allStateDeclarations,
285
+ hasSlots,
286
+ slotNames,
287
+ html: zenFile.html,
288
+ scripts: zenFile.scripts.map(s => s.content),
289
+ styles: zenFile.styles.map(s => s.content)
290
+ };
291
+
292
+ layouts.set(layoutName, metadata);
293
+ } catch (error) {
294
+ console.warn(`Warning: Failed to parse layout ${fullPath}:`, error);
295
+ }
296
+ }
297
+ }
298
+
299
+ return layouts;
300
+ }
301
+
@@ -0,0 +1,50 @@
1
+
2
+ // compiler/event.ts
3
+ // Phase 2: fully dynamic, compiler-driven DOM event system.
4
+ // Uses a single generic delegate function for all event types and adds DOM helper methods.
5
+
6
+ export function generateEventBindingRuntime(eventTypes: string[]): string {
7
+ // Phase 2: Generate DOM helper methods and a single generic delegate function
8
+ // The delegate function dynamically handles any event type by checking data-zen-* attributes
9
+
10
+ const eventTypesStr = JSON.stringify(eventTypes);
11
+
12
+ return `
13
+ // Phase 2 runtime helpers - DOM manipulation methods on HTMLElement prototype
14
+ HTMLElement.prototype.show = function() { this.style.display = "block"; }
15
+ HTMLElement.prototype.hide = function() { this.style.display = "none"; }
16
+ HTMLElement.prototype.addClass = function(name) { this.classList.add(name); }
17
+ HTMLElement.prototype.removeClass = function(name) { this.classList.remove(name); }
18
+ HTMLElement.prototype.setText = function(text) { this.textContent = text; }
19
+ HTMLElement.prototype.setHTML = function(html) { this.innerHTML = html; }
20
+
21
+ // Phase 2: Dynamic delegated event listener - single generic function for all event types
22
+ function delegate(event) {
23
+ let type = event.type;
24
+ let el = event.target;
25
+ // Ensure el is an Element (not text node, etc.) before checking attributes
26
+ while (el && el.nodeType === 1 && !el.hasAttribute(\`data-zen-\${type}\`)) {
27
+ el = el.parentElement;
28
+ }
29
+ // Also handle case where initial target wasn't an element
30
+ if (el && el.nodeType !== 1) {
31
+ el = el.parentElement;
32
+ while (el && el.nodeType === 1 && !el.hasAttribute(\`data-zen-\${type}\`)) {
33
+ el = el.parentElement;
34
+ }
35
+ }
36
+ if (el && el.nodeType === 1) {
37
+ const handlerName = el.getAttribute(\`data-zen-\${type}\`);
38
+ if (handlerName && typeof window[handlerName] === "function") {
39
+ window[handlerName](event, el);
40
+ }
41
+ }
42
+ }
43
+
44
+ // Attach delegate function to all detected event types dynamically
45
+ const zenEvents = ${eventTypesStr};
46
+ zenEvents.forEach(type => {
47
+ document.addEventListener(type, delegate);
48
+ });
49
+ `;
50
+ }