@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,280 @@
1
+ // compiler/mutation.ts
2
+ // State mutation detection and validation
3
+
4
+ export interface EventHandlerInfo {
5
+ functionName: string;
6
+ isInline: boolean; // true if it's an inline arrow function
7
+ inlineCode?: string; // the arrow function code if isInline is true
8
+ }
9
+
10
+ /**
11
+ * Extract event handler names from HTML (both function names and inline arrow functions)
12
+ * Returns: { eventHandlers: Set<string>, inlineHandlers: Map<string, string> }
13
+ * where eventHandlers contains all function names (including generated ones for inline functions)
14
+ * and inlineHandlers maps generated function names to their code
15
+ */
16
+ export interface InlineHandlerInfo {
17
+ body: string;
18
+ paramName: string; // the parameter name from the arrow function (e.g., "e", "event", "")
19
+ }
20
+
21
+ export function extractEventHandlers(html: string): {
22
+ eventHandlers: Set<string>;
23
+ inlineHandlers: Map<string, InlineHandlerInfo>; // functionName -> arrow function info
24
+ } {
25
+ const eventHandlers = new Set<string>();
26
+ const inlineHandlers = new Map<string, InlineHandlerInfo>();
27
+ let inlineCounter = 0;
28
+
29
+ // Simple regex to find onclick="..." and similar attributes
30
+ // Match: onclick="..." or onclick='...'
31
+ const eventAttrRegex = /on(\w+)="([^"]*)"|on(\w+)='([^']*)'/gi;
32
+ let match;
33
+
34
+ while ((match = eventAttrRegex.exec(html)) !== null) {
35
+ const handlerValue = match[2] || match[4]; // Get value from either quote type
36
+
37
+ if (!handlerValue) continue;
38
+
39
+ // Check if it's an inline arrow function: () => ... or (e) => ... or (event) => ...
40
+ const arrowFunctionMatch = handlerValue.match(/^\s*\(([^)]*)\)\s*=>\s*(.+)$/);
41
+
42
+ if (arrowFunctionMatch) {
43
+ // It's an inline arrow function - generate a function name
44
+ const generatedName = `__zen_inline_handler_${inlineCounter++}`;
45
+ eventHandlers.add(generatedName);
46
+
47
+ // Extract the parameter name and body
48
+ const paramName = (arrowFunctionMatch[1] || '').trim();
49
+ const arrowBody = (arrowFunctionMatch[2] || '').trim();
50
+ inlineHandlers.set(generatedName, { body: arrowBody, paramName });
51
+ } else {
52
+ // It's a function name reference
53
+ const functionName = handlerValue.trim();
54
+ if (functionName && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(functionName)) {
55
+ eventHandlers.add(functionName);
56
+
57
+ // Also extract the base function name if it's instance-scoped (__zen_comp_0_handleClick -> handleClick)
58
+ const instanceScopedMatch = functionName.match(/^__zen_comp_\d+_(.+)$/);
59
+ if (instanceScopedMatch && instanceScopedMatch[1]) {
60
+ eventHandlers.add(instanceScopedMatch[1]); // Add the base function name
61
+ }
62
+ }
63
+ }
64
+ }
65
+
66
+ // Also extract function names from component props (e.g., <Button onClick=increment />)
67
+ // Look for component tags with attributes that are function names (not quoted, valid identifier)
68
+ // Pattern: <ComponentName propName=functionName /> or <ComponentName propName="functionName" />
69
+ const componentPropRegex = /<[A-Z]\w+\s+[^>]*?(\w+)=([a-zA-Z_$][a-zA-Z0-9_$]*)[^>]*>/gi;
70
+ let componentMatch;
71
+ while ((componentMatch = componentPropRegex.exec(html)) !== null) {
72
+ const propValue = componentMatch[2];
73
+ // If it's a valid function name (not quoted, valid identifier), add it to event handlers
74
+ // This allows functions passed as props to mutate state
75
+ if (propValue && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(propValue)) {
76
+ eventHandlers.add(propValue);
77
+ }
78
+ }
79
+
80
+ return { eventHandlers, inlineHandlers };
81
+ }
82
+
83
+ /**
84
+ * Detect state mutations in JavaScript code
85
+ * Returns an array of mutation locations: { functionName, line, column, stateName }
86
+ */
87
+ export interface MutationLocation {
88
+ functionName: string | null; // null means top-level (outside any function)
89
+ line: number;
90
+ column: number;
91
+ stateName: string;
92
+ code: string; // the mutation code for error reporting
93
+ }
94
+
95
+ /**
96
+ * Extract function declarations and their boundaries from JavaScript code
97
+ */
98
+ interface FunctionInfo {
99
+ name: string;
100
+ startLine: number;
101
+ endLine: number;
102
+ }
103
+
104
+ function extractFunctions(scriptContent: string): FunctionInfo[] {
105
+ const functions: FunctionInfo[] = [];
106
+ const lines = scriptContent.split('\n');
107
+
108
+ // Simple regex to match: function name(...) {
109
+ const functionRegex = /function\s+(\w+)\s*\([^)]*\)\s*\{/g;
110
+
111
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
112
+ const line = lines[lineIndex];
113
+ if (!line) continue;
114
+ const match = functionRegex.exec(line);
115
+
116
+ if (match && match[1]) {
117
+ const funcName = match[1];
118
+ const startLine = lineIndex;
119
+
120
+ // Find the matching closing brace using brace counting
121
+ let braceCount = 1;
122
+ let endLine = startLine;
123
+
124
+ for (let i = lineIndex + 1; i < lines.length && braceCount > 0; i++) {
125
+ const currentLine = lines[i];
126
+ if (!currentLine) continue;
127
+ const openBraces = (currentLine.match(/\{/g) || []).length;
128
+ const closeBraces = (currentLine.match(/\}/g) || []).length;
129
+ braceCount += openBraces - closeBraces;
130
+ if (braceCount === 0) {
131
+ endLine = i;
132
+ break;
133
+ }
134
+ }
135
+
136
+ functions.push({ name: funcName, startLine, endLine });
137
+ }
138
+
139
+ // Reset regex for next line
140
+ functionRegex.lastIndex = 0;
141
+ }
142
+
143
+ return functions;
144
+ }
145
+
146
+ export function detectStateMutations(
147
+ scriptContent: string,
148
+ declaredStates: Set<string>
149
+ ): MutationLocation[] {
150
+ const mutations: MutationLocation[] = [];
151
+ const lines = scriptContent.split('\n');
152
+ const functions = extractFunctions(scriptContent);
153
+
154
+ // Check each line for mutations
155
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
156
+ const line = lines[lineIndex];
157
+ if (!line) continue;
158
+
159
+ // Find which function this line belongs to (if any)
160
+ let currentFunction: string | null = null;
161
+ for (const func of functions) {
162
+ if (lineIndex >= func.startLine && lineIndex <= func.endLine) {
163
+ currentFunction = func.name;
164
+ break;
165
+ }
166
+ }
167
+
168
+ // Skip state declarations (state name = value;)
169
+ if (/^\s*state\s+\w+\s*=/.test(line)) {
170
+ continue; // Skip this line, it's a state declaration, not a mutation
171
+ }
172
+
173
+ // Check for state mutations on this line
174
+ for (const stateName of Array.from(declaredStates)) {
175
+ // Pattern 1: stateName++ or stateName-- (postfix)
176
+ // Pattern 2: ++stateName or --stateName (prefix)
177
+ const incrementPattern = new RegExp(`(?:^|[^a-zA-Z0-9_$])(\\+\\+|--)?\\s*${stateName}\\s*(\\+\\+|--)?`, 'g');
178
+ let incMatch;
179
+ while ((incMatch = incrementPattern.exec(line)) !== null) {
180
+ if ((incMatch[1] || incMatch[2]) && incMatch.index !== undefined) { // Found ++ or --
181
+ mutations.push({
182
+ functionName: currentFunction,
183
+ line: lineIndex + 1,
184
+ column: incMatch.index + 1,
185
+ stateName,
186
+ code: line.trim()
187
+ });
188
+ }
189
+ }
190
+
191
+ // Pattern 3: assignment stateName = ... (but not == or ===, and not state declarations)
192
+ const assignPattern = new RegExp(`(?:^|[^a-zA-Z0-9_$])${stateName}\\s*=(?!=)`, 'g');
193
+ let assignMatch;
194
+ while ((assignMatch = assignPattern.exec(line)) !== null) {
195
+ // Double-check it's not a state declaration
196
+ if (assignMatch.index !== undefined && !/^\s*state\s+/.test(line.substring(0, assignMatch.index))) {
197
+ mutations.push({
198
+ functionName: currentFunction,
199
+ line: lineIndex + 1,
200
+ column: assignMatch.index + 1,
201
+ stateName,
202
+ code: line.trim()
203
+ });
204
+ }
205
+ }
206
+
207
+ // Pattern 4: compound assignment stateName += ... etc.
208
+ const compoundPattern = new RegExp(`(?:^|[^a-zA-Z0-9_$])${stateName}\\s*([+\\-*/%]|\\*\\*)=`, 'g');
209
+ let compoundMatch;
210
+ while ((compoundMatch = compoundPattern.exec(line)) !== null) {
211
+ if (compoundMatch.index !== undefined) {
212
+ mutations.push({
213
+ functionName: currentFunction,
214
+ line: lineIndex + 1,
215
+ column: compoundMatch.index + 1,
216
+ stateName,
217
+ code: line.trim()
218
+ });
219
+ }
220
+ }
221
+
222
+ // Pattern 5: state.stateName mutations (for state. prefix syntax)
223
+ const stateDotPattern = new RegExp(`(?:^|[^a-zA-Z0-9_$])state\\.${stateName}\\s*([+\\-*/%]|\\*\\*)?=(?!=)|(?:^|[^a-zA-Z0-9_$])(\\+\\+|--)?\\s*state\\.${stateName}`, 'g');
224
+ let stateDotMatch;
225
+ while ((stateDotMatch = stateDotPattern.exec(line)) !== null) {
226
+ if (stateDotMatch.index !== undefined) {
227
+ mutations.push({
228
+ functionName: currentFunction,
229
+ line: lineIndex + 1,
230
+ column: stateDotMatch.index + 1,
231
+ stateName,
232
+ code: line.trim()
233
+ });
234
+ }
235
+ }
236
+ }
237
+ }
238
+
239
+ return mutations;
240
+ }
241
+
242
+ /**
243
+ * Validate that state mutations only occur inside event handlers
244
+ * Throws a compile-time error if mutations are found outside event handlers
245
+ */
246
+ export function validateStateMutations(
247
+ mutations: MutationLocation[],
248
+ eventHandlers: Set<string>,
249
+ scriptIndex: number
250
+ ): void {
251
+ for (const mutation of mutations) {
252
+ // Top-level mutations (outside any function) are not allowed
253
+ if (mutation.functionName === null) {
254
+ throw new Error(
255
+ `Compiler Error: State mutation is only allowed inside event handlers.\n` +
256
+ ` Found mutation of "${mutation.stateName}" at script ${scriptIndex + 1}, line ${mutation.line}, column ${mutation.column}.\n` +
257
+ ` Code: ${mutation.code}\n` +
258
+ ` State mutations must occur inside functions that are used as event handlers.`
259
+ );
260
+ }
261
+
262
+ // Mutations in functions that are not event handlers are not allowed
263
+ // Check if the function is an event handler (either directly or as an instance-scoped name)
264
+ const isEventHandler = eventHandlers.has(mutation.functionName) ||
265
+ // Check if it's an instance-scoped function name (__zen_comp_0_handleClick -> handleClick)
266
+ /^__zen_comp_\d+_(.+)$/.test(mutation.functionName) &&
267
+ eventHandlers.has(mutation.functionName.replace(/^__zen_comp_\d+_/, ''));
268
+
269
+ if (!isEventHandler) {
270
+ throw new Error(
271
+ `Compiler Error: State mutation is only allowed inside event handlers.\n` +
272
+ ` Found mutation of "${mutation.stateName}" in function "${mutation.functionName}" at script ${scriptIndex + 1}, line ${mutation.line}, column ${mutation.column}.\n` +
273
+ ` Code: ${mutation.code}\n` +
274
+ ` The function "${mutation.functionName}" is not registered as an event handler.\n` +
275
+ ` State mutations must occur inside functions referenced by onclick, oninput, onchange, etc.`
276
+ );
277
+ }
278
+ }
279
+ }
280
+
@@ -0,0 +1,299 @@
1
+ import fs from "fs"
2
+ import * as parse5 from "parse5"
3
+ import type { ZenFile, ScriptBlock, StyleBlock } from "./types"
4
+
5
+ export function parseZen(path: string): ZenFile {
6
+ const source = fs.readFileSync(path, "utf-8");
7
+ const document = parse5.parse(source);
8
+
9
+ const scripts: ScriptBlock[] = [];
10
+ const styles: StyleBlock[] = [];
11
+ let scriptIndex = 0;
12
+ let stylesIndex = 0;
13
+
14
+ function extractTextContent(node: any): string {
15
+ if (!node.childNodes?.length) return '';
16
+ return node.childNodes
17
+ .filter((n: any) => n.nodeName === '#text')
18
+ .map((n: any) => n.value || '')
19
+ .join('');
20
+ }
21
+
22
+ function walk(node: any) {
23
+ if (node.nodeName === "script" && node.childNodes?.length) {
24
+ const content = extractTextContent(node);
25
+ scripts.push({ content, index: scriptIndex++ })
26
+ }
27
+ if (node.nodeName === "style" && node.childNodes?.length) {
28
+ const content = extractTextContent(node);
29
+ styles.push({
30
+ content,
31
+ index: stylesIndex++
32
+ })
33
+ }
34
+
35
+
36
+ node.childNodes?.forEach(walk)
37
+ }
38
+ walk(document)
39
+
40
+ return {
41
+ html: source,
42
+ scripts,
43
+ styles
44
+ }
45
+ }
46
+
47
+ /**
48
+ * State declaration with location information for error reporting
49
+ */
50
+ export interface StateDeclarationInfo {
51
+ name: string;
52
+ value: string;
53
+ line: number;
54
+ column: number;
55
+ scriptIndex: number;
56
+ }
57
+
58
+ /**
59
+ * Extract state declarations from script content with location information
60
+ * Returns an array of StateDeclarationInfo for redeclaration detection
61
+ */
62
+ export function extractStateDeclarationsWithLocation(
63
+ scriptContent: string,
64
+ scriptIndex: number
65
+ ): StateDeclarationInfo[] {
66
+ const declarations: StateDeclarationInfo[] = [];
67
+ const lines = scriptContent.split('\n');
68
+
69
+ // Find "state identifier = ..." pattern and extract the value
70
+ // Handle multi-line expressions by tracking bracket depth
71
+ const statePattern = /state\s+(\w+)\s*=/g;
72
+ let match;
73
+
74
+ while ((match = statePattern.exec(scriptContent)) !== null) {
75
+ const name = match[1];
76
+ if (!name) continue;
77
+
78
+ const startIndex = match.index;
79
+ const valueStartIndex = match.index + match[0].length;
80
+
81
+ // Extract the value by tracking bracket/brace/paren depth
82
+ let value = '';
83
+ let depth = 0;
84
+ let inString = false;
85
+ let stringChar = '';
86
+ let i = valueStartIndex;
87
+
88
+ // Skip whitespace at start
89
+ while (i < scriptContent.length && /\s/.test(scriptContent[i])) {
90
+ i++;
91
+ }
92
+
93
+ const valueStart = i;
94
+
95
+ while (i < scriptContent.length) {
96
+ const char = scriptContent[i];
97
+ const prevChar = i > 0 ? scriptContent[i - 1] : '';
98
+
99
+ // Handle string literals
100
+ if (!inString && (char === '"' || char === "'" || char === '`')) {
101
+ inString = true;
102
+ stringChar = char;
103
+ value += char;
104
+ i++;
105
+ continue;
106
+ }
107
+
108
+ if (inString && char === stringChar && prevChar !== '\\') {
109
+ inString = false;
110
+ value += char;
111
+ i++;
112
+ continue;
113
+ }
114
+
115
+ if (inString) {
116
+ value += char;
117
+ i++;
118
+ continue;
119
+ }
120
+
121
+ // Track bracket depth
122
+ if (char === '(' || char === '[' || char === '{') {
123
+ depth++;
124
+ value += char;
125
+ i++;
126
+ continue;
127
+ }
128
+
129
+ if (char === ')' || char === ']' || char === '}') {
130
+ depth--;
131
+ value += char;
132
+ i++;
133
+ if (depth < 0) {
134
+ // Unmatched closing bracket - should not happen in valid code
135
+ break;
136
+ }
137
+ // If depth is 0 and we're not in a nested structure, check if we should stop
138
+ if (depth === 0) {
139
+ // Check if next non-whitespace is semicolon or newline (end of statement)
140
+ let nextIdx = i;
141
+ while (nextIdx < scriptContent.length && /\s/.test(scriptContent[nextIdx])) {
142
+ nextIdx++;
143
+ }
144
+ if (nextIdx >= scriptContent.length || scriptContent[nextIdx] === ';' || scriptContent[nextIdx] === '\n') {
145
+ break;
146
+ }
147
+ }
148
+ continue;
149
+ }
150
+
151
+ // If depth is 0 and we hit a semicolon or newline, we're done
152
+ if (depth === 0 && (char === ';' || char === '\n')) {
153
+ break;
154
+ }
155
+
156
+ value += char;
157
+ i++;
158
+ }
159
+
160
+ const trimmedValue = value.trim();
161
+ if (!trimmedValue) continue;
162
+
163
+ // Calculate line and column from start index
164
+ let line = 1;
165
+ let column = 1;
166
+ let currentIndex = 0;
167
+
168
+ for (let j = 0; j < lines.length; j++) {
169
+ const currentLine = lines[j];
170
+ if (!currentLine) continue;
171
+ const lineLength = currentLine.length + 1; // +1 for newline
172
+ if (currentIndex + lineLength > startIndex) {
173
+ line = j + 1;
174
+ column = startIndex - currentIndex + 1;
175
+ break;
176
+ }
177
+ currentIndex += lineLength;
178
+ }
179
+
180
+ declarations.push({
181
+ name,
182
+ value: trimmedValue,
183
+ line,
184
+ column,
185
+ scriptIndex
186
+ });
187
+ }
188
+
189
+ return declarations;
190
+ }
191
+
192
+ /**
193
+ * Extract state declarations from script content
194
+ * Returns a Map of state name -> initial value expression
195
+ * @deprecated Use extractStateDeclarationsWithLocation for redeclaration detection
196
+ */
197
+ export function extractStateDeclarations(scriptContent: string): Map<string, string> {
198
+ const states = new Map<string, string>();
199
+ const declarations = extractStateDeclarationsWithLocation(scriptContent, 0);
200
+ for (const decl of declarations) {
201
+ states.set(decl.name, decl.value);
202
+ }
203
+ return states;
204
+ }
205
+
206
+ /**
207
+ * Transform script content to remove state declarations (they'll be handled by runtime)
208
+ */
209
+ export function transformStateDeclarations(scriptContent: string): string {
210
+ // Remove state declarations by finding them and removing the entire declaration
211
+ // Use the same logic as extractStateDeclarationsWithLocation to find declarations
212
+ const statePattern = /state\s+(\w+)\s*=/g;
213
+ const matches: Array<{ start: number; end: number }> = [];
214
+ let match;
215
+
216
+ while ((match = statePattern.exec(scriptContent)) !== null) {
217
+ const name = match[1];
218
+ if (!name) continue;
219
+
220
+ const valueStartIndex = match.index + match[0].length;
221
+
222
+ // Extract the value by tracking bracket/brace/paren depth (same as extraction)
223
+ let depth = 0;
224
+ let inString = false;
225
+ let stringChar = '';
226
+ let i = valueStartIndex;
227
+
228
+ // Skip whitespace at start
229
+ while (i < scriptContent.length && /\s/.test(scriptContent[i])) {
230
+ i++;
231
+ }
232
+
233
+ while (i < scriptContent.length) {
234
+ const char = scriptContent[i];
235
+ const prevChar = i > 0 ? scriptContent[i - 1] : '';
236
+
237
+ // Handle string literals
238
+ if (!inString && (char === '"' || char === "'" || char === '`')) {
239
+ inString = true;
240
+ stringChar = char;
241
+ i++;
242
+ continue;
243
+ }
244
+
245
+ if (inString && char === stringChar && prevChar !== '\\') {
246
+ inString = false;
247
+ i++;
248
+ continue;
249
+ }
250
+
251
+ if (inString) {
252
+ i++;
253
+ continue;
254
+ }
255
+
256
+ // Track bracket depth
257
+ if (char === '(' || char === '[' || char === '{') {
258
+ depth++;
259
+ i++;
260
+ continue;
261
+ }
262
+
263
+ if (char === ')' || char === ']' || char === '}') {
264
+ depth--;
265
+ i++;
266
+ if (depth < 0) break;
267
+ if (depth === 0) {
268
+ let nextIdx = i;
269
+ while (nextIdx < scriptContent.length && /\s/.test(scriptContent[nextIdx])) {
270
+ nextIdx++;
271
+ }
272
+ if (nextIdx >= scriptContent.length || scriptContent[nextIdx] === ';' || scriptContent[nextIdx] === '\n') {
273
+ if (scriptContent[nextIdx] === ';') nextIdx++;
274
+ matches.push({ start: match.index, end: nextIdx });
275
+ break;
276
+ }
277
+ }
278
+ continue;
279
+ }
280
+
281
+ if (depth === 0 && (char === ';' || char === '\n')) {
282
+ if (char === ';') i++;
283
+ matches.push({ start: match.index, end: i });
284
+ break;
285
+ }
286
+
287
+ i++;
288
+ }
289
+ }
290
+
291
+ // Remove matches in reverse order to preserve indices
292
+ let result = scriptContent;
293
+ for (let i = matches.length - 1; i >= 0; i--) {
294
+ const m = matches[i];
295
+ result = result.slice(0, m.start) + result.slice(m.end);
296
+ }
297
+
298
+ return result;
299
+ }