@zenithbuild/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintignore +15 -0
- package/.gitattributes +2 -0
- package/.github/ISSUE_TEMPLATE/compiler-errors-for-invalid-state-declarations.md +25 -0
- package/.github/ISSUE_TEMPLATE/new_ticket.yaml +34 -0
- package/.github/pull_request_template.md +15 -0
- package/.github/workflows/discord-changelog.yml +141 -0
- package/.github/workflows/discord-notify.yml +242 -0
- package/.github/workflows/discord-version.yml +195 -0
- package/.prettierignore +13 -0
- package/.prettierrc +21 -0
- package/.zen.d.ts +15 -0
- package/LICENSE +21 -0
- package/README.md +55 -0
- package/app/components/Button.zen +46 -0
- package/app/components/Link.zen +11 -0
- package/app/favicon.ico +0 -0
- package/app/layouts/Main.zen +59 -0
- package/app/pages/about.zen +23 -0
- package/app/pages/blog/[id].zen +53 -0
- package/app/pages/blog/index.zen +32 -0
- package/app/pages/dynamic-dx.zen +712 -0
- package/app/pages/dynamic-primitives.zen +453 -0
- package/app/pages/index.zen +154 -0
- package/app/pages/navigation-demo.zen +229 -0
- package/app/pages/posts/[...slug].zen +61 -0
- package/app/pages/primitives-demo.zen +273 -0
- package/assets/logos/0E3B5DDD-605C-4839-BB2E-DFCA8ADC9604.PNG +0 -0
- package/assets/logos/760971E5-79A1-44F9-90B9-925DF30F4278.PNG +0 -0
- package/assets/logos/8A06ED80-9ED2-4689-BCBD-13B2E95EE8E4.JPG +0 -0
- package/assets/logos/C691FF58-ED13-4E8D-B6A3-02E835849340.PNG +0 -0
- package/assets/logos/C691FF58-ED13-4E8D-B6A3-02E835849340.svg +601 -0
- package/assets/logos/README.md +54 -0
- package/assets/logos/zen.icns +0 -0
- package/bun.lock +39 -0
- package/compiler/README.md +380 -0
- package/compiler/errors/compilerError.ts +24 -0
- package/compiler/finalize/finalizeOutput.ts +163 -0
- package/compiler/finalize/generateFinalBundle.ts +82 -0
- package/compiler/index.ts +44 -0
- package/compiler/ir/types.ts +83 -0
- package/compiler/legacy/binding.ts +254 -0
- package/compiler/legacy/bindings.ts +338 -0
- package/compiler/legacy/component-process.ts +1208 -0
- package/compiler/legacy/component.ts +301 -0
- package/compiler/legacy/event.ts +50 -0
- package/compiler/legacy/expression.ts +1149 -0
- package/compiler/legacy/mutation.ts +280 -0
- package/compiler/legacy/parse.ts +299 -0
- package/compiler/legacy/split.ts +608 -0
- package/compiler/legacy/types.ts +32 -0
- package/compiler/output/types.ts +34 -0
- package/compiler/parse/detectMapExpressions.ts +102 -0
- package/compiler/parse/parseScript.ts +22 -0
- package/compiler/parse/parseTemplate.ts +425 -0
- package/compiler/parse/parseZenFile.ts +66 -0
- package/compiler/parse/trackLoopContext.ts +82 -0
- package/compiler/runtime/dataExposure.ts +291 -0
- package/compiler/runtime/generateDOM.ts +144 -0
- package/compiler/runtime/generateHydrationBundle.ts +383 -0
- package/compiler/runtime/hydration.ts +309 -0
- package/compiler/runtime/navigation.ts +432 -0
- package/compiler/runtime/thinRuntime.ts +160 -0
- package/compiler/runtime/transformIR.ts +256 -0
- package/compiler/runtime/wrapExpression.ts +84 -0
- package/compiler/runtime/wrapExpressionWithLoop.ts +77 -0
- package/compiler/spa-build.ts +1000 -0
- package/compiler/test/validate-test.ts +104 -0
- package/compiler/transform/generateBindings.ts +47 -0
- package/compiler/transform/generateHTML.ts +28 -0
- package/compiler/transform/transformNode.ts +126 -0
- package/compiler/transform/transformTemplate.ts +38 -0
- package/compiler/validate/validateExpressions.ts +168 -0
- package/core/index.ts +135 -0
- package/core/lifecycle/index.ts +49 -0
- package/core/lifecycle/zen-mount.ts +182 -0
- package/core/lifecycle/zen-unmount.ts +88 -0
- package/core/reactivity/index.ts +54 -0
- package/core/reactivity/tracking.ts +167 -0
- package/core/reactivity/zen-batch.ts +57 -0
- package/core/reactivity/zen-effect.ts +139 -0
- package/core/reactivity/zen-memo.ts +146 -0
- package/core/reactivity/zen-ref.ts +52 -0
- package/core/reactivity/zen-signal.ts +121 -0
- package/core/reactivity/zen-state.ts +180 -0
- package/core/reactivity/zen-untrack.ts +44 -0
- package/docs/COMMENTS.md +111 -0
- package/docs/COMMITS.md +36 -0
- package/docs/CONTRIBUTING.md +116 -0
- package/docs/STYLEGUIDE.md +62 -0
- package/package.json +44 -0
- package/router/index.ts +76 -0
- package/router/manifest.ts +314 -0
- package/router/navigation/ZenLink.zen +231 -0
- package/router/navigation/index.ts +78 -0
- package/router/navigation/zen-link.ts +584 -0
- package/router/runtime.ts +458 -0
- package/router/types.ts +168 -0
- package/runtime/build.ts +17 -0
- package/runtime/serve.ts +93 -0
- package/scripts/webhook-proxy.ts +213 -0
- package/tsconfig.json +28 -0
|
@@ -0,0 +1,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
|
+
}
|