@zenithbuild/core 0.1.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +24 -40
  3. package/bin/zen-build.ts +2 -0
  4. package/bin/zen-dev.ts +2 -0
  5. package/bin/zen-preview.ts +2 -0
  6. package/bin/zenith.ts +2 -0
  7. package/cli/commands/add.ts +37 -0
  8. package/cli/commands/build.ts +37 -0
  9. package/cli/commands/create.ts +702 -0
  10. package/cli/commands/dev.ts +197 -0
  11. package/cli/commands/index.ts +112 -0
  12. package/cli/commands/preview.ts +62 -0
  13. package/cli/commands/remove.ts +33 -0
  14. package/cli/index.ts +10 -0
  15. package/cli/main.ts +101 -0
  16. package/cli/utils/branding.ts +153 -0
  17. package/cli/utils/logger.ts +40 -0
  18. package/cli/utils/plugin-manager.ts +114 -0
  19. package/cli/utils/project.ts +71 -0
  20. package/compiler/build-analyzer.ts +122 -0
  21. package/compiler/discovery/layouts.ts +61 -0
  22. package/compiler/index.ts +40 -24
  23. package/compiler/ir/types.ts +1 -0
  24. package/compiler/parse/parseScript.ts +29 -5
  25. package/compiler/parse/parseTemplate.ts +96 -58
  26. package/compiler/parse/scriptAnalysis.ts +77 -0
  27. package/compiler/runtime/dataExposure.ts +49 -31
  28. package/compiler/runtime/generateDOM.ts +18 -17
  29. package/compiler/runtime/generateHydrationBundle.ts +24 -5
  30. package/compiler/runtime/transformIR.ts +140 -49
  31. package/compiler/runtime/wrapExpressionWithLoop.ts +11 -11
  32. package/compiler/spa-build.ts +70 -153
  33. package/compiler/ssg-build.ts +412 -0
  34. package/compiler/transform/layoutProcessor.ts +132 -0
  35. package/compiler/transform/transformNode.ts +19 -19
  36. package/dist/cli.js +11648 -0
  37. package/dist/zen-build.js +11659 -0
  38. package/dist/zen-dev.js +11659 -0
  39. package/dist/zen-preview.js +11659 -0
  40. package/dist/zenith.js +11659 -0
  41. package/package.json +22 -2
  42. package/runtime/bundle-generator.ts +416 -0
  43. package/runtime/client-runtime.ts +532 -0
  44. package/.eslintignore +0 -15
  45. package/.gitattributes +0 -2
  46. package/.github/ISSUE_TEMPLATE/compiler-errors-for-invalid-state-declarations.md +0 -25
  47. package/.github/ISSUE_TEMPLATE/new_ticket.yaml +0 -34
  48. package/.github/pull_request_template.md +0 -15
  49. package/.github/workflows/discord-changelog.yml +0 -141
  50. package/.github/workflows/discord-notify.yml +0 -242
  51. package/.github/workflows/discord-version.yml +0 -195
  52. package/.prettierignore +0 -13
  53. package/.prettierrc +0 -21
  54. package/.zen.d.ts +0 -15
  55. package/app/components/Button.zen +0 -46
  56. package/app/components/Link.zen +0 -11
  57. package/app/favicon.ico +0 -0
  58. package/app/layouts/Main.zen +0 -59
  59. package/app/pages/about.zen +0 -23
  60. package/app/pages/blog/[id].zen +0 -53
  61. package/app/pages/blog/index.zen +0 -32
  62. package/app/pages/dynamic-dx.zen +0 -712
  63. package/app/pages/dynamic-primitives.zen +0 -453
  64. package/app/pages/index.zen +0 -154
  65. package/app/pages/navigation-demo.zen +0 -229
  66. package/app/pages/posts/[...slug].zen +0 -61
  67. package/app/pages/primitives-demo.zen +0 -273
  68. package/assets/logos/0E3B5DDD-605C-4839-BB2E-DFCA8ADC9604.PNG +0 -0
  69. package/assets/logos/760971E5-79A1-44F9-90B9-925DF30F4278.PNG +0 -0
  70. package/assets/logos/8A06ED80-9ED2-4689-BCBD-13B2E95EE8E4.JPG +0 -0
  71. package/assets/logos/C691FF58-ED13-4E8D-B6A3-02E835849340.PNG +0 -0
  72. package/assets/logos/C691FF58-ED13-4E8D-B6A3-02E835849340.svg +0 -601
  73. package/assets/logos/README.md +0 -54
  74. package/assets/logos/zen.icns +0 -0
  75. package/bun.lock +0 -39
  76. package/compiler/legacy/binding.ts +0 -254
  77. package/compiler/legacy/bindings.ts +0 -338
  78. package/compiler/legacy/component-process.ts +0 -1208
  79. package/compiler/legacy/component.ts +0 -301
  80. package/compiler/legacy/event.ts +0 -50
  81. package/compiler/legacy/expression.ts +0 -1149
  82. package/compiler/legacy/mutation.ts +0 -280
  83. package/compiler/legacy/parse.ts +0 -299
  84. package/compiler/legacy/split.ts +0 -608
  85. package/compiler/legacy/types.ts +0 -32
  86. package/docs/COMMENTS.md +0 -111
  87. package/docs/COMMITS.md +0 -36
  88. package/docs/CONTRIBUTING.md +0 -116
  89. package/docs/STYLEGUIDE.md +0 -62
  90. package/scripts/webhook-proxy.ts +0 -213
