@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
@@ -32,22 +32,53 @@ function stripBlocks(html: string): string {
32
32
  /**
33
33
  * Normalize attribute expressions before parsing
34
34
  * Replaces attr={expr} with attr="__ZEN_EXPR_base64" so parse5 can parse it
35
+ * Handles nested braces for complex expressions like props={{ title: 'Home' }}
35
36
  */
36
37
  function normalizeAttributeExpressions(html: string): { normalized: string; expressions: Map<string, string> } {
37
38
  const exprMap = new Map<string, string>()
38
39
  let exprCounter = 0
39
-
40
- // Match attributes with expression values: attr={...}
41
- // Use a more sophisticated regex to handle nested braces and quotes
42
- const normalized = html.replace(/(\w+)=\{([^}]+)\}/g, (match, attrName, expr) => {
43
- const placeholder = `__ZEN_EXPR_${exprCounter++}`
44
- exprMap.set(placeholder, expr.trim())
45
- return `${attrName}="${placeholder}"`
46
- })
47
-
40
+
41
+ // Improved matching to handle nested braces: attr={ { foo: { bar: 1 } } }
42
+ let lastPos = 0
43
+ let normalized = ''
44
+
45
+ // Use a regex to find the start of an attribute expression: \w+={
46
+ const startRegex = /(\w+)=\{/g
47
+ let match
48
+
49
+ while ((match = startRegex.exec(html)) !== null) {
50
+ const attrName = match[1]
51
+ const startIndex = match.index + match[0].length - 1 // Index of '{'
52
+
53
+ // Find matching closing brace
54
+ let braceCount = 1
55
+ let i = startIndex + 1
56
+ while (i < html.length && braceCount > 0) {
57
+ if (html[i] === '{') braceCount++
58
+ else if (html[i] === '}') braceCount--
59
+ i++
60
+ }
61
+
62
+ if (braceCount === 0) {
63
+ const expr = html.substring(startIndex + 1, i - 1).trim()
64
+ const placeholder = `__ZEN_EXPR_${exprCounter++}`
65
+ exprMap.set(placeholder, expr)
66
+
67
+ normalized += html.substring(lastPos, match.index)
68
+ normalized += `${attrName}="${placeholder}"`
69
+ lastPos = i
70
+
71
+ // Update regex index to continue after the closing brace
72
+ startRegex.lastIndex = i
73
+ }
74
+ }
75
+
76
+ normalized += html.substring(lastPos)
77
+
48
78
  return { normalized, expressions: exprMap }
49
79
  }
50
80
 
81
+
51
82
  /**
52
83
  * Calculate source location from parse5 node
53
84
  */
@@ -77,11 +108,11 @@ function extractExpressionsFromText(
77
108
  const nodes: (TextNode | ExpressionNode)[] = []
78
109
  let processedText = ''
79
110
  let currentIndex = 0
80
-
111
+
81
112
  // Match { ... } expressions (non-greedy)
82
113
  const expressionRegex = /\{([^}]+)\}/g
83
114
  let match
84
-
115
+
85
116
  while ((match = expressionRegex.exec(text)) !== null) {
86
117
  const beforeExpr = text.substring(currentIndex, match.index)
87
118
  if (beforeExpr) {
@@ -95,7 +126,7 @@ function extractExpressionsFromText(
95
126
  })
96
127
  processedText += beforeExpr
97
128
  }
98
-
129
+
99
130
  // Extract expression
100
131
  const exprCode = (match[1] || '').trim()
101
132
  const exprId = generateExpressionId()
@@ -103,32 +134,32 @@ function extractExpressionsFromText(
103
134
  line: baseLocation.line,
104
135
  column: baseLocation.column + match.index + 1 // +1 for opening brace
105
136
  }
106
-
137
+
107
138
  const exprIR: ExpressionIR = {
108
139
  id: exprId,
109
140
  code: exprCode,
110
141
  location: exprLocation
111
142
  }
112
143
  expressions.push(exprIR)
113
-
144
+
114
145
  // Phase 7: Detect if this is a map expression and extract loop context
115
146
  const mapLoopContext = extractLoopContextFromExpression(exprIR)
116
147
  const activeLoopContext = mergeLoopContext(loopContext, mapLoopContext)
117
-
148
+
118
149
  // Phase 7: Attach loop context if expression references loop variables
119
150
  const attachedLoopContext = shouldAttachLoopContext(exprIR, activeLoopContext)
120
-
151
+
121
152
  nodes.push({
122
153
  type: 'expression',
123
154
  expression: exprId,
124
155
  location: exprLocation,
125
156
  loopContext: attachedLoopContext
126
157
  })
127
-
158
+
128
159
  processedText += `{${exprCode}}` // Keep in processed text for now
129
160
  currentIndex = match.index + match[0].length
130
161
  }
