@aihu/language-server 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/LICENSE +21 -0
- package/README.md +123 -0
- package/dist/bin.js +2119 -0
- package/dist/core/index.d.ts +101 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +2 -0
- package/dist/core-BHishSOT.js +1911 -0
- package/dist/core-BHishSOT.js.map +1 -0
- package/dist/server.d.ts +22 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +201 -0
- package/dist/server.js.map +1 -0
- package/package.json +52 -0
package/dist/bin.js
ADDED
|
@@ -0,0 +1,2119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { CodeActionKind, CompletionList, DiagnosticSeverity, MarkupKind, ProposedFeatures, TextDocumentSyncKind, TextDocuments, TextEdit, createConnection } from "vscode-languageserver/node.js";
|
|
3
|
+
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
4
|
+
import { execFile } from "node:child_process";
|
|
5
|
+
import { dirname, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
//#region ../compiler/js/codemods/macro-simplification/migrate.ts
|
|
9
|
+
const INDENT = " ";
|
|
10
|
+
function migrate(source) {
|
|
11
|
+
const warnings = [];
|
|
12
|
+
const stateBlock = findBlock(source, "state");
|
|
13
|
+
const agentBlock = findBlock(source, "agent");
|
|
14
|
+
const sidecar = /* @__PURE__ */ new Map();
|
|
15
|
+
const preservedAgentLines = [];
|
|
16
|
+
if (agentBlock) parseAgent(agentBlock.body, sidecar, preservedAgentLines, warnings);
|
|
17
|
+
if (!stateBlock) return {
|
|
18
|
+
rewritten: source,
|
|
19
|
+
warnings
|
|
20
|
+
};
|
|
21
|
+
const buckets = walkState(stateBlock.body, sidecar, warnings);
|
|
22
|
+
const newStateBlock = `@state {\n${emitStateBody(buckets)}\n}`;
|
|
23
|
+
const newAgentBlock = preservedAgentLines.length > 0 ? `@agent {\n${preservedAgentLines.join("\n")}\n}` : null;
|
|
24
|
+
let out = source.slice(0, stateBlock.start) + newStateBlock + source.slice(stateBlock.end);
|
|
25
|
+
if (agentBlock) {
|
|
26
|
+
const agentRelocated = findBlock(out, "agent");
|
|
27
|
+
if (agentRelocated) if (newAgentBlock) out = out.slice(0, agentRelocated.start) + newAgentBlock + out.slice(agentRelocated.end);
|
|
28
|
+
else {
|
|
29
|
+
const dropStart = expandLeftToLineStart(out, agentRelocated.start);
|
|
30
|
+
const dropEnd = expandRightThroughBlankLines(out, agentRelocated.end);
|
|
31
|
+
out = out.slice(0, dropStart) + out.slice(dropEnd);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const stateNames = new Set([
|
|
35
|
+
...buckets.props.map((p) => p.name),
|
|
36
|
+
...buckets.computed.map((c) => c.name),
|
|
37
|
+
...buckets.action.map((a) => a.name),
|
|
38
|
+
...buckets.resource.map((r) => r.name),
|
|
39
|
+
...buckets.effectNamed.map((e) => e.name),
|
|
40
|
+
...buckets.plain.map((p) => p.name).filter((x) => !!x)
|
|
41
|
+
]);
|
|
42
|
+
for (const [name] of sidecar) if (!stateNames.has(name)) warnings.push(`@agent references '${name}' but no @state declaration found`);
|
|
43
|
+
return {
|
|
44
|
+
rewritten: out,
|
|
45
|
+
warnings
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function findBlock(source, name) {
|
|
49
|
+
const m = new RegExp(`(^|\\n)([ \\t]*)@${name}\\s*\\{`, "g").exec(source);
|
|
50
|
+
if (!m) return null;
|
|
51
|
+
const startMatch = m.index + (m[1] === "\n" ? 1 : 0);
|
|
52
|
+
const openIdx = m.index + m[0].length - 1;
|
|
53
|
+
const closeIdx = matchBrace(source, openIdx);
|
|
54
|
+
if (closeIdx < 0) return null;
|
|
55
|
+
return {
|
|
56
|
+
start: startMatch,
|
|
57
|
+
end: closeIdx + 1,
|
|
58
|
+
bodyStart: openIdx + 1,
|
|
59
|
+
bodyEnd: closeIdx,
|
|
60
|
+
body: source.slice(openIdx + 1, closeIdx)
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function matchBrace(s, i) {
|
|
64
|
+
if (s[i] !== "{") return -1;
|
|
65
|
+
let depth = 0;
|
|
66
|
+
let j = i;
|
|
67
|
+
while (j < s.length) {
|
|
68
|
+
const c = s[j];
|
|
69
|
+
if (c === "/" && s[j + 1] === "/") {
|
|
70
|
+
const nl = s.indexOf("\n", j);
|
|
71
|
+
j = nl < 0 ? s.length : nl;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (c === "/" && s[j + 1] === "*") {
|
|
75
|
+
const end = s.indexOf("*/", j + 2);
|
|
76
|
+
j = end < 0 ? s.length : end + 2;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (c === "\"" || c === "'" || c === "`") {
|
|
80
|
+
j = skipString(s, j);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (c === "{") depth++;
|
|
84
|
+
else if (c === "}") {
|
|
85
|
+
depth--;
|
|
86
|
+
if (depth === 0) return j;
|
|
87
|
+
}
|
|
88
|
+
j++;
|
|
89
|
+
}
|
|
90
|
+
return -1;
|
|
91
|
+
}
|
|
92
|
+
function skipString(s, i) {
|
|
93
|
+
const quote = s[i];
|
|
94
|
+
let j = i + 1;
|
|
95
|
+
while (j < s.length) {
|
|
96
|
+
const c = s[j];
|
|
97
|
+
if (c === "\\") {
|
|
98
|
+
j += 2;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (quote === "`" && c === "$" && s[j + 1] === "{") {
|
|
102
|
+
const close = matchBrace(s, j + 1);
|
|
103
|
+
j = close < 0 ? s.length : close + 1;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (c === quote) return j + 1;
|
|
107
|
+
j++;
|
|
108
|
+
}
|
|
109
|
+
return s.length;
|
|
110
|
+
}
|
|
111
|
+
function matchParen(s, i) {
|
|
112
|
+
if (s[i] !== "(") return -1;
|
|
113
|
+
let depth = 0;
|
|
114
|
+
let j = i;
|
|
115
|
+
while (j < s.length) {
|
|
116
|
+
const c = s[j];
|
|
117
|
+
if (c === "/" && s[j + 1] === "/") {
|
|
118
|
+
const nl = s.indexOf("\n", j);
|
|
119
|
+
j = nl < 0 ? s.length : nl;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (c === "/" && s[j + 1] === "*") {
|
|
123
|
+
const end = s.indexOf("*/", j + 2);
|
|
124
|
+
j = end < 0 ? s.length : end + 2;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (c === "\"" || c === "'" || c === "`") {
|
|
128
|
+
j = skipString(s, j);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (c === "{") {
|
|
132
|
+
const close = matchBrace(s, j);
|
|
133
|
+
j = close < 0 ? s.length : close + 1;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (c === "(") depth++;
|
|
137
|
+
else if (c === ")") {
|
|
138
|
+
depth--;
|
|
139
|
+
if (depth === 0) return j;
|
|
140
|
+
}
|
|
141
|
+
j++;
|
|
142
|
+
}
|
|
143
|
+
return -1;
|
|
144
|
+
}
|
|
145
|
+
function parseAgent(body, sidecar, preservedAgentLines, _warnings) {
|
|
146
|
+
const upsert = (name, patch) => {
|
|
147
|
+
sidecar.set(name, {
|
|
148
|
+
...sidecar.get(name) ?? {},
|
|
149
|
+
...patch
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
for (const rawLine of body.split("\n")) {
|
|
153
|
+
const line = rawLine.trim();
|
|
154
|
+
if (line === "" || line.startsWith("//") || line.startsWith("/*") || line.startsWith("*")) continue;
|
|
155
|
+
if (line.startsWith("$expose.write ")) {
|
|
156
|
+
const names = parseNameList(line.slice(14));
|
|
157
|
+
for (const n of names) upsert(n, {
|
|
158
|
+
exposeRead: true,
|
|
159
|
+
exposeWrite: true
|
|
160
|
+
});
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (line.startsWith("$expose ")) {
|
|
164
|
+
const names = parseNameList(line.slice(8));
|
|
165
|
+
for (const n of names) upsert(n, { exposeRead: true });
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (line.startsWith("$action ")) {
|
|
169
|
+
const rest = line.slice(8).trim();
|
|
170
|
+
const m = /^([A-Za-z_$][\w$]*)/.exec(rest);
|
|
171
|
+
if (m) upsert(m[1], {
|
|
172
|
+
isAction: true,
|
|
173
|
+
exposeRead: true,
|
|
174
|
+
exposeWrite: true
|
|
175
|
+
});
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (line.startsWith("$describe ")) {
|
|
179
|
+
const rest = line.slice(10).trim();
|
|
180
|
+
const nameMatch = /^([A-Za-z_$][\w$]*)\s+(["'])([\s\S]*)\2\s*$/.exec(rest);
|
|
181
|
+
if (nameMatch) upsert(nameMatch[1], { describe: nameMatch[3] });
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (line.startsWith("$scope ")) {
|
|
185
|
+
preservedAgentLines.push(` ${line}`);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (line.startsWith("$rate-limit ")) preservedAgentLines.push(` ${line}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function parseNameList(s) {
|
|
192
|
+
return s.split(",").map((x) => x.trim()).filter((x) => /^[A-Za-z_$][\w$]*$/.test(x));
|
|
193
|
+
}
|
|
194
|
+
function walkState(body, sidecar, warnings) {
|
|
195
|
+
const buckets = {
|
|
196
|
+
props: [],
|
|
197
|
+
computed: [],
|
|
198
|
+
action: [],
|
|
199
|
+
resource: [],
|
|
200
|
+
effectNamed: [],
|
|
201
|
+
effectAnon: [],
|
|
202
|
+
lifecycle: [],
|
|
203
|
+
plain: [],
|
|
204
|
+
passthrough: []
|
|
205
|
+
};
|
|
206
|
+
let i = 0;
|
|
207
|
+
while (i < body.length && (body[i] === "\n" || body[i] === "\r")) i++;
|
|
208
|
+
let leadingBuf = "";
|
|
209
|
+
let position = 0;
|
|
210
|
+
const consumeWS = () => {
|
|
211
|
+
while (i < body.length) {
|
|
212
|
+
const lineEnd = body.indexOf("\n", i);
|
|
213
|
+
const end = lineEnd < 0 ? body.length : lineEnd;
|
|
214
|
+
const line = body.slice(i, end);
|
|
215
|
+
const trimmed = line.trim();
|
|
216
|
+
if (trimmed === "") {
|
|
217
|
+
leadingBuf += `${line}\n`;
|
|
218
|
+
i = end + 1;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (trimmed.startsWith("//")) {
|
|
222
|
+
leadingBuf += `${line}\n`;
|
|
223
|
+
i = end + 1;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (trimmed.startsWith("/*")) {
|
|
227
|
+
const close = body.indexOf("*/", i);
|
|
228
|
+
if (close < 0) {
|
|
229
|
+
leadingBuf += body.slice(i);
|
|
230
|
+
i = body.length;
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const commentEnd = body.indexOf("\n", close);
|
|
234
|
+
const realEnd = commentEnd < 0 ? body.length : commentEnd + 1;
|
|
235
|
+
leadingBuf += body.slice(i, realEnd);
|
|
236
|
+
i = realEnd;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
while (i < body.length) {
|
|
243
|
+
consumeWS();
|
|
244
|
+
if (i >= body.length) break;
|
|
245
|
+
const lineStart = i;
|
|
246
|
+
const lineEnd = body.indexOf("\n", i);
|
|
247
|
+
const lineEndIdx = lineEnd < 0 ? body.length : lineEnd;
|
|
248
|
+
const lineText = body.slice(lineStart, lineEndIdx);
|
|
249
|
+
const trimmed = lineText.trim();
|
|
250
|
+
const leading = leadingBuf;
|
|
251
|
+
leadingBuf = "";
|
|
252
|
+
const trimStart = lineStart + (lineText.length - lineText.trimStart().length);
|
|
253
|
+
if (trimmed.startsWith("import ")) {
|
|
254
|
+
buckets.passthrough.push({
|
|
255
|
+
raw: lineText.trim(),
|
|
256
|
+
leading,
|
|
257
|
+
position: position++
|
|
258
|
+
});
|
|
259
|
+
i = lineEndIdx + 1;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
{
|
|
263
|
+
const v2 = tryParseV2Collection(body, trimStart, sidecar, buckets, leading, () => position++);
|
|
264
|
+
if (v2 !== null) {
|
|
265
|
+
i = v2;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (/^\$effect\s*:\s*(?:\(|async\b)/.test(trimmed)) {
|
|
270
|
+
const handled = tryParseV2AnonEffect(body, trimStart, buckets, leading, position++);
|
|
271
|
+
if (handled !== null) {
|
|
272
|
+
i = handled;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (trimmed.startsWith("$prop ")) {
|
|
277
|
+
const handled = handleProp(body, trimStart, sidecar, buckets, leading);
|
|
278
|
+
if (handled !== null) {
|
|
279
|
+
i = handled;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
buckets.passthrough.push({
|
|
283
|
+
raw: trimmed,
|
|
284
|
+
leading,
|
|
285
|
+
position: position++
|
|
286
|
+
});
|
|
287
|
+
i = lineEndIdx + 1;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (trimmed.startsWith("$computed ")) {
|
|
291
|
+
const handled = handleComputed(body, trimStart, sidecar, buckets, leading);
|
|
292
|
+
if (handled !== null) {
|
|
293
|
+
i = handled;
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
buckets.passthrough.push({
|
|
297
|
+
raw: trimmed,
|
|
298
|
+
leading,
|
|
299
|
+
position: position++
|
|
300
|
+
});
|
|
301
|
+
i = lineEndIdx + 1;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (trimmed.startsWith("$resource ")) {
|
|
305
|
+
const handled = handleResource(body, trimStart, sidecar, buckets, leading);
|
|
306
|
+
if (handled !== null) {
|
|
307
|
+
i = handled;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
buckets.passthrough.push({
|
|
311
|
+
raw: trimmed,
|
|
312
|
+
leading,
|
|
313
|
+
position: position++
|
|
314
|
+
});
|
|
315
|
+
i = lineEndIdx + 1;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (trimmed.startsWith("$action ")) {
|
|
319
|
+
const handled = handleAction(body, trimStart, sidecar, buckets, leading);
|
|
320
|
+
if (handled !== null) {
|
|
321
|
+
i = handled;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
buckets.passthrough.push({
|
|
325
|
+
raw: trimmed,
|
|
326
|
+
leading,
|
|
327
|
+
position: position++
|
|
328
|
+
});
|
|
329
|
+
i = lineEndIdx + 1;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if (trimmed.startsWith("$lifecycle.mount(") || trimmed.startsWith("$lifecycle.dispose(")) {
|
|
333
|
+
const handled = handleLifecycle(body, trimStart, buckets, leading, position++);
|
|
334
|
+
if (handled !== null) {
|
|
335
|
+
i = handled;
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
buckets.passthrough.push({
|
|
339
|
+
raw: trimmed,
|
|
340
|
+
leading,
|
|
341
|
+
position: position++
|
|
342
|
+
});
|
|
343
|
+
i = lineEndIdx + 1;
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (trimmed.startsWith("$effect(")) {
|
|
347
|
+
const handled = handleEffectAnon(body, trimStart, buckets, leading, position++, warnings);
|
|
348
|
+
if (handled !== null) {
|
|
349
|
+
i = handled;
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
buckets.passthrough.push({
|
|
353
|
+
raw: trimmed,
|
|
354
|
+
leading,
|
|
355
|
+
position: position++
|
|
356
|
+
});
|
|
357
|
+
i = lineEndIdx + 1;
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (trimmed.startsWith("$effect.on(")) {
|
|
361
|
+
const handled = consumePassthroughCall(body, trimStart);
|
|
362
|
+
buckets.passthrough.push({
|
|
363
|
+
raw: body.slice(trimStart, handled).trim(),
|
|
364
|
+
leading,
|
|
365
|
+
position: position++
|
|
366
|
+
});
|
|
367
|
+
i = handled;
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (/^\$[A-Za-z_][\w.$]*\s*\(/.test(trimmed)) {
|
|
371
|
+
const handled = consumePassthroughCall(body, trimStart);
|
|
372
|
+
buckets.passthrough.push({
|
|
373
|
+
raw: body.slice(trimStart, handled).trimEnd(),
|
|
374
|
+
leading,
|
|
375
|
+
position: position++
|
|
376
|
+
});
|
|
377
|
+
i = handled;
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (/^\$[A-Za-z_][\w.$-]*\s+[A-Za-z_$]/.test(trimmed)) {
|
|
381
|
+
buckets.passthrough.push({
|
|
382
|
+
raw: trimmed,
|
|
383
|
+
leading,
|
|
384
|
+
position: position++
|
|
385
|
+
});
|
|
386
|
+
i = lineEndIdx + 1;
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const plain = tryParsePlainDecl(body, trimStart);
|
|
390
|
+
if (plain !== null) {
|
|
391
|
+
const sc = plain.name ? sidecar.get(plain.name) : void 0;
|
|
392
|
+
if (sc) buckets.props.push({
|
|
393
|
+
name: plain.name,
|
|
394
|
+
rawType: plain.rawType,
|
|
395
|
+
rawDefault: plain.rawDefault,
|
|
396
|
+
describe: sc.describe,
|
|
397
|
+
expose: sidecarToExpose(sc),
|
|
398
|
+
leading
|
|
399
|
+
});
|
|
400
|
+
else buckets.plain.push({
|
|
401
|
+
raw: plain.raw,
|
|
402
|
+
leading,
|
|
403
|
+
name: plain.name,
|
|
404
|
+
rawType: plain.rawType,
|
|
405
|
+
rawDefault: plain.rawDefault
|
|
406
|
+
});
|
|
407
|
+
i = plain.endIdx;
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
buckets.passthrough.push({
|
|
411
|
+
raw: trimmed,
|
|
412
|
+
leading,
|
|
413
|
+
position: position++
|
|
414
|
+
});
|
|
415
|
+
i = lineEndIdx + 1;
|
|
416
|
+
}
|
|
417
|
+
return buckets;
|
|
418
|
+
}
|
|
419
|
+
function sidecarToExpose(sc) {
|
|
420
|
+
if (!sc.exposeRead && !sc.exposeWrite && !sc.isAction) return void 0;
|
|
421
|
+
return {
|
|
422
|
+
read: !!sc.exposeRead,
|
|
423
|
+
write: !!sc.exposeWrite
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
function handleProp(body, start, sidecar, buckets, leading) {
|
|
427
|
+
const after = start + 6;
|
|
428
|
+
const nameMatch = /^\s*([A-Za-z_$][\w$]*)\s*:/.exec(body.slice(after));
|
|
429
|
+
if (!nameMatch) return null;
|
|
430
|
+
const name = nameMatch[1];
|
|
431
|
+
let cursor = after + nameMatch[0].length;
|
|
432
|
+
const typeStart = cursor;
|
|
433
|
+
const typeEnd = scanUntilTopLevel(body, cursor, [
|
|
434
|
+
"=",
|
|
435
|
+
"\n",
|
|
436
|
+
";"
|
|
437
|
+
]);
|
|
438
|
+
const rawType = body.slice(typeStart, typeEnd).trim();
|
|
439
|
+
cursor = typeEnd;
|
|
440
|
+
let rawDefault;
|
|
441
|
+
if (body[cursor] === "=") {
|
|
442
|
+
cursor++;
|
|
443
|
+
const defStart = cursor;
|
|
444
|
+
const defEnd = scanUntilTopLevel(body, cursor, ["\n", ";"]);
|
|
445
|
+
rawDefault = body.slice(defStart, defEnd).trim();
|
|
446
|
+
cursor = defEnd;
|
|
447
|
+
}
|
|
448
|
+
if (body[cursor] === ";" || body[cursor] === "\n") cursor++;
|
|
449
|
+
const sc = sidecar.get(name);
|
|
450
|
+
buckets.props.push({
|
|
451
|
+
name,
|
|
452
|
+
rawType: flattenWhitespace(rawType),
|
|
453
|
+
rawDefault: rawDefault !== void 0 ? flattenWhitespace(rawDefault) : void 0,
|
|
454
|
+
describe: sc?.describe,
|
|
455
|
+
expose: sc ? sidecarToExpose(sc) : void 0,
|
|
456
|
+
leading
|
|
457
|
+
});
|
|
458
|
+
return cursor;
|
|
459
|
+
}
|
|
460
|
+
function handleComputed(body, start, sidecar, buckets, leading) {
|
|
461
|
+
const after = start + 10;
|
|
462
|
+
const nameMatch = /^\s*([A-Za-z_$][\w$]*)\s*=/.exec(body.slice(after));
|
|
463
|
+
if (!nameMatch) return null;
|
|
464
|
+
const name = nameMatch[1];
|
|
465
|
+
let cursor = after + nameMatch[0].length;
|
|
466
|
+
const exprStart = cursor;
|
|
467
|
+
const exprEnd = scanComputedOrResourceExpr(body, cursor);
|
|
468
|
+
const expr = body.slice(exprStart, exprEnd).trim();
|
|
469
|
+
cursor = exprEnd;
|
|
470
|
+
const sc = sidecar.get(name);
|
|
471
|
+
buckets.computed.push({
|
|
472
|
+
name,
|
|
473
|
+
expr,
|
|
474
|
+
describe: sc?.describe,
|
|
475
|
+
expose: sc ? sidecarToExpose(sc) : void 0,
|
|
476
|
+
leading
|
|
477
|
+
});
|
|
478
|
+
return cursor;
|
|
479
|
+
}
|
|
480
|
+
function handleResource(body, start, sidecar, buckets, leading) {
|
|
481
|
+
const after = start + 10;
|
|
482
|
+
const nameMatch = /^\s*([A-Za-z_$][\w$]*)\s*=/.exec(body.slice(after));
|
|
483
|
+
if (!nameMatch) return null;
|
|
484
|
+
const name = nameMatch[1];
|
|
485
|
+
let cursor = after + nameMatch[0].length;
|
|
486
|
+
const exprStart = cursor;
|
|
487
|
+
const exprEnd = scanComputedOrResourceExpr(body, cursor);
|
|
488
|
+
const expr = body.slice(exprStart, exprEnd).trim();
|
|
489
|
+
cursor = exprEnd;
|
|
490
|
+
const sc = sidecar.get(name);
|
|
491
|
+
buckets.resource.push({
|
|
492
|
+
name,
|
|
493
|
+
expr,
|
|
494
|
+
describe: sc?.describe,
|
|
495
|
+
expose: sc ? sidecarToExpose(sc) : void 0,
|
|
496
|
+
leading
|
|
497
|
+
});
|
|
498
|
+
return cursor;
|
|
499
|
+
}
|
|
500
|
+
function handleAction(body, start, sidecar, buckets, leading) {
|
|
501
|
+
const after = start + 8;
|
|
502
|
+
const nameMatch = /^\s*([A-Za-z_$][\w$]*)\s*\(/.exec(body.slice(after));
|
|
503
|
+
if (!nameMatch) return null;
|
|
504
|
+
const name = nameMatch[1];
|
|
505
|
+
const parenIdx = after + nameMatch[0].length - 1;
|
|
506
|
+
const parenClose = matchParen(body, parenIdx);
|
|
507
|
+
if (parenClose < 0) return null;
|
|
508
|
+
const args = body.slice(parenIdx + 1, parenClose).trim();
|
|
509
|
+
let cursor = parenClose + 1;
|
|
510
|
+
let retType;
|
|
511
|
+
const retStart = cursor;
|
|
512
|
+
while (cursor < body.length && body[cursor] !== "{") {
|
|
513
|
+
if (body[cursor] === "\n") break;
|
|
514
|
+
cursor++;
|
|
515
|
+
}
|
|
516
|
+
const between = body.slice(retStart, cursor).trim();
|
|
517
|
+
if (between.startsWith(":")) retType = between.slice(1).trim();
|
|
518
|
+
if (body[cursor] !== "{") return null;
|
|
519
|
+
const braceClose = matchBrace(body, cursor);
|
|
520
|
+
if (braceClose < 0) return null;
|
|
521
|
+
const bodySrc = body.slice(cursor + 1, braceClose);
|
|
522
|
+
cursor = braceClose + 1;
|
|
523
|
+
const sc = sidecar.get(name);
|
|
524
|
+
buckets.action.push({
|
|
525
|
+
name,
|
|
526
|
+
args,
|
|
527
|
+
retType,
|
|
528
|
+
body: bodySrc,
|
|
529
|
+
describe: sc?.describe,
|
|
530
|
+
expose: sc ? sidecarToExpose(sc) : void 0,
|
|
531
|
+
leading
|
|
532
|
+
});
|
|
533
|
+
return cursor;
|
|
534
|
+
}
|
|
535
|
+
function handleLifecycle(body, start, buckets, leading, position) {
|
|
536
|
+
const kind = body.slice(start).trim().startsWith("$lifecycle.mount(") ? "mount" : "dispose";
|
|
537
|
+
const parenIdx = body.indexOf("(", start);
|
|
538
|
+
if (parenIdx < 0) return null;
|
|
539
|
+
const parenClose = matchParen(body, parenIdx);
|
|
540
|
+
if (parenClose < 0) return null;
|
|
541
|
+
const cb = body.slice(parenIdx + 1, parenClose).trim();
|
|
542
|
+
const fn = parseArrowOrFunctionExpr(cb);
|
|
543
|
+
buckets.lifecycle.push({
|
|
544
|
+
kind,
|
|
545
|
+
body: fn ? fn.body : cb,
|
|
546
|
+
leading,
|
|
547
|
+
position
|
|
548
|
+
});
|
|
549
|
+
let cursor = parenClose + 1;
|
|
550
|
+
if (body[cursor] === ";" || body[cursor] === "\n") cursor++;
|
|
551
|
+
return cursor;
|
|
552
|
+
}
|
|
553
|
+
function handleEffectAnon(body, start, buckets, leading, position, _warnings) {
|
|
554
|
+
const parenIdx = body.indexOf("(", start);
|
|
555
|
+
if (parenIdx < 0) return null;
|
|
556
|
+
const parenClose = matchParen(body, parenIdx);
|
|
557
|
+
if (parenClose < 0) return null;
|
|
558
|
+
const cb = body.slice(parenIdx + 1, parenClose).trim();
|
|
559
|
+
const fn = parseArrowOrFunctionExpr(cb);
|
|
560
|
+
buckets.effectAnon.push({
|
|
561
|
+
body: fn ? fn.body : cb,
|
|
562
|
+
leading,
|
|
563
|
+
position
|
|
564
|
+
});
|
|
565
|
+
let cursor = parenClose + 1;
|
|
566
|
+
if (body[cursor] === ";" || body[cursor] === "\n") cursor++;
|
|
567
|
+
return cursor;
|
|
568
|
+
}
|
|
569
|
+
function consumePassthroughCall(body, start) {
|
|
570
|
+
const parenIdx = body.indexOf("(", start);
|
|
571
|
+
if (parenIdx < 0) {
|
|
572
|
+
const nl = body.indexOf("\n", start);
|
|
573
|
+
return nl < 0 ? body.length : nl + 1;
|
|
574
|
+
}
|
|
575
|
+
const close = matchParen(body, parenIdx);
|
|
576
|
+
if (close < 0) {
|
|
577
|
+
const nl = body.indexOf("\n", start);
|
|
578
|
+
return nl < 0 ? body.length : nl + 1;
|
|
579
|
+
}
|
|
580
|
+
let cursor = close + 1;
|
|
581
|
+
if (body[cursor] === ";" || body[cursor] === "\n") cursor++;
|
|
582
|
+
return cursor;
|
|
583
|
+
}
|
|
584
|
+
function tryParsePlainDecl(body, start) {
|
|
585
|
+
const m = /^([ \t]*)([A-Za-z_$][\w$]*)\s*:/.exec(body.slice(start));
|
|
586
|
+
if (!m) return null;
|
|
587
|
+
const name = m[2];
|
|
588
|
+
let cursor = start + m[0].length;
|
|
589
|
+
const typeStart = cursor;
|
|
590
|
+
const typeEnd = scanUntilTopLevel(body, cursor, [
|
|
591
|
+
"=",
|
|
592
|
+
"\n",
|
|
593
|
+
";"
|
|
594
|
+
]);
|
|
595
|
+
const rawType = body.slice(typeStart, typeEnd).trim();
|
|
596
|
+
cursor = typeEnd;
|
|
597
|
+
let rawDefault;
|
|
598
|
+
if (body[cursor] === "=") {
|
|
599
|
+
cursor++;
|
|
600
|
+
const defStart = cursor;
|
|
601
|
+
const defEnd = scanUntilTopLevel(body, cursor, ["\n", ";"]);
|
|
602
|
+
rawDefault = body.slice(defStart, defEnd).trim();
|
|
603
|
+
cursor = defEnd;
|
|
604
|
+
}
|
|
605
|
+
if (body[cursor] === ";") cursor++;
|
|
606
|
+
if (body[cursor] === "\n") cursor++;
|
|
607
|
+
return {
|
|
608
|
+
name,
|
|
609
|
+
rawType,
|
|
610
|
+
rawDefault,
|
|
611
|
+
raw: body.slice(start, cursor).replace(/\n+$/, ""),
|
|
612
|
+
endIdx: cursor
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
function parseArrowOrFunctionExpr(s) {
|
|
616
|
+
s = s.trim();
|
|
617
|
+
if (s.startsWith("(")) {
|
|
618
|
+
const closeIdx = matchParen(s, 0);
|
|
619
|
+
if (closeIdx < 0) return null;
|
|
620
|
+
const args = s.slice(1, closeIdx);
|
|
621
|
+
let cur = closeIdx + 1;
|
|
622
|
+
while (cur < s.length && /\s/.test(s[cur])) cur++;
|
|
623
|
+
let retType;
|
|
624
|
+
if (s[cur] === ":") {
|
|
625
|
+
cur++;
|
|
626
|
+
const arrowAt = s.indexOf("=>", cur);
|
|
627
|
+
if (arrowAt < 0) return null;
|
|
628
|
+
retType = s.slice(cur, arrowAt).trim();
|
|
629
|
+
cur = arrowAt;
|
|
630
|
+
}
|
|
631
|
+
if (s.slice(cur, cur + 2) !== "=>") return null;
|
|
632
|
+
cur += 2;
|
|
633
|
+
while (cur < s.length && /\s/.test(s[cur])) cur++;
|
|
634
|
+
if (s[cur] === "{") {
|
|
635
|
+
const close = matchBrace(s, cur);
|
|
636
|
+
if (close < 0) return null;
|
|
637
|
+
return {
|
|
638
|
+
args,
|
|
639
|
+
body: s.slice(cur + 1, close),
|
|
640
|
+
retType,
|
|
641
|
+
isArrow: true
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
return {
|
|
645
|
+
args,
|
|
646
|
+
body: ` return ${s.slice(cur).trim()} `,
|
|
647
|
+
retType,
|
|
648
|
+
isArrow: true
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
const fnMatch = /^function\s*(?:[A-Za-z_$][\w$]*)?\s*\(/.exec(s);
|
|
652
|
+
if (fnMatch) {
|
|
653
|
+
const parenIdx = fnMatch[0].length - 1;
|
|
654
|
+
const parenClose = matchParen(s, parenIdx);
|
|
655
|
+
if (parenClose < 0) return null;
|
|
656
|
+
const args = s.slice(parenIdx + 1, parenClose);
|
|
657
|
+
let cur = parenClose + 1;
|
|
658
|
+
while (cur < s.length && /\s/.test(s[cur])) cur++;
|
|
659
|
+
let retType;
|
|
660
|
+
if (s[cur] === ":") {
|
|
661
|
+
cur++;
|
|
662
|
+
const braceAt = s.indexOf("{", cur);
|
|
663
|
+
if (braceAt < 0) return null;
|
|
664
|
+
retType = s.slice(cur, braceAt).trim();
|
|
665
|
+
cur = braceAt;
|
|
666
|
+
}
|
|
667
|
+
if (s[cur] !== "{") return null;
|
|
668
|
+
const close = matchBrace(s, cur);
|
|
669
|
+
if (close < 0) return null;
|
|
670
|
+
return {
|
|
671
|
+
args,
|
|
672
|
+
body: s.slice(cur + 1, close),
|
|
673
|
+
retType,
|
|
674
|
+
isArrow: false
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
function scanUntilTopLevel(s, start, stops) {
|
|
680
|
+
let j = start;
|
|
681
|
+
while (j < s.length) {
|
|
682
|
+
const c = s[j];
|
|
683
|
+
if (c === "/" && s[j + 1] === "/") {
|
|
684
|
+
const nl = s.indexOf("\n", j);
|
|
685
|
+
if (stops.includes("\n")) return nl < 0 ? s.length : nl;
|
|
686
|
+
j = nl < 0 ? s.length : nl + 1;
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
if (c === "/" && s[j + 1] === "*") {
|
|
690
|
+
const end = s.indexOf("*/", j + 2);
|
|
691
|
+
j = end < 0 ? s.length : end + 2;
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
if (c === "\"" || c === "'" || c === "`") {
|
|
695
|
+
j = skipString(s, j);
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
if (c === "{") {
|
|
699
|
+
const close = matchBrace(s, j);
|
|
700
|
+
j = close < 0 ? s.length : close + 1;
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
if (c === "(") {
|
|
704
|
+
const close = matchParen(s, j);
|
|
705
|
+
j = close < 0 ? s.length : close + 1;
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
if (c === "<") {
|
|
709
|
+
const close = scanGeneric(s, j);
|
|
710
|
+
if (close > j) {
|
|
711
|
+
j = close;
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
if (stops.includes(c)) return j;
|
|
716
|
+
j++;
|
|
717
|
+
}
|
|
718
|
+
return s.length;
|
|
719
|
+
}
|
|
720
|
+
function scanGeneric(s, start) {
|
|
721
|
+
if (s[start] !== "<") return start;
|
|
722
|
+
let depth = 0;
|
|
723
|
+
let j = start;
|
|
724
|
+
while (j < s.length) {
|
|
725
|
+
const c = s[j];
|
|
726
|
+
if (c === "<") depth++;
|
|
727
|
+
else if (c === ">") {
|
|
728
|
+
depth--;
|
|
729
|
+
if (depth === 0) return j + 1;
|
|
730
|
+
} else if (c === "\n") return start;
|
|
731
|
+
else if (c === "(" || c === "{") return start;
|
|
732
|
+
else if (c === "=" || c === ";") return start;
|
|
733
|
+
j++;
|
|
734
|
+
}
|
|
735
|
+
return start;
|
|
736
|
+
}
|
|
737
|
+
function scanComputedOrResourceExpr(body, start) {
|
|
738
|
+
const startLineIndent = countIndent(body, body.lastIndexOf("\n", start - 1) + 1);
|
|
739
|
+
let j = start;
|
|
740
|
+
const lineEnd = body.indexOf("\n", j);
|
|
741
|
+
if (lineEnd < 0) return body.length;
|
|
742
|
+
j = lineEnd + 1;
|
|
743
|
+
while (j < body.length) {
|
|
744
|
+
const nextLineEnd = body.indexOf("\n", j);
|
|
745
|
+
const lineEndIdx = nextLineEnd < 0 ? body.length : nextLineEnd;
|
|
746
|
+
const trimmed = body.slice(j, lineEndIdx).trim();
|
|
747
|
+
if (trimmed === "") return j;
|
|
748
|
+
const indent = countIndent(body, j);
|
|
749
|
+
if (indent > startLineIndent || indent === startLineIndent && /^[?:&|+\-*/<>=,.[\](){}]/.test(trimmed) && !trimmed.startsWith("//")) {
|
|
750
|
+
j = nextLineEnd < 0 ? body.length : nextLineEnd + 1;
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
return j;
|
|
754
|
+
}
|
|
755
|
+
return j;
|
|
756
|
+
}
|
|
757
|
+
function countIndent(body, lineStart) {
|
|
758
|
+
let n = 0;
|
|
759
|
+
while (lineStart + n < body.length) {
|
|
760
|
+
const c = body[lineStart + n];
|
|
761
|
+
if (c === " " || c === " ") n++;
|
|
762
|
+
else break;
|
|
763
|
+
}
|
|
764
|
+
return n;
|
|
765
|
+
}
|
|
766
|
+
function emitStateBody(buckets) {
|
|
767
|
+
const out = [];
|
|
768
|
+
const imports = buckets.passthrough.filter((p) => p.raw.startsWith("import "));
|
|
769
|
+
const otherPassthrough = buckets.passthrough.filter((p) => !p.raw.startsWith("import "));
|
|
770
|
+
for (const imp of imports) {
|
|
771
|
+
if (imp.leading.trim()) out.push(rstrip(imp.leading));
|
|
772
|
+
out.push(`${INDENT}${imp.raw}`);
|
|
773
|
+
}
|
|
774
|
+
if (imports.length > 0) out.push("");
|
|
775
|
+
if (buckets.props.length > 0) pushBlock(out, emitProps(buckets.props));
|
|
776
|
+
for (const p of buckets.plain) {
|
|
777
|
+
if (p.leading.trim()) out.push(rstrip(p.leading));
|
|
778
|
+
out.push(rebaseLeadingIndent(p.raw, INDENT));
|
|
779
|
+
out.push("");
|
|
780
|
+
}
|
|
781
|
+
trimTrailingBlanks(out);
|
|
782
|
+
if (buckets.computed.length > 0) pushBlock(out, emitComputed(buckets.computed));
|
|
783
|
+
if (buckets.action.length > 0) pushBlock(out, emitAction(buckets.action));
|
|
784
|
+
if (buckets.resource.length > 0) pushBlock(out, emitResource(buckets.resource));
|
|
785
|
+
if (buckets.effectNamed.length > 0) pushBlock(out, emitEffectNamed(buckets.effectNamed));
|
|
786
|
+
if (buckets.lifecycle.length > 0) pushBlock(out, emitLifecycle(buckets.lifecycle));
|
|
787
|
+
if (buckets.effectAnon.length > 0) pushBlock(out, emitEffectAnon(buckets.effectAnon));
|
|
788
|
+
for (const p of otherPassthrough) {
|
|
789
|
+
if (p.leading.trim()) out.push(rstrip(p.leading));
|
|
790
|
+
if (p.raw.includes("\n")) out.push(rebaseLeadingIndent(p.raw, INDENT));
|
|
791
|
+
else out.push(`${INDENT}${p.raw}`);
|
|
792
|
+
out.push("");
|
|
793
|
+
}
|
|
794
|
+
trimTrailingBlanks(out);
|
|
795
|
+
return out.join("\n");
|
|
796
|
+
}
|
|
797
|
+
function pushBlock(out, block) {
|
|
798
|
+
if (out.length > 0 && out[out.length - 1] !== "") out.push("");
|
|
799
|
+
out.push(block);
|
|
800
|
+
out.push("");
|
|
801
|
+
}
|
|
802
|
+
function trimTrailingBlanks(out) {
|
|
803
|
+
while (out.length > 0 && out[out.length - 1] === "") out.pop();
|
|
804
|
+
}
|
|
805
|
+
function emitProps(entries) {
|
|
806
|
+
const lines = [];
|
|
807
|
+
const firstLeading = entries[0]?.leading;
|
|
808
|
+
const headerLeading = firstLeading?.trim() ? firstLeading : "";
|
|
809
|
+
const inner = [];
|
|
810
|
+
for (const [idx, e] of entries.entries()) {
|
|
811
|
+
if (idx > 0 && e.leading.trim()) inner.push(rstrip(indentBlock(e.leading, `${INDENT}${INDENT}`)));
|
|
812
|
+
inner.push(formatPropEntry(e));
|
|
813
|
+
}
|
|
814
|
+
if (headerLeading) lines.push(rstrip(headerLeading));
|
|
815
|
+
lines.push(`${INDENT}$prop: {`);
|
|
816
|
+
lines.push(...inner);
|
|
817
|
+
lines.push(`${INDENT}}`);
|
|
818
|
+
return lines.join("\n");
|
|
819
|
+
}
|
|
820
|
+
function formatPropEntry(e) {
|
|
821
|
+
const keys = [];
|
|
822
|
+
if (shouldKeepType(e.rawType, e.rawDefault) && e.rawType) keys.push({
|
|
823
|
+
k: "type",
|
|
824
|
+
v: e.rawType
|
|
825
|
+
});
|
|
826
|
+
if (e.rawDefault !== void 0) keys.push({
|
|
827
|
+
k: "default",
|
|
828
|
+
v: e.rawDefault
|
|
829
|
+
});
|
|
830
|
+
if (e.describe) keys.push({
|
|
831
|
+
k: "describe",
|
|
832
|
+
v: quoteSingle(e.describe)
|
|
833
|
+
});
|
|
834
|
+
if (e.expose) keys.push({
|
|
835
|
+
k: "expose",
|
|
836
|
+
v: formatExpose(e.expose)
|
|
837
|
+
});
|
|
838
|
+
const inline = `${e.name}: { ${keys.map(({ k, v }) => `${k}: ${v}`).join(", ")} }`;
|
|
839
|
+
const baseIndent = 4;
|
|
840
|
+
const linePrefix = " ".repeat(baseIndent);
|
|
841
|
+
if (keys.length <= 3 && baseIndent + inline.length + 1 <= 100) return `${linePrefix}${inline},`;
|
|
842
|
+
const lines = [`${linePrefix}${e.name}: {`];
|
|
843
|
+
const inner = " ".repeat(baseIndent + 2);
|
|
844
|
+
for (const { k, v } of keys) lines.push(`${inner}${k}: ${v},`);
|
|
845
|
+
lines.push(`${linePrefix}},`);
|
|
846
|
+
return lines.join("\n");
|
|
847
|
+
}
|
|
848
|
+
function shouldKeepType(rawType, rawDefault) {
|
|
849
|
+
if (!rawType) return false;
|
|
850
|
+
if (rawDefault === void 0) return true;
|
|
851
|
+
const trimmed = rawDefault.trim();
|
|
852
|
+
if (trimmed === "null" || trimmed === "undefined") return true;
|
|
853
|
+
if (trimmed === "[]") return true;
|
|
854
|
+
if (trimmed === "{}") return true;
|
|
855
|
+
if (/^['"`]/.test(trimmed) && rawType.includes("|")) return true;
|
|
856
|
+
if (/[<>{}]/.test(rawType)) {
|
|
857
|
+
if (/^[0-9.+-]/.test(trimmed) || trimmed === "true" || trimmed === "false") return true;
|
|
858
|
+
if (/^['"`]/.test(trimmed)) return true;
|
|
859
|
+
return true;
|
|
860
|
+
}
|
|
861
|
+
return false;
|
|
862
|
+
}
|
|
863
|
+
function emitComputed(entries) {
|
|
864
|
+
return emitValueCollection("$computed", entries);
|
|
865
|
+
}
|
|
866
|
+
function emitResource(entries) {
|
|
867
|
+
return emitValueCollection("$resource", entries);
|
|
868
|
+
}
|
|
869
|
+
function emitEffectNamed(entries) {
|
|
870
|
+
return emitValueCollection("$effect", entries);
|
|
871
|
+
}
|
|
872
|
+
function emitValueCollection(macro, entries) {
|
|
873
|
+
const firstLeading = entries[0]?.leading;
|
|
874
|
+
const headerLeading = firstLeading?.trim() ? firstLeading : "";
|
|
875
|
+
const inner = [];
|
|
876
|
+
for (const [idx, e] of entries.entries()) {
|
|
877
|
+
if (idx > 0 && e.leading.trim()) inner.push(rstrip(indentBlock(e.leading, `${INDENT}${INDENT}`)));
|
|
878
|
+
inner.push(formatValueEntry(e));
|
|
879
|
+
}
|
|
880
|
+
const lines = [];
|
|
881
|
+
if (headerLeading) lines.push(rstrip(headerLeading));
|
|
882
|
+
lines.push(`${INDENT}${macro}: {`);
|
|
883
|
+
lines.push(...inner);
|
|
884
|
+
lines.push(`${INDENT}}`);
|
|
885
|
+
return lines.join("\n");
|
|
886
|
+
}
|
|
887
|
+
function formatValueEntry(e) {
|
|
888
|
+
const baseIndent = 4;
|
|
889
|
+
const linePrefix = " ".repeat(baseIndent);
|
|
890
|
+
if (!e.describe && !e.expose) {
|
|
891
|
+
if (!e.expr.includes("\n")) {
|
|
892
|
+
const inline = `${linePrefix}${e.name}: () => ${e.expr},`;
|
|
893
|
+
if (inline.length <= 100) return inline;
|
|
894
|
+
}
|
|
895
|
+
const indented = reindentExpr(e.expr, baseIndent + 2);
|
|
896
|
+
return `${linePrefix}${e.name}: () => ${indented},`;
|
|
897
|
+
}
|
|
898
|
+
const keys = [];
|
|
899
|
+
if (e.describe) keys.push({
|
|
900
|
+
k: "describe",
|
|
901
|
+
v: quoteSingle(e.describe)
|
|
902
|
+
});
|
|
903
|
+
if (e.expose) keys.push({
|
|
904
|
+
k: "expose",
|
|
905
|
+
v: formatExpose(e.expose)
|
|
906
|
+
});
|
|
907
|
+
const valueExpr = e.expr.includes("\n") ? `() => ${reindentExpr(e.expr, baseIndent + 4)}` : `() => ${e.expr}`;
|
|
908
|
+
keys.push({
|
|
909
|
+
k: "value",
|
|
910
|
+
v: valueExpr
|
|
911
|
+
});
|
|
912
|
+
const lines = [`${linePrefix}${e.name}: {`];
|
|
913
|
+
const inner = " ".repeat(baseIndent + 2);
|
|
914
|
+
for (const { k, v } of keys) lines.push(`${inner}${k}: ${v},`);
|
|
915
|
+
lines.push(`${linePrefix}},`);
|
|
916
|
+
return lines.join("\n");
|
|
917
|
+
}
|
|
918
|
+
function emitAction(entries) {
|
|
919
|
+
const firstLeading = entries[0]?.leading;
|
|
920
|
+
const headerLeading = firstLeading?.trim() ? firstLeading : "";
|
|
921
|
+
const inner = [];
|
|
922
|
+
for (const [idx, e] of entries.entries()) {
|
|
923
|
+
if (idx > 0 && e.leading.trim()) inner.push(rstrip(indentBlock(e.leading, `${INDENT}${INDENT}`)));
|
|
924
|
+
inner.push(formatActionEntry(e));
|
|
925
|
+
}
|
|
926
|
+
const lines = [];
|
|
927
|
+
if (headerLeading) lines.push(rstrip(headerLeading));
|
|
928
|
+
lines.push(`${INDENT}$action: {`);
|
|
929
|
+
lines.push(...inner);
|
|
930
|
+
lines.push(`${INDENT}}`);
|
|
931
|
+
return lines.join("\n");
|
|
932
|
+
}
|
|
933
|
+
function formatActionEntry(e) {
|
|
934
|
+
const ret = e.retType ? `: ${e.retType}` : "";
|
|
935
|
+
const arrow = `(${e.args})${ret} => `;
|
|
936
|
+
const baseIndent = 4;
|
|
937
|
+
const linePrefix = " ".repeat(baseIndent);
|
|
938
|
+
if (e.describe || e.expose) {
|
|
939
|
+
const keys = [];
|
|
940
|
+
if (e.describe) keys.push({
|
|
941
|
+
k: "describe",
|
|
942
|
+
v: quoteSingle(e.describe)
|
|
943
|
+
});
|
|
944
|
+
if (e.expose) keys.push({
|
|
945
|
+
k: "expose",
|
|
946
|
+
v: formatExpose(e.expose)
|
|
947
|
+
});
|
|
948
|
+
const bodyIndented = reindentBlockBody(e.body, baseIndent + 4);
|
|
949
|
+
keys.push({
|
|
950
|
+
k: "handler",
|
|
951
|
+
v: `${arrow}{${bodyIndented}}`
|
|
952
|
+
});
|
|
953
|
+
const lines = [`${linePrefix}${e.name}: {`];
|
|
954
|
+
const inner = " ".repeat(baseIndent + 2);
|
|
955
|
+
for (const { k, v } of keys) lines.push(`${inner}${k}: ${v},`);
|
|
956
|
+
lines.push(`${linePrefix}},`);
|
|
957
|
+
return lines.join("\n");
|
|
958
|
+
}
|
|
959
|
+
const bodyIndented = reindentBlockBody(e.body, baseIndent + 2);
|
|
960
|
+
return `${linePrefix}${e.name}: ${arrow}{${bodyIndented}},`;
|
|
961
|
+
}
|
|
962
|
+
function emitLifecycle(hooks) {
|
|
963
|
+
const firstLeading = hooks[0]?.leading;
|
|
964
|
+
const headerLeading = firstLeading?.trim() ? firstLeading : "";
|
|
965
|
+
const inner = [];
|
|
966
|
+
const baseIndent = 4;
|
|
967
|
+
const linePrefix = " ".repeat(baseIndent);
|
|
968
|
+
for (const [idx, h] of hooks.entries()) {
|
|
969
|
+
if (idx > 0 && h.leading.trim()) inner.push(rstrip(indentBlock(h.leading, linePrefix)));
|
|
970
|
+
const bodyIndented = reindentBlockBody(h.body, baseIndent + 2);
|
|
971
|
+
inner.push(`${linePrefix}${h.kind}: () => {${bodyIndented}},`);
|
|
972
|
+
}
|
|
973
|
+
const lines = [];
|
|
974
|
+
if (headerLeading) lines.push(rstrip(headerLeading));
|
|
975
|
+
lines.push(`${INDENT}$lifecycle: {`);
|
|
976
|
+
lines.push(...inner);
|
|
977
|
+
lines.push(`${INDENT}}`);
|
|
978
|
+
return lines.join("\n");
|
|
979
|
+
}
|
|
980
|
+
function emitEffectAnon(effects) {
|
|
981
|
+
const out = [];
|
|
982
|
+
const baseIndent = 2;
|
|
983
|
+
const linePrefix = " ".repeat(baseIndent);
|
|
984
|
+
for (const [idx, e] of effects.entries()) {
|
|
985
|
+
if (idx > 0) out.push("");
|
|
986
|
+
if (e.leading.trim()) out.push(rstrip(e.leading));
|
|
987
|
+
const bodyIndented = reindentBlockBody(e.body, baseIndent + 2);
|
|
988
|
+
out.push(`${linePrefix}$effect: () => {${bodyIndented}}`);
|
|
989
|
+
}
|
|
990
|
+
return out.join("\n");
|
|
991
|
+
}
|
|
992
|
+
function formatExpose(e) {
|
|
993
|
+
const parts = [];
|
|
994
|
+
if (e.read) parts.push("read: true");
|
|
995
|
+
if (e.write) parts.push("write: true");
|
|
996
|
+
return `{ ${parts.join(", ")} }`;
|
|
997
|
+
}
|
|
998
|
+
function quoteSingle(s) {
|
|
999
|
+
return `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
1000
|
+
}
|
|
1001
|
+
function rstrip(s) {
|
|
1002
|
+
return s.replace(/[ \t]+(\n|$)/g, "$1").replace(/^\n+/, "").replace(/\n+$/g, "");
|
|
1003
|
+
}
|
|
1004
|
+
function rebaseLeadingIndent(s, indent) {
|
|
1005
|
+
const lines = s.split("\n");
|
|
1006
|
+
const first = lines[0] ?? "";
|
|
1007
|
+
let n = 0;
|
|
1008
|
+
while (n < first.length && (first[n] === " " || first[n] === " ")) n++;
|
|
1009
|
+
if (n === 0) return [indent + first, ...lines.slice(1)].join("\n");
|
|
1010
|
+
return lines.map((line) => {
|
|
1011
|
+
if (line.length === 0) return line;
|
|
1012
|
+
let m = 0;
|
|
1013
|
+
while (m < line.length && m < n && (line[m] === " " || line[m] === " ")) m++;
|
|
1014
|
+
return indent + line.slice(m);
|
|
1015
|
+
}).join("\n");
|
|
1016
|
+
}
|
|
1017
|
+
function indentBlock(s, indent) {
|
|
1018
|
+
return s.split("\n").map((line) => line.length > 0 ? indent + line : line).join("\n");
|
|
1019
|
+
}
|
|
1020
|
+
function flattenWhitespace(s) {
|
|
1021
|
+
return s.replace(/\s+/g, " ").trim();
|
|
1022
|
+
}
|
|
1023
|
+
function reindentBlockBody(bodyText, targetInner) {
|
|
1024
|
+
const lines = bodyText.split("\n");
|
|
1025
|
+
let firstReal = 0;
|
|
1026
|
+
while (firstReal < lines.length && lines[firstReal].trim() === "") firstReal++;
|
|
1027
|
+
let lastReal = lines.length - 1;
|
|
1028
|
+
while (lastReal >= 0 && lines[lastReal].trim() === "") lastReal--;
|
|
1029
|
+
if (firstReal > lastReal) return "";
|
|
1030
|
+
let minIndent = Infinity;
|
|
1031
|
+
for (let k = firstReal; k <= lastReal; k++) {
|
|
1032
|
+
const line = lines[k];
|
|
1033
|
+
if (line.trim() === "") continue;
|
|
1034
|
+
let n = 0;
|
|
1035
|
+
while (n < line.length && (line[n] === " " || line[n] === " ")) n++;
|
|
1036
|
+
if (n < minIndent) minIndent = n;
|
|
1037
|
+
}
|
|
1038
|
+
if (!Number.isFinite(minIndent)) minIndent = 0;
|
|
1039
|
+
const rebased = [""];
|
|
1040
|
+
const targetSpaces = " ".repeat(targetInner);
|
|
1041
|
+
for (let k = firstReal; k <= lastReal; k++) {
|
|
1042
|
+
const line = lines[k];
|
|
1043
|
+
if (line.trim() === "") {
|
|
1044
|
+
rebased.push("");
|
|
1045
|
+
continue;
|
|
1046
|
+
}
|
|
1047
|
+
rebased.push((targetSpaces + line.slice(minIndent)).replace(/[ \t]+$/, ""));
|
|
1048
|
+
}
|
|
1049
|
+
rebased.push(" ".repeat(Math.max(targetInner - 2, 0)));
|
|
1050
|
+
return rebased.join("\n");
|
|
1051
|
+
}
|
|
1052
|
+
function reindentExpr(expr, targetInner) {
|
|
1053
|
+
const lines = expr.split("\n");
|
|
1054
|
+
if (lines.length === 1) return expr;
|
|
1055
|
+
const firstLine = lines[0];
|
|
1056
|
+
const rest = lines.slice(1);
|
|
1057
|
+
let minIndent = Infinity;
|
|
1058
|
+
for (const line of rest) {
|
|
1059
|
+
if (line.trim() === "") continue;
|
|
1060
|
+
let n = 0;
|
|
1061
|
+
while (n < line.length && (line[n] === " " || line[n] === " ")) n++;
|
|
1062
|
+
if (n < minIndent) minIndent = n;
|
|
1063
|
+
}
|
|
1064
|
+
if (!Number.isFinite(minIndent)) minIndent = 0;
|
|
1065
|
+
const targetSpaces = " ".repeat(targetInner);
|
|
1066
|
+
return [firstLine, ...rest.map((line) => line.trim() === "" ? "" : targetSpaces + line.slice(minIndent))].join("\n");
|
|
1067
|
+
}
|
|
1068
|
+
function expandLeftToLineStart(s, idx) {
|
|
1069
|
+
let j = idx;
|
|
1070
|
+
while (j > 0 && s[j - 1] !== "\n") j--;
|
|
1071
|
+
if (j > 0 && s[j - 1] === "\n") {
|
|
1072
|
+
let k = j - 1;
|
|
1073
|
+
while (k > 0 && (s[k - 1] === " " || s[k - 1] === " ")) k--;
|
|
1074
|
+
if (k > 0 && s[k - 1] === "\n") j = k;
|
|
1075
|
+
}
|
|
1076
|
+
return j;
|
|
1077
|
+
}
|
|
1078
|
+
function expandRightThroughBlankLines(s, idx) {
|
|
1079
|
+
let j = idx;
|
|
1080
|
+
while (j < s.length && (s[j] === " " || s[j] === " ")) j++;
|
|
1081
|
+
if (s[j] === "\n") j++;
|
|
1082
|
+
while (j < s.length) {
|
|
1083
|
+
let k = j;
|
|
1084
|
+
while (k < s.length && (s[k] === " " || s[k] === " ")) k++;
|
|
1085
|
+
if (s[k] === "\n") {
|
|
1086
|
+
j = k + 1;
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
break;
|
|
1090
|
+
}
|
|
1091
|
+
return j;
|
|
1092
|
+
}
|
|
1093
|
+
function tryParseV2Collection(body, start, sidecar, buckets, leading, nextPosition) {
|
|
1094
|
+
const m = /^\$(prop|computed|action|resource|effect|lifecycle)\s*:\s*\{/.exec(body.slice(start));
|
|
1095
|
+
if (!m) return null;
|
|
1096
|
+
const macro = m[1];
|
|
1097
|
+
const braceIdx = start + m[0].length - 1;
|
|
1098
|
+
const closeIdx = matchBrace(body, braceIdx);
|
|
1099
|
+
if (closeIdx < 0) return null;
|
|
1100
|
+
const entries = splitTopLevelEntries(body.slice(braceIdx + 1, closeIdx));
|
|
1101
|
+
let outerLeadingConsumed = false;
|
|
1102
|
+
for (const e of entries) {
|
|
1103
|
+
const parsed = parseV2Entry(e.text);
|
|
1104
|
+
if (!parsed) continue;
|
|
1105
|
+
let entryLeading;
|
|
1106
|
+
if (!outerLeadingConsumed) {
|
|
1107
|
+
entryLeading = e.leading.trim() ? e.leading : leading;
|
|
1108
|
+
outerLeadingConsumed = true;
|
|
1109
|
+
} else entryLeading = e.leading.trim() ? e.leading : "";
|
|
1110
|
+
routeV2Entry(macro, parsed, sidecar, buckets, entryLeading, nextPosition);
|
|
1111
|
+
}
|
|
1112
|
+
let cursor = closeIdx + 1;
|
|
1113
|
+
if (body[cursor] === ";") cursor++;
|
|
1114
|
+
if (body[cursor] === "\n") cursor++;
|
|
1115
|
+
return cursor;
|
|
1116
|
+
}
|
|
1117
|
+
function tryParseV2AnonEffect(body, start, buckets, leading, position) {
|
|
1118
|
+
const colonIdx = body.indexOf(":", start);
|
|
1119
|
+
if (colonIdx < 0) return null;
|
|
1120
|
+
let cursor = colonIdx + 1;
|
|
1121
|
+
while (cursor < body.length && /[ \t]/.test(body[cursor])) cursor++;
|
|
1122
|
+
if (body.slice(cursor, cursor + 5) === "async") cursor += 5;
|
|
1123
|
+
while (cursor < body.length && /[ \t]/.test(body[cursor])) cursor++;
|
|
1124
|
+
if (body[cursor] !== "(") return null;
|
|
1125
|
+
const exprStart = cursor;
|
|
1126
|
+
const fn = parseArrowOrFunctionExpr(body.slice(exprStart));
|
|
1127
|
+
if (!fn) return null;
|
|
1128
|
+
const closeIdx = scanArrowExprEnd(body, exprStart);
|
|
1129
|
+
if (closeIdx < 0) return null;
|
|
1130
|
+
buckets.effectAnon.push({
|
|
1131
|
+
body: fn.body,
|
|
1132
|
+
leading,
|
|
1133
|
+
position
|
|
1134
|
+
});
|
|
1135
|
+
let c = closeIdx;
|
|
1136
|
+
if (body[c] === ";") c++;
|
|
1137
|
+
if (body[c] === "\n") c++;
|
|
1138
|
+
return c;
|
|
1139
|
+
}
|
|
1140
|
+
function scanArrowExprEnd(s, start) {
|
|
1141
|
+
if (s[start] !== "(") return -1;
|
|
1142
|
+
const parenClose = matchParen(s, start);
|
|
1143
|
+
if (parenClose < 0) return -1;
|
|
1144
|
+
let cur = parenClose + 1;
|
|
1145
|
+
while (cur < s.length && /[ \t]/.test(s[cur])) cur++;
|
|
1146
|
+
if (s[cur] === ":") {
|
|
1147
|
+
cur++;
|
|
1148
|
+
const arrowAt = s.indexOf("=>", cur);
|
|
1149
|
+
if (arrowAt < 0) return -1;
|
|
1150
|
+
cur = arrowAt;
|
|
1151
|
+
}
|
|
1152
|
+
if (s.slice(cur, cur + 2) !== "=>") return -1;
|
|
1153
|
+
cur += 2;
|
|
1154
|
+
while (cur < s.length && /[ \t\n]/.test(s[cur])) cur++;
|
|
1155
|
+
if (s[cur] === "{") {
|
|
1156
|
+
const close = matchBrace(s, cur);
|
|
1157
|
+
if (close < 0) return -1;
|
|
1158
|
+
return close + 1;
|
|
1159
|
+
}
|
|
1160
|
+
const nl = s.indexOf("\n", cur);
|
|
1161
|
+
return nl < 0 ? s.length : nl;
|
|
1162
|
+
}
|
|
1163
|
+
function splitTopLevelEntries(inner) {
|
|
1164
|
+
const out = [];
|
|
1165
|
+
let depthBrace = 0;
|
|
1166
|
+
let depthParen = 0;
|
|
1167
|
+
let depthBracket = 0;
|
|
1168
|
+
let start = 0;
|
|
1169
|
+
let j = 0;
|
|
1170
|
+
while (j < inner.length) {
|
|
1171
|
+
const c = inner[j];
|
|
1172
|
+
if (c === "/" && inner[j + 1] === "/") {
|
|
1173
|
+
const nl = inner.indexOf("\n", j);
|
|
1174
|
+
j = nl < 0 ? inner.length : nl;
|
|
1175
|
+
continue;
|
|
1176
|
+
}
|
|
1177
|
+
if (c === "/" && inner[j + 1] === "*") {
|
|
1178
|
+
const end = inner.indexOf("*/", j + 2);
|
|
1179
|
+
j = end < 0 ? inner.length : end + 2;
|
|
1180
|
+
continue;
|
|
1181
|
+
}
|
|
1182
|
+
if (c === "\"" || c === "'" || c === "`") {
|
|
1183
|
+
j = skipString(inner, j);
|
|
1184
|
+
continue;
|
|
1185
|
+
}
|
|
1186
|
+
if (c === "{") depthBrace++;
|
|
1187
|
+
else if (c === "}") depthBrace--;
|
|
1188
|
+
else if (c === "(") depthParen++;
|
|
1189
|
+
else if (c === ")") depthParen--;
|
|
1190
|
+
else if (c === "[") depthBracket++;
|
|
1191
|
+
else if (c === "]") depthBracket--;
|
|
1192
|
+
else if (c === "," && depthBrace === 0 && depthParen === 0 && depthBracket === 0) {
|
|
1193
|
+
pushSplitEntry(out, inner.slice(start, j));
|
|
1194
|
+
start = j + 1;
|
|
1195
|
+
}
|
|
1196
|
+
j++;
|
|
1197
|
+
}
|
|
1198
|
+
if (start < inner.length) pushSplitEntry(out, inner.slice(start));
|
|
1199
|
+
return out;
|
|
1200
|
+
}
|
|
1201
|
+
function pushSplitEntry(out, piece) {
|
|
1202
|
+
let head = "";
|
|
1203
|
+
let i = 0;
|
|
1204
|
+
while (i < piece.length) {
|
|
1205
|
+
const lineEnd = piece.indexOf("\n", i);
|
|
1206
|
+
const end = lineEnd < 0 ? piece.length : lineEnd;
|
|
1207
|
+
const line = piece.slice(i, end);
|
|
1208
|
+
if (line.trim() === "" || line.trim().startsWith("//") || line.trim().startsWith("/*")) {
|
|
1209
|
+
head += `${line}\n`;
|
|
1210
|
+
i = end + 1;
|
|
1211
|
+
if (lineEnd < 0) break;
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
break;
|
|
1215
|
+
}
|
|
1216
|
+
const text = piece.slice(i).trim();
|
|
1217
|
+
if (text === "") return;
|
|
1218
|
+
out.push({
|
|
1219
|
+
text,
|
|
1220
|
+
leading: head
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
function parseV2Entry(text) {
|
|
1224
|
+
const m = /^([A-Za-z_$][\w$]*)\s*:\s*/.exec(text);
|
|
1225
|
+
if (!m) return null;
|
|
1226
|
+
const name = m[1];
|
|
1227
|
+
const rest = text.slice(m[0].length).trim();
|
|
1228
|
+
if (rest.startsWith("{")) {
|
|
1229
|
+
const close = matchBrace(rest, 0);
|
|
1230
|
+
if (close < 0) return null;
|
|
1231
|
+
return {
|
|
1232
|
+
name,
|
|
1233
|
+
rhs: rest.slice(1, close).trim(),
|
|
1234
|
+
isWrapped: true
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
return {
|
|
1238
|
+
name,
|
|
1239
|
+
rhs: rest,
|
|
1240
|
+
isWrapped: false
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
function parseWrappedFields(body) {
|
|
1244
|
+
const out = {};
|
|
1245
|
+
const entries = splitTopLevelEntries(body);
|
|
1246
|
+
for (const e of entries) {
|
|
1247
|
+
const m = /^([A-Za-z_$][\w$]*)\s*:\s*/.exec(e.text);
|
|
1248
|
+
if (!m) continue;
|
|
1249
|
+
const k = m[1];
|
|
1250
|
+
const v = e.text.slice(m[0].length).trim();
|
|
1251
|
+
if (k === "describe") out.describe = unquoteString(v);
|
|
1252
|
+
else if (k === "expose") {
|
|
1253
|
+
const parsed = parseExposeLiteral(v);
|
|
1254
|
+
if (parsed) out.expose = parsed;
|
|
1255
|
+
} else if (k === "value") out.value = v;
|
|
1256
|
+
else if (k === "handler") out.handler = v;
|
|
1257
|
+
else if (k === "default") out.default = v;
|
|
1258
|
+
else if (k === "type") out.type = v;
|
|
1259
|
+
else if (k === "on") out.on = v;
|
|
1260
|
+
}
|
|
1261
|
+
return out;
|
|
1262
|
+
}
|
|
1263
|
+
function unquoteString(s) {
|
|
1264
|
+
s = s.trim();
|
|
1265
|
+
if (s.startsWith("'") && s.endsWith("'") || s.startsWith("\"") && s.endsWith("\"")) return s.slice(1, -1).replace(/\\'/g, "'").replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
|
|
1266
|
+
return s;
|
|
1267
|
+
}
|
|
1268
|
+
function parseExposeLiteral(s) {
|
|
1269
|
+
s = s.trim();
|
|
1270
|
+
if (!s.startsWith("{")) return void 0;
|
|
1271
|
+
const close = matchBrace(s, 0);
|
|
1272
|
+
if (close < 0) return void 0;
|
|
1273
|
+
const body = s.slice(1, close);
|
|
1274
|
+
return {
|
|
1275
|
+
read: /\bread\s*:\s*true\b/.test(body),
|
|
1276
|
+
write: /\bwrite\s*:\s*true\b/.test(body)
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
function routeV2Entry(macro, entry, _sidecar, buckets, leading, nextPosition) {
|
|
1280
|
+
if (macro === "prop") {
|
|
1281
|
+
if (entry.isWrapped) {
|
|
1282
|
+
const f = parseWrappedFields(entry.rhs);
|
|
1283
|
+
buckets.props.push({
|
|
1284
|
+
name: entry.name,
|
|
1285
|
+
rawType: f.type,
|
|
1286
|
+
rawDefault: f.default,
|
|
1287
|
+
describe: f.describe,
|
|
1288
|
+
expose: f.expose,
|
|
1289
|
+
leading
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
if (macro === "computed" || macro === "resource") {
|
|
1295
|
+
let expr;
|
|
1296
|
+
let describe;
|
|
1297
|
+
let expose;
|
|
1298
|
+
if (entry.isWrapped) {
|
|
1299
|
+
const f = parseWrappedFields(entry.rhs);
|
|
1300
|
+
expr = stripThunk(f.value ?? "");
|
|
1301
|
+
describe = f.describe;
|
|
1302
|
+
expose = f.expose;
|
|
1303
|
+
} else expr = stripThunk(entry.rhs);
|
|
1304
|
+
(macro === "computed" ? buckets.computed : buckets.resource).push({
|
|
1305
|
+
name: entry.name,
|
|
1306
|
+
expr,
|
|
1307
|
+
describe,
|
|
1308
|
+
expose,
|
|
1309
|
+
leading
|
|
1310
|
+
});
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
if (macro === "action") {
|
|
1314
|
+
let handlerSrc;
|
|
1315
|
+
let describe;
|
|
1316
|
+
let expose;
|
|
1317
|
+
if (entry.isWrapped) {
|
|
1318
|
+
const f = parseWrappedFields(entry.rhs);
|
|
1319
|
+
handlerSrc = f.handler ?? "";
|
|
1320
|
+
describe = f.describe;
|
|
1321
|
+
expose = f.expose;
|
|
1322
|
+
} else handlerSrc = entry.rhs;
|
|
1323
|
+
const fn = parseArrowOrFunctionExpr(handlerSrc);
|
|
1324
|
+
if (!fn) return;
|
|
1325
|
+
buckets.action.push({
|
|
1326
|
+
name: entry.name,
|
|
1327
|
+
args: fn.args,
|
|
1328
|
+
retType: fn.retType,
|
|
1329
|
+
body: fn.body,
|
|
1330
|
+
describe,
|
|
1331
|
+
expose,
|
|
1332
|
+
leading
|
|
1333
|
+
});
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
if (macro === "effect") {
|
|
1337
|
+
let expr;
|
|
1338
|
+
let describe;
|
|
1339
|
+
let expose;
|
|
1340
|
+
if (entry.isWrapped) {
|
|
1341
|
+
const f = parseWrappedFields(entry.rhs);
|
|
1342
|
+
expr = stripThunk(f.value ?? "");
|
|
1343
|
+
describe = f.describe;
|
|
1344
|
+
expose = f.expose;
|
|
1345
|
+
} else expr = stripThunk(entry.rhs);
|
|
1346
|
+
buckets.effectNamed.push({
|
|
1347
|
+
name: entry.name,
|
|
1348
|
+
expr,
|
|
1349
|
+
describe,
|
|
1350
|
+
expose,
|
|
1351
|
+
leading
|
|
1352
|
+
});
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
if (macro === "lifecycle") {
|
|
1356
|
+
if (entry.name !== "mount" && entry.name !== "dispose") return;
|
|
1357
|
+
const fn = parseArrowOrFunctionExpr(entry.isWrapped ? entry.rhs : entry.rhs);
|
|
1358
|
+
if (!fn) return;
|
|
1359
|
+
buckets.lifecycle.push({
|
|
1360
|
+
kind: entry.name,
|
|
1361
|
+
body: fn.body,
|
|
1362
|
+
leading,
|
|
1363
|
+
position: nextPosition()
|
|
1364
|
+
});
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
function stripThunk(s) {
|
|
1369
|
+
s = s.trim();
|
|
1370
|
+
const m = /^\(\s*\)\s*(?::\s*[^=]+)?=>\s*/.exec(s);
|
|
1371
|
+
if (!m) return s;
|
|
1372
|
+
const rest = s.slice(m[0].length);
|
|
1373
|
+
if (rest.startsWith("{")) {
|
|
1374
|
+
const close = matchBrace(rest, 0);
|
|
1375
|
+
if (close < 0) return rest;
|
|
1376
|
+
return rest.slice(1, close).trim();
|
|
1377
|
+
}
|
|
1378
|
+
return rest.trim();
|
|
1379
|
+
}
|
|
1380
|
+
//#endregion
|
|
1381
|
+
//#region src/core/code-action.ts
|
|
1382
|
+
/**
|
|
1383
|
+
* packages/language-server/src/core/code-action.ts
|
|
1384
|
+
*
|
|
1385
|
+
* QuickFix code-action backing — bridges compiler diagnostic codes C440-C444
|
|
1386
|
+
* (rejected old-spec macro forms) to the macro-simplification codemod
|
|
1387
|
+
* (`migrate()`), which rewrites a full document to v2 collection-form syntax.
|
|
1388
|
+
*
|
|
1389
|
+
* Editor-agnostic: this module computes WHAT the fix is (the rewritten text +
|
|
1390
|
+
* any warnings). The connection layer (src/server.ts) turns that into a protocol
|
|
1391
|
+
* `WorkspaceEdit`/`CodeAction`. This keeps the codemod bridge a clean seam for a
|
|
1392
|
+
* future Volar code-action provider.
|
|
1393
|
+
*
|
|
1394
|
+
* The migrate() codemod is internal monorepo source under @aihu/compiler (not a
|
|
1395
|
+
* public package export); imported via the workspace-relative path the same way
|
|
1396
|
+
* the original embedded server did.
|
|
1397
|
+
*/
|
|
1398
|
+
/** Compiler diagnostic codes whose QuickFix is the v2 macro migration codemod. */
|
|
1399
|
+
const MIGRATE_CODES = new Set([
|
|
1400
|
+
"C440",
|
|
1401
|
+
"C441",
|
|
1402
|
+
"C442",
|
|
1403
|
+
"C443",
|
|
1404
|
+
"C444"
|
|
1405
|
+
]);
|
|
1406
|
+
/**
|
|
1407
|
+
* Run the macro-simplification codemod over `source` and return the fix payload.
|
|
1408
|
+
* Returns `null` when the codemod throws (malformed source) so callers can fall
|
|
1409
|
+
* back to offering no action rather than surfacing a crash.
|
|
1410
|
+
*/
|
|
1411
|
+
function buildMigrateFix(source) {
|
|
1412
|
+
let result;
|
|
1413
|
+
try {
|
|
1414
|
+
result = migrate(source);
|
|
1415
|
+
} catch {
|
|
1416
|
+
return null;
|
|
1417
|
+
}
|
|
1418
|
+
const { rewritten, warnings } = result;
|
|
1419
|
+
let title = "Migrate to v2 macro syntax (aihu codemod)";
|
|
1420
|
+
if (warnings.length > 0) title += ` (${warnings.length} warning(s) - see Output panel)`;
|
|
1421
|
+
return {
|
|
1422
|
+
rewritten,
|
|
1423
|
+
warnings,
|
|
1424
|
+
title
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
//#endregion
|
|
1428
|
+
//#region src/core/completion.ts
|
|
1429
|
+
/**
|
|
1430
|
+
* packages/language-server/src/core/completion.ts
|
|
1431
|
+
*
|
|
1432
|
+
* LSP completion items for aihu v2 macro-kind collection-form snippets.
|
|
1433
|
+
* Triggered by '$' inside @state block, '@' at top level.
|
|
1434
|
+
*
|
|
1435
|
+
* Uses inline numeric constants for CompletionItemKind and InsertTextFormat
|
|
1436
|
+
* to avoid importing vscode-languageserver at module load time (enables testing
|
|
1437
|
+
* without the vscode-languageserver package installed, and keeps this module
|
|
1438
|
+
* editor-agnostic — the clean seam for a future Volar adoption).
|
|
1439
|
+
*/
|
|
1440
|
+
const SNIPPET_KIND = 15;
|
|
1441
|
+
const SNIPPET_FORMAT = 2;
|
|
1442
|
+
/** v2 macro-kind snippet completions (triggered by '$' in @state block). */
|
|
1443
|
+
const STATE_MACRO_COMPLETIONS = [
|
|
1444
|
+
{
|
|
1445
|
+
label: "$prop",
|
|
1446
|
+
kind: SNIPPET_KIND,
|
|
1447
|
+
insertTextFormat: SNIPPET_FORMAT,
|
|
1448
|
+
detail: "v2 collection-form prop declarations",
|
|
1449
|
+
sortText: "0_$prop",
|
|
1450
|
+
insertText: [
|
|
1451
|
+
"$prop: {",
|
|
1452
|
+
" ${1:name}: { default: ${2:undefined}, describe: '${3:description}' },",
|
|
1453
|
+
"}"
|
|
1454
|
+
].join("\n")
|
|
1455
|
+
},
|
|
1456
|
+
{
|
|
1457
|
+
label: "$computed",
|
|
1458
|
+
kind: SNIPPET_KIND,
|
|
1459
|
+
insertTextFormat: SNIPPET_FORMAT,
|
|
1460
|
+
detail: "v2 collection-form computed declarations",
|
|
1461
|
+
sortText: "0_$computed",
|
|
1462
|
+
insertText: [
|
|
1463
|
+
"$computed: {",
|
|
1464
|
+
" ${1:name}: () => ${2:expression},",
|
|
1465
|
+
"}"
|
|
1466
|
+
].join("\n")
|
|
1467
|
+
},
|
|
1468
|
+
{
|
|
1469
|
+
label: "$action",
|
|
1470
|
+
kind: SNIPPET_KIND,
|
|
1471
|
+
insertTextFormat: SNIPPET_FORMAT,
|
|
1472
|
+
detail: "v2 collection-form action declarations",
|
|
1473
|
+
sortText: "0_$action",
|
|
1474
|
+
insertText: [
|
|
1475
|
+
"$action: {",
|
|
1476
|
+
" ${1:name}: (${2:args}) => { ${3:body} },",
|
|
1477
|
+
"}"
|
|
1478
|
+
].join("\n")
|
|
1479
|
+
},
|
|
1480
|
+
{
|
|
1481
|
+
label: "$resource",
|
|
1482
|
+
kind: SNIPPET_KIND,
|
|
1483
|
+
insertTextFormat: SNIPPET_FORMAT,
|
|
1484
|
+
detail: "v2 collection-form resource declarations",
|
|
1485
|
+
sortText: "0_$resource",
|
|
1486
|
+
insertText: [
|
|
1487
|
+
"$resource: {",
|
|
1488
|
+
" ${1:name}: () => ${2:fetchExpr()},",
|
|
1489
|
+
"}"
|
|
1490
|
+
].join("\n")
|
|
1491
|
+
},
|
|
1492
|
+
{
|
|
1493
|
+
label: "$effect",
|
|
1494
|
+
kind: SNIPPET_KIND,
|
|
1495
|
+
insertTextFormat: SNIPPET_FORMAT,
|
|
1496
|
+
detail: "v2 anonymous effect declaration",
|
|
1497
|
+
sortText: "0_$effect",
|
|
1498
|
+
insertText: [
|
|
1499
|
+
"$effect: () => {",
|
|
1500
|
+
" ${1:body}",
|
|
1501
|
+
"}"
|
|
1502
|
+
].join("\n")
|
|
1503
|
+
},
|
|
1504
|
+
{
|
|
1505
|
+
label: "$lifecycle",
|
|
1506
|
+
kind: SNIPPET_KIND,
|
|
1507
|
+
insertTextFormat: SNIPPET_FORMAT,
|
|
1508
|
+
detail: "v2 collection-form lifecycle hooks",
|
|
1509
|
+
sortText: "0_$lifecycle",
|
|
1510
|
+
insertText: [
|
|
1511
|
+
"$lifecycle: {",
|
|
1512
|
+
" mount: () => { ${1:initBody} },",
|
|
1513
|
+
" dispose: () => { ${2:cleanupBody} },",
|
|
1514
|
+
"}"
|
|
1515
|
+
].join("\n")
|
|
1516
|
+
},
|
|
1517
|
+
{
|
|
1518
|
+
label: "$emit",
|
|
1519
|
+
kind: SNIPPET_KIND,
|
|
1520
|
+
insertTextFormat: SNIPPET_FORMAT,
|
|
1521
|
+
detail: "typed custom event declarations",
|
|
1522
|
+
sortText: "0_$emit",
|
|
1523
|
+
insertText: [
|
|
1524
|
+
"$emit: {",
|
|
1525
|
+
" ${1:eventName}: (payload: ${2:PayloadType}) => void",
|
|
1526
|
+
"}"
|
|
1527
|
+
].join("\n")
|
|
1528
|
+
},
|
|
1529
|
+
{
|
|
1530
|
+
label: "$on",
|
|
1531
|
+
kind: SNIPPET_KIND,
|
|
1532
|
+
insertTextFormat: SNIPPET_FORMAT,
|
|
1533
|
+
detail: "event listener attribute (template)",
|
|
1534
|
+
sortText: "0_$on",
|
|
1535
|
+
insertText: "$on.${1:click}={${2:handler}}"
|
|
1536
|
+
},
|
|
1537
|
+
{
|
|
1538
|
+
label: "$bind",
|
|
1539
|
+
kind: SNIPPET_KIND,
|
|
1540
|
+
insertTextFormat: SNIPPET_FORMAT,
|
|
1541
|
+
detail: "two-way attribute binding (template)",
|
|
1542
|
+
sortText: "0_$bind",
|
|
1543
|
+
insertText: "$bind.${1:value}={${2:signal}}"
|
|
1544
|
+
}
|
|
1545
|
+
];
|
|
1546
|
+
/** Top-level block completions (triggered by '@' at top level). */
|
|
1547
|
+
const BLOCK_COMPLETIONS = [
|
|
1548
|
+
{
|
|
1549
|
+
label: "@state",
|
|
1550
|
+
kind: SNIPPET_KIND,
|
|
1551
|
+
insertTextFormat: SNIPPET_FORMAT,
|
|
1552
|
+
detail: "aihu @state block",
|
|
1553
|
+
sortText: "0_@state",
|
|
1554
|
+
insertText: "@state"
|
|
1555
|
+
},
|
|
1556
|
+
{
|
|
1557
|
+
label: "@template",
|
|
1558
|
+
kind: SNIPPET_KIND,
|
|
1559
|
+
insertTextFormat: SNIPPET_FORMAT,
|
|
1560
|
+
detail: "aihu @template block",
|
|
1561
|
+
sortText: "0_@template",
|
|
1562
|
+
insertText: "@template"
|
|
1563
|
+
},
|
|
1564
|
+
{
|
|
1565
|
+
label: "@style",
|
|
1566
|
+
kind: SNIPPET_KIND,
|
|
1567
|
+
insertTextFormat: SNIPPET_FORMAT,
|
|
1568
|
+
detail: "aihu @style block",
|
|
1569
|
+
sortText: "0_@style",
|
|
1570
|
+
insertText: "@style"
|
|
1571
|
+
},
|
|
1572
|
+
{
|
|
1573
|
+
label: "@agent",
|
|
1574
|
+
kind: SNIPPET_KIND,
|
|
1575
|
+
insertTextFormat: SNIPPET_FORMAT,
|
|
1576
|
+
detail: "aihu @agent block for MCP tool exposure",
|
|
1577
|
+
sortText: "0_@agent",
|
|
1578
|
+
insertText: "@agent"
|
|
1579
|
+
},
|
|
1580
|
+
{
|
|
1581
|
+
label: "@route",
|
|
1582
|
+
kind: SNIPPET_KIND,
|
|
1583
|
+
insertTextFormat: SNIPPET_FORMAT,
|
|
1584
|
+
detail: "aihu @route block for file-based routing",
|
|
1585
|
+
sortText: "0_@route",
|
|
1586
|
+
insertText: "@route"
|
|
1587
|
+
}
|
|
1588
|
+
];
|
|
1589
|
+
//#endregion
|
|
1590
|
+
//#region src/core/diagnostics.ts
|
|
1591
|
+
/**
|
|
1592
|
+
* packages/language-server/src/core/diagnostics.ts
|
|
1593
|
+
*
|
|
1594
|
+
* Async wrapper around the aihu-compile Rust binary with --machine-errors support.
|
|
1595
|
+
* Invoked by the LSP server on every textDocument/didOpen and textDocument/didChange.
|
|
1596
|
+
*
|
|
1597
|
+
* NOTE: Uses node:child_process execFile (not exec) with argv arrays — safe from
|
|
1598
|
+
* shell injection. The LSP server process runs out-of-process for editor isolation.
|
|
1599
|
+
*
|
|
1600
|
+
* This module is editor-agnostic: it returns plain `AihuDiagnostic` records with
|
|
1601
|
+
* 0-based LSP positions. The transport/connection layer (src/server.ts) maps these
|
|
1602
|
+
* onto protocol `Diagnostic` objects. Keeping the parse logic here is the clean
|
|
1603
|
+
* seam for a future `@volar/language-core` virtual-code adoption (arch-4 §2.7).
|
|
1604
|
+
*/
|
|
1605
|
+
const execFileAsync = promisify(execFile);
|
|
1606
|
+
const ext = process.platform === "win32" ? ".exe" : "";
|
|
1607
|
+
const binPath = process.env.AIHU_COMPILE_BIN ?? resolve(dirname(fileURLToPath(import.meta.url)), `../../../compiler/bin/aihu-compile${ext}`);
|
|
1608
|
+
function toAihuDiagnostic(raw) {
|
|
1609
|
+
let lspRange = null;
|
|
1610
|
+
if (raw.range && raw.range.line > 0) {
|
|
1611
|
+
const startLine = raw.range.line - 1;
|
|
1612
|
+
const endLine = raw.range.end_line - 1;
|
|
1613
|
+
lspRange = {
|
|
1614
|
+
start: {
|
|
1615
|
+
line: startLine,
|
|
1616
|
+
character: raw.range.col
|
|
1617
|
+
},
|
|
1618
|
+
end: {
|
|
1619
|
+
line: endLine,
|
|
1620
|
+
character: raw.range.end_col
|
|
1621
|
+
}
|
|
1622
|
+
};
|
|
1623
|
+
}
|
|
1624
|
+
return {
|
|
1625
|
+
code: raw.code,
|
|
1626
|
+
message: raw.message,
|
|
1627
|
+
hint: raw.hint,
|
|
1628
|
+
fix: raw.fix,
|
|
1629
|
+
fromText: raw.from ?? null,
|
|
1630
|
+
toText: raw.to ?? null,
|
|
1631
|
+
range: lspRange
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
/**
|
|
1635
|
+
* Fallback: parse a plain-text compiler error into a synthetic diagnostic.
|
|
1636
|
+
* Used when the binary does not emit JSON (very old binary or stdin parse error).
|
|
1637
|
+
* TODO: remove when --machine-errors covers all error paths.
|
|
1638
|
+
*/
|
|
1639
|
+
function parsePlainError(stderr, _filePath) {
|
|
1640
|
+
const m = /\b(\d+):\s*(.+)/.exec(stderr);
|
|
1641
|
+
const line = m ? Math.max(0, Number.parseInt(m[1], 10) - 1) : 0;
|
|
1642
|
+
return {
|
|
1643
|
+
code: "C000",
|
|
1644
|
+
message: m ? m[2] : stderr.trim() || "Unknown compiler error",
|
|
1645
|
+
fromText: null,
|
|
1646
|
+
toText: null,
|
|
1647
|
+
range: {
|
|
1648
|
+
start: {
|
|
1649
|
+
line,
|
|
1650
|
+
character: 0
|
|
1651
|
+
},
|
|
1652
|
+
end: {
|
|
1653
|
+
line,
|
|
1654
|
+
character: 0
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
/**
|
|
1660
|
+
* Parse the Rust binary's `--machine-errors` stderr stream into structured
|
|
1661
|
+
* diagnostics. Each JSON line is one error; non-JSON lines are skipped. When no
|
|
1662
|
+
* JSON is present at all, falls back to a single synthetic plain-text diagnostic.
|
|
1663
|
+
*
|
|
1664
|
+
* Pure function — no I/O. Exposed so editors/tests can exercise the mapping
|
|
1665
|
+
* without spawning the compiler.
|
|
1666
|
+
*/
|
|
1667
|
+
function parseMachineErrors(stderr, filePath) {
|
|
1668
|
+
const diagnostics = [];
|
|
1669
|
+
let jsonParsed = false;
|
|
1670
|
+
for (const line of stderr.split("\n")) {
|
|
1671
|
+
const trimmed = line.trim();
|
|
1672
|
+
if (!trimmed?.startsWith("{")) continue;
|
|
1673
|
+
try {
|
|
1674
|
+
const raw = JSON.parse(trimmed);
|
|
1675
|
+
diagnostics.push(toAihuDiagnostic(raw));
|
|
1676
|
+
jsonParsed = true;
|
|
1677
|
+
} catch {}
|
|
1678
|
+
}
|
|
1679
|
+
if (!jsonParsed) diagnostics.push(parsePlainError(stderr, filePath));
|
|
1680
|
+
return diagnostics;
|
|
1681
|
+
}
|
|
1682
|
+
/**
|
|
1683
|
+
* Compile a .aihu source string via stdin, returning structured diagnostics.
|
|
1684
|
+
*/
|
|
1685
|
+
async function compileWithDiagnostics(source, filePath) {
|
|
1686
|
+
const stem = filePath.replace(/\\/g, "/").split("/").pop()?.replace(/\.aihu$/, "") ?? "component";
|
|
1687
|
+
try {
|
|
1688
|
+
const { stdout } = await execFileAsync(binPath, [
|
|
1689
|
+
"--stdin",
|
|
1690
|
+
"--tag",
|
|
1691
|
+
stem,
|
|
1692
|
+
"--path",
|
|
1693
|
+
filePath,
|
|
1694
|
+
"--machine-errors"
|
|
1695
|
+
], {
|
|
1696
|
+
input: source,
|
|
1697
|
+
encoding: "utf8",
|
|
1698
|
+
maxBuffer: 8 * 1024 * 1024
|
|
1699
|
+
});
|
|
1700
|
+
return {
|
|
1701
|
+
code: stdout,
|
|
1702
|
+
diagnostics: []
|
|
1703
|
+
};
|
|
1704
|
+
} catch (err) {
|
|
1705
|
+
return {
|
|
1706
|
+
code: null,
|
|
1707
|
+
diagnostics: parseMachineErrors(err.stderr ?? "", filePath)
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
//#endregion
|
|
1712
|
+
//#region src/core/hover.ts
|
|
1713
|
+
/**
|
|
1714
|
+
* packages/language-server/src/core/hover.ts
|
|
1715
|
+
*
|
|
1716
|
+
* Static hover lookup table for aihu macro keywords.
|
|
1717
|
+
* Used by the LSP server textDocument/hover handler.
|
|
1718
|
+
*
|
|
1719
|
+
* Editor-agnostic: returns Markdown strings + does pure position math. The
|
|
1720
|
+
* connection layer wraps the result in a protocol `Hover`. Clean seam for a
|
|
1721
|
+
* future Volar virtual-code hover provider.
|
|
1722
|
+
*/
|
|
1723
|
+
const HOVER_TABLE = {
|
|
1724
|
+
$prop: [
|
|
1725
|
+
"**aihu macro: `$prop`**",
|
|
1726
|
+
"",
|
|
1727
|
+
"Declares reactive prop signals from parent attributes.",
|
|
1728
|
+
"",
|
|
1729
|
+
"Compiles to:",
|
|
1730
|
+
"```typescript",
|
|
1731
|
+
"const name = computed(() => ctx.attrs.name)",
|
|
1732
|
+
"```",
|
|
1733
|
+
"",
|
|
1734
|
+
"[v2 spec](docs/superpowers/specs/2026-05-05-spec-macro-vocabulary-v2.md)"
|
|
1735
|
+
].join("\n"),
|
|
1736
|
+
$computed: [
|
|
1737
|
+
"**aihu macro: `$computed`**",
|
|
1738
|
+
"",
|
|
1739
|
+
"Declares a derived signal.",
|
|
1740
|
+
"",
|
|
1741
|
+
"Compiles to:",
|
|
1742
|
+
"```typescript",
|
|
1743
|
+
"const name = computed(() => expr)",
|
|
1744
|
+
"```",
|
|
1745
|
+
"",
|
|
1746
|
+
"[v2 spec](docs/superpowers/specs/2026-05-05-spec-macro-vocabulary-v2.md)"
|
|
1747
|
+
].join("\n"),
|
|
1748
|
+
$action: [
|
|
1749
|
+
"**aihu macro: `$action`**",
|
|
1750
|
+
"",
|
|
1751
|
+
"Declares a callable action in component scope.",
|
|
1752
|
+
"",
|
|
1753
|
+
"Compiles to:",
|
|
1754
|
+
"```typescript",
|
|
1755
|
+
"function name(args) { return batch(() => { body }) }",
|
|
1756
|
+
"```",
|
|
1757
|
+
"",
|
|
1758
|
+
"[v2 spec](docs/superpowers/specs/2026-05-05-spec-macro-vocabulary-v2.md)"
|
|
1759
|
+
].join("\n"),
|
|
1760
|
+
$resource: [
|
|
1761
|
+
"**aihu macro: `$resource`**",
|
|
1762
|
+
"",
|
|
1763
|
+
"Declares an async data resource signal.",
|
|
1764
|
+
"",
|
|
1765
|
+
"Compiles to:",
|
|
1766
|
+
"```typescript",
|
|
1767
|
+
"const name = createResource(() => expr)",
|
|
1768
|
+
"```",
|
|
1769
|
+
"",
|
|
1770
|
+
"[v2 spec](docs/superpowers/specs/2026-05-05-spec-macro-vocabulary-v2.md)"
|
|
1771
|
+
].join("\n"),
|
|
1772
|
+
$effect: [
|
|
1773
|
+
"**aihu macro: `$effect`**",
|
|
1774
|
+
"",
|
|
1775
|
+
"Declares a reactive side effect.",
|
|
1776
|
+
"",
|
|
1777
|
+
"Compiles to:",
|
|
1778
|
+
"```typescript",
|
|
1779
|
+
"effect(() => { body })",
|
|
1780
|
+
"```",
|
|
1781
|
+
"",
|
|
1782
|
+
"[v2 spec](docs/superpowers/specs/2026-05-05-spec-macro-vocabulary-v2.md)"
|
|
1783
|
+
].join("\n"),
|
|
1784
|
+
$lifecycle: [
|
|
1785
|
+
"**aihu macro: `$lifecycle`**",
|
|
1786
|
+
"",
|
|
1787
|
+
"Declares lifecycle hooks (mount, dispose, adopt, attributeChange).",
|
|
1788
|
+
"",
|
|
1789
|
+
"Compiles to:",
|
|
1790
|
+
"```typescript",
|
|
1791
|
+
"onMount(() => { ... })",
|
|
1792
|
+
"onCleanup(() => { ... })",
|
|
1793
|
+
"```",
|
|
1794
|
+
"",
|
|
1795
|
+
"[v2 spec](docs/superpowers/specs/2026-05-05-spec-macro-vocabulary-v2.md)"
|
|
1796
|
+
].join("\n"),
|
|
1797
|
+
$if: [
|
|
1798
|
+
"**aihu template directive: `$if` / `{#if}`**",
|
|
1799
|
+
"",
|
|
1800
|
+
"Conditional rendering.",
|
|
1801
|
+
"",
|
|
1802
|
+
"Compiles to:",
|
|
1803
|
+
"```typescript",
|
|
1804
|
+
"branch(condition, () => trueBranch)",
|
|
1805
|
+
"```",
|
|
1806
|
+
"",
|
|
1807
|
+
"[Template spec](docs/superpowers/specs/2026-05-05-spec-macro-vocabulary-v2.md)"
|
|
1808
|
+
].join("\n"),
|
|
1809
|
+
$each: [
|
|
1810
|
+
"**aihu template directive: `$each` / `{#each}`**",
|
|
1811
|
+
"",
|
|
1812
|
+
"List rendering.",
|
|
1813
|
+
"",
|
|
1814
|
+
"Compiles to:",
|
|
1815
|
+
"```typescript",
|
|
1816
|
+
"branch.each(items, (item) => leaf(...))",
|
|
1817
|
+
"```",
|
|
1818
|
+
"",
|
|
1819
|
+
"[Template spec](docs/superpowers/specs/2026-05-05-spec-macro-vocabulary-v2.md)"
|
|
1820
|
+
].join("\n"),
|
|
1821
|
+
$html: [
|
|
1822
|
+
"**aihu template directive: `$html` / `{@html}`**",
|
|
1823
|
+
"",
|
|
1824
|
+
"Raw HTML injection. Use with trusted content only.",
|
|
1825
|
+
"",
|
|
1826
|
+
"Compiles to:",
|
|
1827
|
+
"```typescript",
|
|
1828
|
+
"leaf({ nodeValue: htmlContent })",
|
|
1829
|
+
"```",
|
|
1830
|
+
"",
|
|
1831
|
+
"[Template spec](docs/superpowers/specs/2026-05-05-spec-macro-vocabulary-v2.md)"
|
|
1832
|
+
].join("\n"),
|
|
1833
|
+
$show: [
|
|
1834
|
+
"**aihu template directive: `$show`**",
|
|
1835
|
+
"",
|
|
1836
|
+
"Toggles element visibility without removing it from the DOM.",
|
|
1837
|
+
"",
|
|
1838
|
+
"Compiles to:",
|
|
1839
|
+
"```typescript",
|
|
1840
|
+
"el.style.display = condition ? \"\" : \"none\"",
|
|
1841
|
+
"```",
|
|
1842
|
+
"",
|
|
1843
|
+
"[Template spec](docs/superpowers/specs/2026-05-05-spec-macro-vocabulary-v2.md)"
|
|
1844
|
+
].join("\n"),
|
|
1845
|
+
$on: [
|
|
1846
|
+
"**aihu template directive: `$on`**",
|
|
1847
|
+
"",
|
|
1848
|
+
"Attaches an event listener to a DOM element.",
|
|
1849
|
+
"",
|
|
1850
|
+
"Compiles to:",
|
|
1851
|
+
"```typescript",
|
|
1852
|
+
"element.addEventListener(\"event\", handler)",
|
|
1853
|
+
"```",
|
|
1854
|
+
"",
|
|
1855
|
+
"Usage: `$on.click={handler}`",
|
|
1856
|
+
"",
|
|
1857
|
+
"[Template spec](docs/superpowers/specs/2026-05-05-spec-macro-vocabulary-v2.md)"
|
|
1858
|
+
].join("\n"),
|
|
1859
|
+
$bind: [
|
|
1860
|
+
"**aihu template directive: `$bind`**",
|
|
1861
|
+
"",
|
|
1862
|
+
"Two-way binding between a signal and an element attribute.",
|
|
1863
|
+
"",
|
|
1864
|
+
"Compiles to:",
|
|
1865
|
+
"```typescript",
|
|
1866
|
+
"// two-way binding via signal setter",
|
|
1867
|
+
"el.addEventListener(\"input\", e => setSignal(e.target.value))",
|
|
1868
|
+
"```",
|
|
1869
|
+
"",
|
|
1870
|
+
"Usage: `$bind.value={signal}`",
|
|
1871
|
+
"",
|
|
1872
|
+
"[Template spec](docs/superpowers/specs/2026-05-05-spec-macro-vocabulary-v2.md)"
|
|
1873
|
+
].join("\n"),
|
|
1874
|
+
$emit: [
|
|
1875
|
+
"**aihu macro: `$emit`**",
|
|
1876
|
+
"",
|
|
1877
|
+
"Declares and dispatches typed custom events.",
|
|
1878
|
+
"",
|
|
1879
|
+
"Compiles to:",
|
|
1880
|
+
"```typescript",
|
|
1881
|
+
"this.dispatchEvent(new CustomEvent(name, { detail: payload }))",
|
|
1882
|
+
"```",
|
|
1883
|
+
"",
|
|
1884
|
+
"Usage: `$emit.name(payload)`",
|
|
1885
|
+
"",
|
|
1886
|
+
"[v2 spec](docs/superpowers/specs/2026-05-05-spec-macro-vocabulary-v2.md)"
|
|
1887
|
+
].join("\n")
|
|
1888
|
+
};
|
|
1889
|
+
function getBlockContext(lines, lineIndex) {
|
|
1890
|
+
for (let i = lineIndex; i >= 0; i--) {
|
|
1891
|
+
const trimmed = lines[i].trimStart();
|
|
1892
|
+
if (/^@state\s*\{/.test(trimmed)) return "state";
|
|
1893
|
+
if (/^@template\s*\{/.test(trimmed)) return "template";
|
|
1894
|
+
if (/^@(style|agent|route)\s*\{/.test(trimmed)) return "unknown";
|
|
1895
|
+
}
|
|
1896
|
+
return "unknown";
|
|
1897
|
+
}
|
|
1898
|
+
function getMacroAtPosition(lineText, character) {
|
|
1899
|
+
let m;
|
|
1900
|
+
const namespacedRe = /\$(on|bind)(?:[.:][A-Za-z_$][\w$]*)?/g;
|
|
1901
|
+
while ((m = namespacedRe.exec(lineText)) !== null) if (character >= m.index && character <= m.index + m[0].length) return `$${m[1]}`;
|
|
1902
|
+
const bareRe = /\$(prop|computed|action|resource|effect|lifecycle|emit|if|each|html|show)\b/g;
|
|
1903
|
+
while ((m = bareRe.exec(lineText)) !== null) if (character >= m.index && character <= m.index + m[0].length) return `$${m[1]}`;
|
|
1904
|
+
const blockRe = /\{(?:#(if|each)|@(html))\b/g;
|
|
1905
|
+
while ((m = blockRe.exec(lineText)) !== null) if (character >= m.index && character <= m.index + m[0].length) return `$${m[1] ?? m[2]}`;
|
|
1906
|
+
return null;
|
|
1907
|
+
}
|
|
1908
|
+
function getHoverContent(macro) {
|
|
1909
|
+
return HOVER_TABLE[macro] ?? null;
|
|
1910
|
+
}
|
|
1911
|
+
//#endregion
|
|
1912
|
+
//#region src/server.ts
|
|
1913
|
+
/**
|
|
1914
|
+
* packages/language-server/src/server.ts
|
|
1915
|
+
*
|
|
1916
|
+
* The aihu cross-editor Language Server — transport/connection layer.
|
|
1917
|
+
*
|
|
1918
|
+
* Started as an out-of-process Node.js child by any LSP client (VS Code via
|
|
1919
|
+
* vscode-languageclient, Neovim, Helix, …) over stdio. The runnable binary
|
|
1920
|
+
* (`aihu-language-server`) is `src/bin.ts`, which calls `startServer()`.
|
|
1921
|
+
*
|
|
1922
|
+
* Features implemented:
|
|
1923
|
+
* - Diagnostics: shell out to aihu-compile --machine-errors (debounced 300ms)
|
|
1924
|
+
* - Code actions: QuickFix for C440-C444 via the migrate() codemod
|
|
1925
|
+
* - Hover: static lookup table for 13 macro keywords
|
|
1926
|
+
* - Completion: 9 macro-kind snippets ($ trigger) + 5 block names (@ trigger)
|
|
1927
|
+
*
|
|
1928
|
+
* All feature logic lives in ./core (editor-agnostic). This file only wires the
|
|
1929
|
+
* core functions onto the LSP connection — the clean seam for a future Volar
|
|
1930
|
+
* adoption (arch-4 §2.6/§2.7).
|
|
1931
|
+
*/
|
|
1932
|
+
/**
|
|
1933
|
+
* Build and wire the LSP server onto a connection, returning it without calling
|
|
1934
|
+
* `.listen()`. Exposed so tests / alternative transports can drive the server.
|
|
1935
|
+
* The default `createConnection(ProposedFeatures.all)` reads stdio.
|
|
1936
|
+
*/
|
|
1937
|
+
function createServer(connection = createConnection(ProposedFeatures.all)) {
|
|
1938
|
+
const documents = new TextDocuments(TextDocument);
|
|
1939
|
+
connection.onInitialize((_params) => {
|
|
1940
|
+
connection.console.log("Aihu LSP server started");
|
|
1941
|
+
return { capabilities: {
|
|
1942
|
+
textDocumentSync: TextDocumentSyncKind.Incremental,
|
|
1943
|
+
hoverProvider: true,
|
|
1944
|
+
completionProvider: { triggerCharacters: ["$", "@"] },
|
|
1945
|
+
codeActionProvider: { codeActionKinds: [CodeActionKind.QuickFix] }
|
|
1946
|
+
} };
|
|
1947
|
+
});
|
|
1948
|
+
const pendingValidations = /* @__PURE__ */ new Map();
|
|
1949
|
+
function scheduleValidation(doc) {
|
|
1950
|
+
const uri = doc.uri;
|
|
1951
|
+
const existing = pendingValidations.get(uri);
|
|
1952
|
+
if (existing !== void 0) clearTimeout(existing);
|
|
1953
|
+
const handle = setTimeout(() => {
|
|
1954
|
+
pendingValidations.delete(uri);
|
|
1955
|
+
validateDocument(doc);
|
|
1956
|
+
}, 300);
|
|
1957
|
+
pendingValidations.set(uri, handle);
|
|
1958
|
+
}
|
|
1959
|
+
async function validateDocument(doc) {
|
|
1960
|
+
const filePath = doc.uri.replace(/^file:\/\/\/([A-Za-z]:)/, "$1").replace(/^file:\/\//, "");
|
|
1961
|
+
let result;
|
|
1962
|
+
try {
|
|
1963
|
+
result = await compileWithDiagnostics(doc.getText(), filePath);
|
|
1964
|
+
} catch (err) {
|
|
1965
|
+
connection.console.error(`Aihu LSP: validation crashed for ${doc.uri}: ${String(err)}`);
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1968
|
+
const diagnostics = result.diagnostics.map(toDiagnostic);
|
|
1969
|
+
connection.sendDiagnostics({
|
|
1970
|
+
uri: doc.uri,
|
|
1971
|
+
diagnostics
|
|
1972
|
+
});
|
|
1973
|
+
}
|
|
1974
|
+
function toDiagnostic(d) {
|
|
1975
|
+
return {
|
|
1976
|
+
range: d.range ? {
|
|
1977
|
+
start: d.range.start,
|
|
1978
|
+
end: d.range.end
|
|
1979
|
+
} : {
|
|
1980
|
+
start: {
|
|
1981
|
+
line: 0,
|
|
1982
|
+
character: 0
|
|
1983
|
+
},
|
|
1984
|
+
end: {
|
|
1985
|
+
line: 0,
|
|
1986
|
+
character: 0
|
|
1987
|
+
}
|
|
1988
|
+
},
|
|
1989
|
+
message: d.message,
|
|
1990
|
+
severity: DiagnosticSeverity.Error,
|
|
1991
|
+
code: d.code,
|
|
1992
|
+
source: "aihu-compile"
|
|
1993
|
+
};
|
|
1994
|
+
}
|
|
1995
|
+
documents.onDidOpen((e) => scheduleValidation(e.document));
|
|
1996
|
+
documents.onDidChangeContent((e) => scheduleValidation(e.document));
|
|
1997
|
+
documents.onDidClose((e) => {
|
|
1998
|
+
const uri = e.document.uri;
|
|
1999
|
+
const pending = pendingValidations.get(uri);
|
|
2000
|
+
if (pending !== void 0) {
|
|
2001
|
+
clearTimeout(pending);
|
|
2002
|
+
pendingValidations.delete(uri);
|
|
2003
|
+
}
|
|
2004
|
+
connection.sendDiagnostics({
|
|
2005
|
+
uri,
|
|
2006
|
+
diagnostics: []
|
|
2007
|
+
});
|
|
2008
|
+
});
|
|
2009
|
+
connection.onCodeAction((params) => {
|
|
2010
|
+
const doc = documents.get(params.textDocument.uri);
|
|
2011
|
+
if (!doc) return [];
|
|
2012
|
+
const migrateDiags = params.context.diagnostics.filter((d) => typeof d.code === "string" && MIGRATE_CODES.has(d.code));
|
|
2013
|
+
if (migrateDiags.length === 0) return [];
|
|
2014
|
+
const fix = buildMigrateFix(doc.getText());
|
|
2015
|
+
if (!fix) {
|
|
2016
|
+
connection.console.error("Aihu LSP: migrate() threw — no code action offered");
|
|
2017
|
+
return [];
|
|
2018
|
+
}
|
|
2019
|
+
for (const w of fix.warnings) connection.console.warn(`Aihu codemod warning: ${w}`);
|
|
2020
|
+
const fullRange = {
|
|
2021
|
+
start: {
|
|
2022
|
+
line: 0,
|
|
2023
|
+
character: 0
|
|
2024
|
+
},
|
|
2025
|
+
end: {
|
|
2026
|
+
line: doc.lineCount,
|
|
2027
|
+
character: 0
|
|
2028
|
+
}
|
|
2029
|
+
};
|
|
2030
|
+
const edit = { changes: { [params.textDocument.uri]: [TextEdit.replace(fullRange, fix.rewritten)] } };
|
|
2031
|
+
return [{
|
|
2032
|
+
title: fix.title,
|
|
2033
|
+
kind: CodeActionKind.QuickFix,
|
|
2034
|
+
diagnostics: migrateDiags,
|
|
2035
|
+
edit,
|
|
2036
|
+
isPreferred: true
|
|
2037
|
+
}];
|
|
2038
|
+
});
|
|
2039
|
+
connection.onHover((params) => {
|
|
2040
|
+
const doc = documents.get(params.textDocument.uri);
|
|
2041
|
+
if (!doc) return null;
|
|
2042
|
+
const pos = params.position;
|
|
2043
|
+
const macro = getMacroAtPosition(doc.getText({
|
|
2044
|
+
start: {
|
|
2045
|
+
line: pos.line,
|
|
2046
|
+
character: 0
|
|
2047
|
+
},
|
|
2048
|
+
end: {
|
|
2049
|
+
line: pos.line,
|
|
2050
|
+
character: Number.MAX_SAFE_INTEGER
|
|
2051
|
+
}
|
|
2052
|
+
}), pos.character);
|
|
2053
|
+
if (!macro) return null;
|
|
2054
|
+
getBlockContext(doc.getText().split("\n"), pos.line);
|
|
2055
|
+
const content = getHoverContent(macro);
|
|
2056
|
+
if (!content) return null;
|
|
2057
|
+
return { contents: {
|
|
2058
|
+
kind: MarkupKind.Markdown,
|
|
2059
|
+
value: content
|
|
2060
|
+
} };
|
|
2061
|
+
});
|
|
2062
|
+
connection.onCompletion((params) => {
|
|
2063
|
+
const doc = documents.get(params.textDocument.uri);
|
|
2064
|
+
if (!doc) return null;
|
|
2065
|
+
const pos = params.position;
|
|
2066
|
+
const lineText = doc.getText({
|
|
2067
|
+
start: {
|
|
2068
|
+
line: pos.line,
|
|
2069
|
+
character: 0
|
|
2070
|
+
},
|
|
2071
|
+
end: {
|
|
2072
|
+
line: pos.line,
|
|
2073
|
+
character: pos.character
|
|
2074
|
+
}
|
|
2075
|
+
});
|
|
2076
|
+
const triggerChar = params.context?.triggerCharacter;
|
|
2077
|
+
if (triggerChar === "$" || lineText.trimEnd().endsWith("$")) {
|
|
2078
|
+
if (getBlockContext(doc.getText().split("\n"), pos.line) === "state") return CompletionList.create(STATE_MACRO_COMPLETIONS, false);
|
|
2079
|
+
const templateItems = STATE_MACRO_COMPLETIONS.filter((c) => [
|
|
2080
|
+
"$on",
|
|
2081
|
+
"$bind",
|
|
2082
|
+
"$show"
|
|
2083
|
+
].includes(c.label));
|
|
2084
|
+
return CompletionList.create(templateItems, false);
|
|
2085
|
+
}
|
|
2086
|
+
if (triggerChar === "@" || /^\s*@$/.test(lineText)) return CompletionList.create(BLOCK_COMPLETIONS, false);
|
|
2087
|
+
return null;
|
|
2088
|
+
});
|
|
2089
|
+
documents.listen(connection);
|
|
2090
|
+
return connection;
|
|
2091
|
+
}
|
|
2092
|
+
/**
|
|
2093
|
+
* Start the language server on stdio and begin listening. This is the entry
|
|
2094
|
+
* point the `aihu-language-server` bin calls.
|
|
2095
|
+
*
|
|
2096
|
+
* Bind the connection to `process.stdin`/`process.stdout` explicitly so the
|
|
2097
|
+
* binary works when launched directly (e.g. `{ command: "aihu-language-server" }`
|
|
2098
|
+
* in Neovim/Helix). vscode-languageserver's bare `createConnection` otherwise
|
|
2099
|
+
* requires a `--stdio` / `--node-ipc` CLI flag and throws without one.
|
|
2100
|
+
*/
|
|
2101
|
+
function startServer() {
|
|
2102
|
+
const connection = createServer(createConnection(ProposedFeatures.all, process.stdin, process.stdout));
|
|
2103
|
+
connection.listen();
|
|
2104
|
+
return connection;
|
|
2105
|
+
}
|
|
2106
|
+
//#endregion
|
|
2107
|
+
//#region src/bin.ts
|
|
2108
|
+
/**
|
|
2109
|
+
* packages/language-server/src/bin.ts
|
|
2110
|
+
*
|
|
2111
|
+
* Runnable entry for the `aihu-language-server` binary. Editors launch this over
|
|
2112
|
+
* stdio (e.g. VS Code via vscode-languageclient, or any LSP-aware editor with a
|
|
2113
|
+
* `{ command: "aihu-language-server" }` server spec).
|
|
2114
|
+
*
|
|
2115
|
+
* The rolldown build prepends a `#!/usr/bin/env node` shebang to dist/bin.js.
|
|
2116
|
+
*/
|
|
2117
|
+
startServer();
|
|
2118
|
+
//#endregion
|
|
2119
|
+
export {};
|