@@ -17,7 +17,7 @@ export function generateDOMCode(
17
17
  varCounter: { count: number } = { count: 0 }
18
18
  ): { code: string; varName: string } {
19
19
  const varName = `node_${varCounter.count++}`
20
-
20
+
21
21
  switch (node.type) {
22
22
  case 'text': {
23
23
  const textNode = node as TextNode
@@ -27,14 +27,14 @@ export function generateDOMCode(
27
27
  varName
28
28
  }
29
29
  }
30
-
30
+
31
31
  case 'expression': {
32
32
  const exprNode = node as ExpressionNode
33
33
  const expr = expressions.find(e => e.id === exprNode.expression)
34
34
  if (!expr) {
35
35
  throw new Error(`Expression ${exprNode.expression} not found`)
36
36
  }
37
-
37
+
38
38
  // Create a span element to hold the expression result
39
39
  return {
40
40
  code: `${indent}const ${varName} = document.createElement('span');
@@ -43,13 +43,13 @@ ${indent}${varName}.setAttribute('data-zen-expr', '${exprNode.expression}');`,
43
43
  varName
44
44
  }
45
45
  }
46
-
46
+
47
47
  case 'element': {
48
48
  const elNode = node as ElementNode
49
49
  const tag = elNode.tag
50
-
50
+
51
51
  let code = `${indent}const ${varName} = document.createElement('${tag}');\n`
52
-
52
+
53
53
  // Handle attributes
54
54
  for (const attr of elNode.attributes) {
55
55
  if (typeof attr.value === 'string') {
@@ -60,7 +60,7 @@ ${indent}${varName}.setAttribute('data-zen-expr', '${exprNode.expression}');`,
60
60
  // Expression attribute
61
61
  const expr = attr.value as ExpressionIR
62
62
  const attrName = attr.name === 'className' ? 'class' : attr.name
63
-
63
+
64
64
  // Handle special attributes
65
65
  if (attrName === 'class' || attrName === 'className') {
66
66
  code += `${indent}${varName}.className = String(${expr.id}(state) ?? '');\n`
@@ -70,9 +70,10 @@ ${indent}if (typeof styleValue_${varCounter.count} === 'string') {
70
70
  ${indent} ${varName}.style.cssText = styleValue_${varCounter.count};
71
71
  ${indent}}\n`
72
72
  } else if (attrName.startsWith('on')) {
73
- // Event handler - store handler name, will be bound later
73
+ // Event handler - store handler name/id, will be bound later
74
74
  const eventType = attrName.slice(2).toLowerCase() // Remove 'on' prefix
75
- code += `${indent}${varName}.setAttribute('data-zen-${eventType}', ${JSON.stringify(attr.value as any)});\n`
75
+ const value = typeof attr.value === 'string' ? attr.value : (attr.value as ExpressionIR).id
76
+ code += `${indent}${varName}.setAttribute('data-zen-${eventType}', ${JSON.stringify(value)});\n`
76
77
  } else {
77
78
  const tempVar = `attr_${varCounter.count++}`
78
79
  code += `${indent}const ${tempVar} = ${expr.id}(state);
@@ -82,7 +83,7 @@ ${indent}}\n`
82
83
  }
83
84
  }
84
85
  }
85
-
86
+
86
87
  // Handle children
87
88
  if (elNode.children.length > 0) {
88
89
  for (const child of elNode.children) {
@@ -91,7 +92,7 @@ ${indent}}\n`
91
92
  code += `${indent}${varName}.appendChild(${childResult.varName});\n`
92
93
  }
93
94
  }
94
-
95
+
95
96
  return { code, varName }
96
97
  }
97
98
  }
@@ -112,11 +113,11 @@ export function generateDOMFunction(
112
113
  return fragment;
113
114
  }`
114
115
  }
115
-
116
+
116
117
  const varCounter = { count: 0 }
117
118
  let code = `function ${functionName}(state) {
118
119
  `
119
-
120
+
120
121
  if (nodes.length === 1) {
121
122
  const node = nodes[0]
122
123
  if (!node) {
@@ -127,18 +128,18 @@ export function generateDOMFunction(
127
128
  code += `\n return ${result.varName};\n}`
128
129
  return code
129
130
  }
130
-
131
+
131
132
  // Multiple nodes - create a fragment
132
133
  code += ` const fragment = document.createDocumentFragment();\n`
133
-
134
+
134
135
  for (const node of nodes) {
135
136
  const result = generateDOMCode(node, expressions, ' ', varCounter)
136
137
  code += `${result.code}\n`
137
138
  code += ` fragment.appendChild(${result.varName});\n`
138
139
  }
139
-
140
+
140
141
  code += ` return fragment;
141
142
  }`
142
-
143
+
143
144
  return code
144
145
  }
@@ -255,10 +255,29 @@ export function generateHydrationRuntime(): string {
255
255
  // Create new handler
256
256
  const handler = function(event) {
257
257
  try {
258
- // Get handler function from window (functions are registered on window)
259
- const handlerFunc = window[handlerName];
258
+ // 1. Try to find handler function on window (for named functions)
259
+ let handlerFunc = window[handlerName];
260
+
261
+ // 2. If not found, try the expression registry (for inline expressions)
262
+ if (typeof handlerFunc !== 'function' && window.__ZENITH_EXPRESSIONS__) {
263
+ handlerFunc = window.__ZENITH_EXPRESSIONS__.get(handlerName);
264
+ }
265
+
260
266
  if (typeof handlerFunc === 'function') {
261
- handlerFunc(event, element);
267
+ // Call the handler. For expressions, we pass the current state.
268
+ // Note: Phase 6 handles passing loaderData, props, etc. if needed.
269
+ const state = window.__ZENITH_STATE__ || {};
270
+ const loaderData = window.__ZENITH_LOADER_DATA__ || {};
271
+ const props = window.__ZENITH_PROPS__ || {};
272
+ const stores = window.__ZENITH_STORES__ || {};
273
+
274
+ if (handlerFunc.length === 1) {
275
+ // Legacy or simple handler
276
+ handlerFunc(event, element);
277
+ } else {
278
+ // Full context handler
279
+ handlerFunc(event, element, state, loaderData, props, stores);
280
+ }
262
281
  } else {
263
282
  console.warn('[Zenith] Event handler "' + handlerName + '" not found for ' + eventType + ' event');
264
283
  }
@@ -366,11 +385,11 @@ export function generateExpressionRegistry(expressions: ExpressionIR[]): string
366
385
  // Registry already initialized
367
386
  }`
368
387
  }
369
-
388
+
370
389
  const registryCode = expressions.map(expr => {
371
390
  return ` window.__ZENITH_EXPRESSIONS__.set('${expr.id}', ${expr.id});`
372
391
  }).join('\n')
373
-
392
+
374
393
  return `
375
394
  // Initialize expression registry
376
395
  if (typeof window !== 'undefined') {
@@ -8,9 +8,9 @@ import type { ZenIR } from '../ir/types'
8
8
  import { generateExpressionWrappers } from './wrapExpression'
9
9
  import { generateDOMFunction } from './generateDOM'
10
10
  import { generateHydrationRuntime, generateExpressionRegistry } from './generateHydrationBundle'
11
- import { analyzeAllExpressions, type ExpressionDataDependencies } from './dataExposure'
11
+ import { analyzeAllExpressions } from './dataExposure'
12
12
  import { generateNavigationRuntime } from './navigation'
13
- import { extractStateDeclarations } from '../legacy/parse'
13
+ import { extractStateDeclarations, extractProps, transformStateDeclarations } from '../parse/scriptAnalysis'
14
14
 
15
15
  export interface RuntimeCode {
16
16
  expressions: string // Expression wrapper functions
@@ -30,44 +30,46 @@ export function transformIR(ir: ZenIR): RuntimeCode {
30
30
  const expressionDependencies = analyzeAllExpressions(
31
31
  ir.template.expressions,
32
32
  ir.filePath,
33
- [], // declaredLoaderProps - can be enhanced with loader analysis
34
- [], // declaredProps - can be enhanced with component prop analysis
35
- [] // declaredStores - can be enhanced with store import analysis
33
+ [], // declaredLoaderProps
34
+ ir.script?.attributes['props'] ? ir.script.attributes['props'].split(',') : [], // declaredProps
35
+ [] // declaredStores
36
36
  )
37
-
37
+
38
38
  // Generate expression wrappers with dependencies
39
39
  const expressions = generateExpressionWrappers(ir.template.expressions, expressionDependencies)
40
-
40
+
41
41
  // Generate DOM creation code
42
42
  const renderFunction = generateDOMFunction(
43
43
  ir.template.nodes,
44
44
  ir.template.expressions,
45
45
  'renderDynamicPage'
46
46
  )
47
-
47
+
48
48
  // Generate hydrate function (legacy, for reference)
49
49
  const hydrateFunction = generateHydrateFunction()
50
-
50
+
51
51
  // Generate Phase 5 hydration runtime
52
52
  const hydrationRuntime = generateHydrationRuntime()
53
-
53
+
54
54
  // Generate Phase 7 navigation runtime
55
55
  const navigationRuntime = generateNavigationRuntime()
56
-
56
+
57
57
  // Generate expression registry initialization
58
58
  const expressionRegistry = generateExpressionRegistry(ir.template.expressions)
59
-
59
+
60
60
  // Generate style injection code
61
61
  const stylesCode = generateStyleInjection(ir.styles)
62
-
63
- // Extract state declarations and generate initialization
62
+
63
+ // Extract state and prop declarations
64
64
  const scriptContent = ir.script?.raw || ''
65
65
  const stateDeclarations = extractStateDeclarations(scriptContent)
66
- const stateInitCode = generateStateInitialization(stateDeclarations)
67
-
68
- // Transform script (remove state declarations, they're handled by runtime)
69
- const scriptCode = transformScript(scriptContent, stateDeclarations)
70
-
66
+ const propKeys = Object.keys(ir.script?.attributes || {}).filter(k => k !== 'setup' && k !== 'lang')
67
+ const propDeclarations = extractProps(scriptContent)
68
+ const stateInitCode = generateStateInitialization(stateDeclarations, [...propDeclarations, ...propKeys])
69
+
70
+ // Transform script (remove state and prop declarations, they're handled by runtime)
71
+ const scriptCode = transformStateDeclarations(scriptContent)
72
+
71
73
  // Generate complete runtime bundle
72
74
  const bundle = generateRuntimeBundle({
73
75
  expressions,
@@ -78,7 +80,7 @@ export function transformIR(ir: ZenIR): RuntimeCode {
78
80
  scriptCode,
79
81
  stateInitCode
80
82
  })
81
-
83
+
82
84
  return {
83
85
  expressions,
84
86
  render: renderFunction,
@@ -102,6 +104,9 @@ function generateRuntimeBundle(parts: {
102
104
  scriptCode: string
103
105
  stateInitCode: string
104
106
  }): string {
107
+ // Extract function declarations from script code to register on window
108
+ const functionRegistrations = extractFunctionRegistrations(parts.scriptCode)
109
+
105
110
  return `// Zenith Runtime Bundle (Phase 5)
106
111
  // Generated at compile time - no .zen parsing in browser
107
112
 
@@ -119,8 +124,14 @@ ${parts.stateInitCode}` : ''}
119
124
  ${parts.stylesCode ? `// Style injection
120
125
  ${parts.stylesCode}` : ''}
121
126
 
122
- ${parts.scriptCode ? `// User script code
123
- ${parts.scriptCode}` : ''}
127
+ // User script code and function registration
128
+ (function() {
129
+ 'use strict';
130
+
131
+ ${parts.scriptCode ? parts.scriptCode : ''}
132
+
133
+ ${functionRegistrations}
134
+ })();
124
135
 
125
136
  // Export hydration functions
126
137
  if (typeof window !== 'undefined') {
@@ -137,9 +148,95 @@ if (typeof window !== 'undefined') {
137
148
  console.warn('[Zenith] Cleanup runtime not loaded');
138
149
  };
139
150
  }
151
+
152
+ // Auto-hydrate on page mount
153
+ (function() {
154
+ 'use strict';
155
+
156
+ function autoHydrate() {
157
+ // Initialize state object
158
+ const state = window.__ZENITH_STATE__ || {};
159
+
160
+ // Run state initialization if defined
161
+ if (typeof initializeState === 'function') {
162
+ initializeState(state);
163
+ }
164
+
165
+ // Store state globally
166
+ window.__ZENITH_STATE__ = state;
167
+
168
+ // Expose state variables on window with reactive getters/setters
169
+ // This allows user functions (like increment) to access state variables directly
170
+ for (const key in state) {
171
+ if (state.hasOwnProperty(key) && !window.hasOwnProperty(key)) {
172
+ Object.defineProperty(window, key, {
173
+ get: function() { return window.__ZENITH_STATE__[key]; },
174
+ set: function(value) {
175
+ window.__ZENITH_STATE__[key] = value;
176
+ // Trigger reactive update
177
+ if (window.__zenith_update) {
178
+ window.__zenith_update(window.__ZENITH_STATE__);
179
+ }
180
+ },
181
+ configurable: true
182
+ });
183
+ }
184
+ }
185
+
186
+ // Inject styles if defined
187
+ if (typeof injectStyles === 'function') {
188
+ injectStyles();
189
+ }
190
+
191
+ // Get the router outlet or body
192
+ const container = document.querySelector('#app') || document.body;
193
+
194
+ // Hydrate with state
195
+ if (window.__zenith_hydrate) {
196
+ window.__zenith_hydrate(state, {}, {}, {}, container);
197
+ }
198
+ }
199
+
200
+ // Run on DOM ready
201
+ if (document.readyState === 'loading') {
202
+ document.addEventListener('DOMContentLoaded', autoHydrate);
203
+ } else {
204
+ // DOM already loaded, run on next tick to ensure all scripts are executed
205
+ setTimeout(autoHydrate, 0);
206
+ }
207
+ })();
140
208
  `
141
209
  }
142
210
 
211
+ /**
212
+ * Extract function declarations and generate window registration code
213
+ */
214
+ function extractFunctionRegistrations(scriptCode: string): string {
215
+ if (!scriptCode) return ''
216
+
217
+ // Match function declarations: function name(...) { ... }
218
+ const functionPattern = /function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g
219
+ const functionNames: string[] = []
220
+ let match
221
+
222
+ while ((match = functionPattern.exec(scriptCode)) !== null) {
223
+ if (match[1]) {
224
+ functionNames.push(match[1])
225
+ }
226
+ }
227
+
228
+ if (functionNames.length === 0) {
229
+ return ''
230
+ }
231
+
232
+ // Generate window registration for each function
233
+ const registrations = functionNames.map(name =>
234
+ ` if (typeof ${name} === 'function') window.${name} = ${name};`
235
+ ).join('\n')
236
+
237
+ return `// Register functions on window for event handlers\n${registrations}`
238
+ }
239
+
143
240
  /**
144
241
  * Generate hydrate function that mounts the DOM with reactivity
145
242
  */
@@ -204,7 +301,7 @@ function generateStyleInjection(styles: Array<{ raw: string }>): string {
204
301
  if (styles.length === 0) {
205
302
  return ''
206
303
  }
207
-
304
+
208
305
  const styleBlocks = styles.map((style, index) => {
209
306
  const escapedStyle = style.raw.replace(/`/g, '\\`').replace(/\$/g, '\\$')
210
307
  return `
@@ -212,45 +309,39 @@ function generateStyleInjection(styles: Array<{ raw: string }>): string {
212
309
  style${index}.textContent = \`${escapedStyle}\`;
213
310
  document.head.appendChild(style${index});`
214
311
  }).join('')
215
-
312
+
216
313
  return `function injectStyles() {${styleBlocks}
217
314
  }`
218
315
  }
219
316
 
220
317
  /**
221
318
  * Generate state initialization code
319
+ * In Phase 9: Also handles props passing
222
320
  */
223
- function generateStateInitialization(stateDeclarations: Map<string, string>): string {
224
- if (stateDeclarations.size === 0) {
225
- return ''
226
- }
227
-
228
- const initCode = Array.from(stateDeclarations.entries()).map(([name, value]) => {
321
+ function generateStateInitialization(stateDeclarations: Map<string, string>, propDeclarations: string[]): string {
322
+ const stateInit = Array.from(stateDeclarations.entries()).map(([name, value]) => {
229
323
  return `
230
324
  // Initialize state: ${name}
231
- if (!state.${name}) {
325
+ if (typeof state.${name} === 'undefined') {
232
326
  state.${name} = ${value};
233
327
  }`
234
328
  }).join('')
235
-
236
- return `function initializeState(state) {${initCode}
329
+
330
+ const legacyPropInit = propDeclarations.includes('props') ? `
331
+ // Initialize props object (legacy)
332
+ if (typeof window.__ZEN_PROPS__ !== 'undefined') {
333
+ state.props = window.__ZEN_PROPS__;
334
+ }` : ''
335
+
336
+ const individualPropInit = propDeclarations.filter(p => p !== 'props').map(prop => `
337
+ // Initialize prop: ${prop}
338
+ if (typeof state.${prop} === 'undefined' && typeof window.__ZEN_PROPS__ !== 'undefined' && typeof window.__ZEN_PROPS__.${prop} !== 'undefined') {
339
+ state.${prop} = window.__ZEN_PROPS__.${prop};
340
+ }`).join('')
341
+
342
+ return `function initializeState(state) {${stateInit}${legacyPropInit}${individualPropInit}
237
343
  }`
238
344
  }
239
345
 
240
- /**
241
- * Transform script content
242
- * Removes state declarations (they're handled by state initialization)
243
- */
244
- function transformScript(scriptContent: string, stateDeclarations: Map<string, string>): string {
245
- // Remove state declarations - they're handled by initializeState
246
- let transformed = scriptContent
247
-
248
- for (const [name] of stateDeclarations.entries()) {
249
- // Remove "state name = value" declarations
250
- const stateRegex = new RegExp(`state\\s+${name}\\s*=[^;]*;?`, 'g')
251
- transformed = transformed.replace(stateRegex, '')
252
- }
253
-
254
- return transformed.trim()
255
- }
346
+ // Note: transformScript is now handled by transformStateDeclarations in legacy/parse.ts
256
347
 
@@ -23,42 +23,42 @@ export function wrapExpressionWithLoopContext(
23
23
  ): string {
24
24
  const { id, code } = expr
25
25
  const escapedCode = code.replace(/`/g, '\\`').replace(/\$/g, '\\$')
26
-
26
+
27
27
  if (!loopContext || loopContext.variables.length === 0) {
28
28
  // No loop context - use standard wrapper (will be handled by wrapExpression)
29
29
  return ''
30
30
  }
31
-
31
+
32
32
  // Determine arguments based on dependencies
33
33
  const args: string[] = []
34
- if (dependencies?.usesState || dependencies?.stateProperties.length > 0) args.push('state')
34
+ if (dependencies?.usesState || (dependencies?.stateProperties && dependencies.stateProperties.length > 0)) args.push('state')
35
35
  if (dependencies?.usesLoaderData) args.push('loaderData')
36
36
  if (dependencies?.usesProps) args.push('props')
37
37
  if (dependencies?.usesStores) args.push('stores')
38
-
38
+
39
39
  // Phase 7: Always add loopContext as the last argument
40
40
  args.push('loopContext')
41
-
41
+
42
42
  const argsStr = args.join(', ')
43
-
43
+
44
44
  // Generate function that merges state and loop context
45
45
  // Loop context variables take precedence over state properties with the same name
46
46
  const loopVarsDecl = loopContext.variables.map(v => ` const ${v} = loopContext?.${v};`).join('\n')
47
47
  const loopVarsObject = `{ ${loopContext.variables.join(', ')} }`
48
-
48
+
49
49
  // Create merged context for expression evaluation
50
50
  // Order: loopContext > stores > props > loaderData > state
51
51
  const contextMerge: string[] = []
52
- if (dependencies?.usesState || dependencies?.stateProperties.length > 0) contextMerge.push('state')
52
+ if (dependencies?.usesState || (dependencies?.stateProperties && dependencies.stateProperties.length > 0)) contextMerge.push('state')
53
53
  if (dependencies?.usesStores) contextMerge.push('stores')
54
54
  if (dependencies?.usesProps) contextMerge.push('props')
55
55
  if (dependencies?.usesLoaderData) contextMerge.push('loaderData')
56
56
  if (loopContext) contextMerge.push('loopContext')
57
-
58
- const contextObject = contextMerge.length > 0
57
+
58
+ const contextObject = contextMerge.length > 0
59
59
  ? `const __ctx = Object.assign({}, ${contextMerge.join(', ')});`
60
60
  : `const __ctx = loopContext || {};`
61
-
61
+
62
62
  return `
63
63
  // Expression with loop context: ${escapedCode}
64
64
  // Loop variables: ${loopContext.variables.join(', ')}