131
-
162
+
132
163
  // Add remaining text
133
164
  const remaining = text.substring(currentIndex)
134
165
  if (remaining) {
@@ -142,7 +173,7 @@ function extractExpressionsFromText(
142
173
  })
143
174
  processedText += remaining
144
175
  }
145
-
176
+
146
177
  // If no expressions found, return single text node
147
178
  if (nodes.length === 0) {
148
179
  nodes.push({
@@ -152,7 +183,7 @@ function extractExpressionsFromText(
152
183
  })
153
184
  processedText = text
154
185
  }
155
-
186
+
156
187
  return { processedText, nodes }
157
188
  }
158
189
 
@@ -173,41 +204,41 @@ function parseAttributeValue(
173
204
  if (!exprCode) {
174
205
  throw new Error(`Normalized expression placeholder not found: ${value}`)
175
206
  }
176
-
207
+
177
208
  const exprId = generateExpressionId()
178
-
209
+
179
210
  expressions.push({
180
211
  id: exprId,
181
212
  code: exprCode,
182
213
  location: baseLocation
183
214
  })
184
-
215
+
185
216
  return {
186
217
  id: exprId,
187
218
  code: exprCode,
188
219
  location: baseLocation
189
220
  }
190
221
  }
191
-
222
+
192
223
  // Check if attribute value is an expression { ... } (shouldn't happen after normalization)
193
224
  const exprMatch = value.match(/^\{([^}]+)\}$/)
194
225
  if (exprMatch && exprMatch[1]) {
195
226
  const exprCode = exprMatch[1].trim()
196
227
  const exprId = generateExpressionId()
197
-
228
+
198
229
  expressions.push({
199
230
  id: exprId,
200
231
  code: exprCode,
201
232
  location: baseLocation
202
233
  })
203
-
234
+
204
235
  return {
205
236
  id: exprId,
206
237
  code: exprCode,
207
238
  location: baseLocation
208
239
  }
209
240
  }
210
-
241
+
211
242
  // Regular string value
212
243
  return value
213
244
  }
@@ -226,16 +257,16 @@ function parseNode(
226
257
  if (node.nodeName === '#text') {
227
258
  const text = node.value || ''
228
259
  const location = getLocation(node, originalHtml)
229
-
260
+
230
261
  // Extract expressions from text
231
262
  // Phase 7: Pass loop context to detect map expressions and attach context
232
263
  const { nodes } = extractExpressionsFromText(text, location, expressions, parentLoopContext)
233
-
264
+
234
265
  // If single text node with no expressions, return it
235
266
  if (nodes.length === 1 && nodes[0] && nodes[0].type === 'text') {
236
267
  return nodes[0]
237
268
  }
238
-
269
+
239
270
  // Otherwise, we need to handle multiple nodes
240
271
  // For Phase 1, we'll flatten to text for now (will be handled in future phases)
241
272
  // This is a limitation we accept for Phase 1
@@ -249,32 +280,32 @@ function parseNode(
249
280
  location
250
281
  }
251
282
  }
252
-
283
+
253
284
  if (node.nodeName === '#comment') {
254
285
  // Skip comments for Phase 1
255
286
  return null
256
287
  }
257
-
288
+
258
289
  if (node.nodeName && node.nodeName !== '#text' && node.nodeName !== '#comment') {
259
290
  const location = getLocation(node, originalHtml)
260
291
  const tag = node.tagName?.toLowerCase() || node.nodeName
261
-
292
+
262
293
  // Parse attributes
263
294
  const attributes: AttributeIR[] = []
264
295
  if (node.attrs) {
265
296
  for (const attr of node.attrs) {
266
- const attrLocation = node.sourceCodeLocation?.attrs?.[attr.name]
297
+ const attrLocation = node.sourceCodeLocation?.attrs?.[attr.name]
267
298
  ? {
268
- line: node.sourceCodeLocation.attrs[attr.name].startLine || location.line,
269
- column: node.sourceCodeLocation.attrs[attr.name].startCol || location.column
270
- }
299
+ line: node.sourceCodeLocation.attrs[attr.name].startLine || location.line,
300
+ column: node.sourceCodeLocation.attrs[attr.name].startCol || location.column
301
+ }
271
302
  : location
272
-
303
+
273
304
  // Handle :attr="expr" syntax (colon-prefixed reactive attributes)
274
305
  let attrName = attr.name
275
306
  let attrValue = attr.value || ''
276
307
  let isReactive = false
277
-
308
+
278
309
  if (attrName.startsWith(':')) {
279
310
  // This is a reactive attribute like :class="expr"
280
311
  attrName = attrName.slice(1) // Remove the colon
@@ -283,17 +314,17 @@ function parseNode(
283
314
  // Treat it as an expression
284
315
  const exprId = generateExpressionId()
285
316
  const exprCode = attrValue.trim()
286
-
317
+
287
318
  const exprIR: ExpressionIR = {
288
319
  id: exprId,
289
320
  code: exprCode,
290
321
  location: attrLocation
291
322
  }
292
323
  expressions.push(exprIR)
293
-
324
+
294
325
  // Phase 7: Attach loop context if expression references loop variables
295
326
  const attachedLoopContext = shouldAttachLoopContext(exprIR, parentLoopContext)
296
-
327
+
297
328
  attributes.push({
298
329
  name: attrName, // Store without colon (e.g., "class" not ":class")
299
330
  value: exprIR,
@@ -303,23 +334,30 @@ function parseNode(
303
334
  } else {
304
335
  // Regular attribute or attr={expr} syntax
305
336
  const attrValueResult = parseAttributeValue(attrValue, attrLocation, expressions, normalizedExprs, parentLoopContext)
306
-
337
+
338
+ // Transform event attributes: onclick -> data-zen-click, onchange -> data-zen-change, etc.
339
+ let finalAttrName = attrName
340
+ if (attrName.startsWith('on') && attrName.length > 2) {
341
+ const eventType = attrName.slice(2) // Remove "on" prefix
342
+ finalAttrName = `data-zen-${eventType}`
343
+ }
344
+
307
345
  if (typeof attrValueResult === 'string') {
308
346
  // Static attribute value
309
347
  attributes.push({
310
- name: attrName,
348
+ name: finalAttrName,
311
349
  value: attrValueResult,
312
350
  location: attrLocation
313
351
  })
314
352
  } else {
315
353
  // Expression attribute value
316
354
  const exprIR = attrValueResult
317
-
355
+
318
356
  // Phase 7: Attach loop context if expression references loop variables
319
357
  const attachedLoopContext = shouldAttachLoopContext(exprIR, parentLoopContext)
320
-
358
+
321
359
  attributes.push({
322
- name: attrName,
360
+ name: finalAttrName,
323
361
  value: exprIR,
324
362
  location: attrLocation,
325
363
  loopContext: attachedLoopContext
@@ -328,7 +366,7 @@ function parseNode(
328
366
  }
329
367
  }
330
368
  }
331
-
369
+
332
370
  // Parse children
333
371
  const children: TemplateNode[] = []
334
372
  if (node.childNodes) {
@@ -338,7 +376,7 @@ function parseNode(
338
376
  const text = child.value || ''
339
377
  const location = getLocation(child, originalHtml)
340
378
  const { nodes: textNodes } = extractExpressionsFromText(text, location, expressions, parentLoopContext)
341
-
379
+
342
380
  // Add all nodes from text (can be multiple: text + expression + text)
343
381
  for (const textNode of textNodes) {
344
382
  children.push(textNode)
@@ -351,11 +389,11 @@ function parseNode(
351
389
  }
352
390
  }
353
391
  }
354
-
392
+
355
393
  // Phase 7: Check if any child expression is a map expression and extract its loop context
356
394
  // This allows nested loops to work correctly
357
395
  let elementLoopContext = parentLoopContext
358
-
396
+
359
397
  // Check children for map expressions (they create new loop contexts)
360
398
  for (const child of children) {
361
399
  if (child.type === 'expression' && child.loopContext) {
@@ -363,7 +401,7 @@ function parseNode(
363
401
  elementLoopContext = mergeLoopContext(elementLoopContext, child.loopContext)
364
402
  }
365
403
  }
366
-
404
+
367
405
  return {
368
406
  type: 'element',
369
407
  tag,
@@ -373,7 +411,7 @@ function parseNode(
373
411
  loopContext: elementLoopContext // Phase 7: Inherited loop context for child processing
374
412
  }
375
413
  }
376
-
414
+
377
415
  return null
378
416
  }
379
417
 
@@ -383,20 +421,20 @@ function parseNode(
383
421
  export function parseTemplate(html: string, filePath: string): TemplateIR {
384
422
  // Strip script and style blocks
385
423
  let templateHtml = stripBlocks(html)
386
-
424
+
387
425
  // Normalize attribute expressions so parse5 can parse them
388
426
  const { normalized, expressions: normalizedExprs } = normalizeAttributeExpressions(templateHtml)
389
427
  templateHtml = normalized
390
-
428
+
391
429
  try {
392
430
  // Parse HTML using parseFragment (handles fragments without html/body wrapper)
393
431
  const fragment = parseFragment(templateHtml, {
394
432
  sourceCodeLocationInfo: true
395
433
  })
396
-
434
+
397
435
  const expressions: ExpressionIR[] = []
398
436
  const nodes: TemplateNode[] = []
399
-
437
+
400
438
  // Parse fragment children
401
439
  // Phase 7: Start with no loop context (top-level expressions)
402
440
  if (fragment.childNodes) {
@@ -407,7 +445,7 @@ export function parseTemplate(html: string, filePath: string): TemplateIR {
407
445
  }
408
446
  }
409
447
  }
410
-
448
+
411
449
  return {
412
450
  raw: templateHtml,
413
451
  nodes,
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Script Analysis Utilities
3
+ *
4
+ * Extracts state and prop declarations from <script> blocks
5
+ */
6
+
7
+ export interface StateInfo {
8
+ name: string
9
+ value: string
10
+ }
11
+
12
+ /**
13
+ * Extract state declarations: state name = value
14
+ */
15
+ export function extractStateDeclarations(script: string): Map<string, string> {
16
+ const states = new Map<string, string>()
17
+ const statePattern = /state\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*([^;]+?)(?:\s*;|\s*$)/gm
18
+ let match
19
+
20
+ while ((match = statePattern.exec(script)) !== null) {
21
+ if (match[1] && match[2]) {
22
+ states.set(match[1], match[2].trim())
23
+ }
24
+ }
25
+
26
+ return states
27
+ }
28
+
29
+ /**
30
+ * Extract prop declarations: export let props: Props;
31
+ */
32
+ export function extractProps(script: string): string[] {
33
+ const props: string[] = []
34
+ const propPattern = /export\s+let\s+props(?:\s*:\s*([^;]+))?[ \t]*;?/g
35
+ let match
36
+
37
+ while ((match = propPattern.exec(script)) !== null) {
38
+ if (!props.includes('props')) {
39
+ props.push('props')
40
+ }
41
+ }
42
+
43
+ return props
44
+ }
45
+
46
+ /**
47
+ * Transform script by removing state and prop declarations
48
+ */
49
+ export function transformStateDeclarations(script: string): string {
50
+ let transformed = script
51
+
52
+ // Remove state declarations (state count = 0)
53
+ transformed = transformed.replace(/state\s+([a-zA-Z_$][a-zA-Z0-9_$]*)[ \t]*=[ \t]*([^;]+?)(?:[ \t]*;|\s*$)/gm, '')
54
+
55
+ // Remove export let props (legacy)
56
+ transformed = transformed.replace(/export\s+let\s+props(?:\s*:\s*([^;]+))?\s*;?[ \t]*/g, '')
57
+
58
+ // Remove type/interface Props (carefully handling comments)
59
+ // We search for the start of the word 'type' or 'interface' and match until the closing brace
60
+ transformed = transformed.replace(/(?:type|interface)\s+Props\s*=?\s*\{[^}]*(?:\{[^}]*\}[^}]*)*\}[ \t]*;?/gs, '')
61
+
62
+ // Remove zenith/runtime imports
63
+ transformed = transformed.replace(/import\s+{[^}]+}\s+from\s+['"]zenith\/runtime['"]\s*;?[ \t]*/g, '')
64
+
65
+ return transformed.trim()
66
+ }
67
+
68
+ /**
69
+ * Inject props into a setup script as top-level variables
70
+ */
71
+ export function injectPropsIntoSetup(script: string, props: Record<string, any>): string {
72
+ const propDeclarations = Object.entries(props)
73
+ .map(([key, value]) => `const ${key} = ${typeof value === 'string' ? `'${value}'` : JSON.stringify(value)};`)
74
+ .join('\n')
75
+
76
+ return `${propDeclarations}\n\n${script}`
77
+ }
@@ -39,7 +39,7 @@ export function analyzeExpressionDependencies(
39
39
  declaredStores: string[] = []
40
40
  ): ExpressionDataDependencies {
41
41
  const { id, code } = expr
42
-
42
+
43
43
  const dependencies: ExpressionDataDependencies = {
44
44
  expressionId: id,
45
45
  usesLoaderData: false,
@@ -51,14 +51,14 @@ export function analyzeExpressionDependencies(
51
51
  storeNames: [],
52
52
  stateProperties: []
53
53
  }
54
-
54
+
55
55
  // Simple pattern matching (for Phase 6 - can be enhanced with proper AST parsing later)
56
-
56
+
57
57
  // Check for loader data references (loaderData.property or direct property access)
58
58
  // We assume properties not starting with props/stores/state are loader data
59
59
  const loaderPattern = /\b(loaderData\.(\w+(?:\.\w+)*)|(?<!props\.|stores\.|state\.)(\w+)\.(\w+))/g
60
60
  let match
61
-
61
+
62
62
  // Check for explicit loaderData references
63
63
  if (/loaderData\./.test(code)) {
64
64
  dependencies.usesLoaderData = true
@@ -71,7 +71,7 @@ export function analyzeExpressionDependencies(
71
71
  }
72
72
  }
73
73
  }
74
-
74
+
75
75
  // Check for props references
76
76
  const propsPattern = /\bprops\.(\w+)(?:\.(\w+))*/g
77
77
  if (/props\./.test(code)) {
@@ -83,7 +83,7 @@ export function analyzeExpressionDependencies(
83
83
  }
84
84
  }
85
85
  }
86
-
86
+
87
87
  // Check for stores references
88
88
  const storesPattern = /\bstores\.(\w+)(?:\.(\w+))*/g
89
89
  if (/stores\./.test(code)) {
@@ -95,12 +95,12 @@ export function analyzeExpressionDependencies(
95
95
  }
96
96
  }
97
97
  }
98
-
98
+
99
99
  // Check for state references (top-level properties)
100
100
  // Simple identifiers that aren't part of props/stores/loaderData paths
101
101
  const identifierPattern = /\b([a-zA-Z_$][a-zA-Z0-9_$]*)\b/g
102
102
  const reserved = ['props', 'stores', 'loaderData', 'state', 'true', 'false', 'null', 'undefined', 'this', 'window']
103
-
103
+
104
104
  const identifiers = new Set<string>()
105
105
  while ((match = identifierPattern.exec(code)) !== null) {
106
106
  const ident = match[1]
@@ -108,13 +108,31 @@ export function analyzeExpressionDependencies(
108
108
  identifiers.add(ident)
109
109
  }
110
110
  }
111
-
112
- // If we have identifiers and no explicit data source, assume state
113
- if (identifiers.size > 0 && !dependencies.usesLoaderData && !dependencies.usesProps && !dependencies.usesStores) {
114
- dependencies.usesState = true
115
- dependencies.stateProperties = Array.from(identifiers)
111
+
112
+ // If we have identifiers, check if they are props or state
113
+ if (identifiers.size > 0) {
114
+ const propIdents: string[] = []
115
+ const stateIdents: string[] = []
116
+
117
+ for (const ident of identifiers) {
118
+ if (declaredProps.includes(ident)) {
119
+ propIdents.push(ident)
120
+ } else {
121
+ stateIdents.push(ident)
122
+ }
123
+ }
124
+
125
+ if (propIdents.length > 0) {
126
+ dependencies.usesProps = true
127
+ dependencies.propNames = [...new Set([...dependencies.propNames, ...propIdents])]
128
+ }
129
+
130
+ if (stateIdents.length > 0) {
131
+ dependencies.usesState = true
132
+ dependencies.stateProperties = Array.from(new Set([...dependencies.stateProperties, ...stateIdents]))
133
+ }
116
134
  }
117
-
135
+
118
136
  return dependencies
119
137
  }
120
138
 
@@ -129,7 +147,7 @@ export function validateDataDependencies(
129
147
  declaredStores: string[] = []
130
148
  ): void {
131
149
  const errors: CompilerError[] = []
132
-
150
+
133
151
  // Validate loader data properties
134
152
  if (dependencies.usesLoaderData && dependencies.loaderProperties.length > 0) {
135
153
  // For Phase 6, we'll allow any loader property (can be enhanced with type checking later)
@@ -145,7 +163,7 @@ export function validateDataDependencies(
145
163
  }
146
164
  }
147
165
  }
148
-
166
+
149
167
  // Validate props
150
168
  if (dependencies.usesProps && dependencies.propNames.length > 0) {
151
169
  for (const propName of dependencies.propNames) {
@@ -155,7 +173,7 @@ export function validateDataDependencies(
155
173
  }
156
174
  }
157
175
  }
158
-
176
+
159
177
  // Validate stores
160
178
  if (dependencies.usesStores && dependencies.storeNames.length > 0) {
161
179
  for (const storeName of dependencies.storeNames) {
@@ -169,7 +187,7 @@ export function validateDataDependencies(
169
187
  }
170
188
  }
171
189
  }
172
-
190
+
173
191
  if (errors.length > 0) {
174
192
  throw errors[0] // Throw first error (can be enhanced to collect all)
175
193
  }
@@ -189,14 +207,14 @@ export function transformExpressionCode(
189
207
  declaredProps: string[] = []
190
208
  ): string {
191
209
  let transformed = code
192
-
210
+
193
211
  // For Phase 6, we keep the code as-is but ensure expressions
194
212
  // receive the right arguments. The actual transformation happens
195
213
  // in the expression wrapper function signature.
196
-
214
+
197
215
  // However, if the code references properties directly (without loaderData/props/stores prefix),
198
216
  // we need to assume they're state properties (backwards compatibility)
199
-
217
+
200
218
  return transformed
201
219
  }
202
220
 
@@ -208,10 +226,10 @@ export function generateExplicitExpressionWrapper(
208
226
  dependencies: ExpressionDataDependencies
209
227
  ): string {
210
228
  const { id, code } = expr
211
-
229
+
212
230
  // Build function signature based on dependencies
213
231
  const params: string[] = ['state']
214
-
232
+
215
233
  if (dependencies.usesLoaderData) {
216
234
  params.push('loaderData')
217
235
  }
@@ -221,12 +239,12 @@ export function generateExplicitExpressionWrapper(
221
239
  if (dependencies.usesStores) {
222
240
  params.push('stores')
223
241
  }
224
-
242
+
225
243
  const paramList = params.join(', ')
226
-
244
+
227
245
  // Build evaluation context
228
246
  const contextParts: string[] = []
229
-
247
+
230
248
  if (dependencies.usesLoaderData) {
231
249
  contextParts.push('loaderData')
232
250
  }
@@ -239,14 +257,14 @@ export function generateExplicitExpressionWrapper(
239
257
  if (dependencies.usesState) {
240
258
  contextParts.push('state')
241
259
  }
242
-
260
+
243
261
  // Create merged context for 'with' statement
244
262
  const contextCode = contextParts.length > 0
245
263
  ? `const __ctx = Object.assign({}, ${contextParts.join(', ')});\n with (__ctx) {`
246
264
  : 'with (state) {'
247
-
265
+
248
266
  const escapedCode = code.replace(/`/g, '\\`').replace(/\$/g, '\\$')
249
-
267
+
250
268
  return `
251
269
  // Expression: ${escapedCode}
252
270
  // Dependencies: ${JSON.stringify({
@@ -280,12 +298,12 @@ export function analyzeAllExpressions(
280
298
  const dependencies = expressions.map(expr =>
281
299
  analyzeExpressionDependencies(expr, declaredLoaderProps, declaredProps, declaredStores)
282
300
  )
283
-
301
+
284
302
  // Validate all dependencies
285
303
  for (const dep of dependencies) {
286
304
  validateDataDependencies(dep, filePath, declaredLoaderProps, declaredProps, declaredStores)
287
305
  }
288
-
306
+
289
307
  return dependencies
290
308
  }
291
309