@vesk/compiler 0.1.1 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/api-routes.d.ts +6 -1
  2. package/dist/api-routes.d.ts.map +1 -1
  3. package/dist/api-routes.js +52 -7
  4. package/dist/client-codegen.d.ts +15 -0
  5. package/dist/client-codegen.d.ts.map +1 -1
  6. package/dist/client-codegen.js +73 -10
  7. package/dist/config.d.ts +2 -0
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +25 -0
  10. package/dist/errors.d.ts.map +1 -1
  11. package/dist/errors.js +33 -4
  12. package/dist/index.d.ts +3 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +2 -0
  15. package/dist/ir-generator.d.ts.map +1 -1
  16. package/dist/ir-generator.js +109 -1
  17. package/dist/md-inline.d.ts +17 -0
  18. package/dist/md-inline.d.ts.map +1 -0
  19. package/dist/md-inline.js +149 -0
  20. package/dist/middleware.d.ts.map +1 -1
  21. package/dist/middleware.js +42 -7
  22. package/dist/router.d.ts +2 -0
  23. package/dist/router.d.ts.map +1 -1
  24. package/dist/router.js +48 -3
  25. package/dist/server-codegen.d.ts +1 -1
  26. package/dist/server-codegen.d.ts.map +1 -1
  27. package/dist/server-codegen.js +1 -1
  28. package/dist/server-jsgen.d.ts.map +1 -1
  29. package/dist/server-jsgen.js +4 -3
  30. package/dist/server-render.d.ts.map +1 -1
  31. package/dist/server-render.js +16 -8
  32. package/dist/server-utils.d.ts +31 -0
  33. package/dist/server-utils.d.ts.map +1 -1
  34. package/dist/server-utils.js +478 -52
  35. package/dist/ssr-store.d.ts +1 -1
  36. package/dist/ssr-store.d.ts.map +1 -1
  37. package/dist/ssr-store.js +3 -0
  38. package/dist/typecheck.d.ts +2 -0
  39. package/dist/typecheck.d.ts.map +1 -1
  40. package/dist/typecheck.js +452 -29
  41. package/dist/types.d.ts +4 -129
  42. package/dist/types.d.ts.map +1 -1
  43. package/dist/types.js +1 -0
  44. package/dist/vesk-plugin.d.ts +2 -0
  45. package/dist/vesk-plugin.d.ts.map +1 -1
  46. package/dist/vesk-plugin.js +152 -0
  47. package/dist/vsk-tsx.d.ts +62 -0
  48. package/dist/vsk-tsx.d.ts.map +1 -1
  49. package/dist/vsk-tsx.js +844 -362
  50. package/package.json +2 -1
  51. package/dist/package.json +0 -15
package/dist/vsk-tsx.js CHANGED
@@ -1,141 +1,49 @@
1
1
  import { parse } from '@vesk/compiler/src/parser';
2
2
  import { getPropsType } from '@vesk/compiler/src/ir-generator';
3
- import { skipWhitespace, findBalancedEnd, splitTopLevel, startsWithIdentifier, stripDeclKeyword, stripTrailingSemicolons, isWhitespaceChar, isIdentStart, isIdentChar, htmlTagEnd, collapseNewlineWhitespace, } from '@vesk/compiler/src/scan';
3
+ import { skipWhitespace, findBalancedEnd, startsWithIdentifier, stripDeclKeyword, stripTrailingSemicolons, isWhitespaceChar, isIdentStart, isIdentChar, htmlTagEnd, } from '@vesk/compiler/src/scan';
4
4
  import { containsIdentifier, isIdentifierImported } from '@vesk/compiler/src/tokens';
5
- function applyEdits(source, edits) {
6
- const sorted = edits.sort((a, b) => b.start - a.start || b.end - a.end);
7
- let out = source;
8
- for (const e of sorted) {
9
- out = out.slice(0, e.start) + e.text + out.slice(e.end);
10
- }
11
- return out;
12
- }
13
- /**
14
- * Collects edit operations that rewrite `&[...] = track<...>(...)`
15
- * declarations into plain typed `let` declarations, using the parsed AST so
16
- * nested generics (`track<Map<string, number>>(...)`), type annotations and
17
- * nested statement positions are handled without regex. `wrapped` is the
18
- * fake component source (offsets inside it), `prefixLen` the distance from
19
- * the start of `wrapped` to the start of the real body text.
20
- */
21
- function collectTrackDeclEdits(wrapped, ast, prefixLen) {
22
- const edits = [];
23
- function walk(stmts) {
24
- for (const stmt of stmts) {
25
- if (!stmt)
26
- continue;
27
- if (stmt.type === 'VariableDeclaration') {
28
- const decls = stmt.declarations || [];
29
- for (let di = 0; di < decls.length; di++) {
30
- const decl = decls[di];
31
- const id = decl.id;
32
- if (!id || id.type !== 'ArrayPattern' || id.lazy !== true)
33
- continue;
34
- if (!decl.init)
35
- continue;
36
- const names = (id.elements || [])
37
- .map((el) => (el ? el.name : null))
38
- .filter((n) => typeof n === 'string');
39
- const initText = wrapped.slice(decl.init.start, decl.init.end);
40
- // The first declarator's replacement must also absorb the statement's
41
- // `const`/`let`/`var` keyword, which starts at `stmt.start`, not at
42
- // the declarator's start.
43
- const start = di === 0 ? stmt.start : decl.start;
44
- const end = decl.end;
45
- // Don't duplicate the statement terminator already present after the
46
- // declaration (avoids `let x = ...;;`).
47
- const terminator = wrapped[end] === ';' ? '' : ';';
48
- if (names.length === 0) {
49
- edits.push({ start: start - prefixLen, end: end - prefixLen, text: `let _ = ${initText}${terminator}` });
50
- continue;
51
- }
52
- const annotation = id.typeAnnotation?.typeAnnotation
53
- ? wrapped.slice(id.typeAnnotation.typeAnnotation.start, id.typeAnnotation.typeAnnotation.end).trim()
54
- : decl.init.typeArguments
55
- ? wrapped.slice(decl.init.typeArguments.start + 1, decl.init.typeArguments.end - 1).trim()
56
- : '';
57
- const first = names[0];
58
- let replacement;
59
- if (annotation) {
60
- replacement = `let ${first}: ${annotation} = (${initText} as unknown as ${annotation});`;
61
- }
62
- else {
63
- replacement = `let ${first}: any = ${initText};`;
64
- }
65
- for (let n = 1; n < names.length; n++) {
66
- replacement += ` let ${names[n]}: any = ${first};`;
67
- }
68
- // The last emitted statement sits directly before `end`; drop its
69
- // terminator when the source already provides one (avoids `;;`).
70
- if (wrapped[end] === ';')
71
- replacement = replacement.slice(0, -1);
72
- edits.push({ start: start - prefixLen, end: end - prefixLen, text: replacement });
73
- }
74
- continue;
75
- }
76
- const kids = [];
77
- if (stmt.consequent)
78
- kids.push(stmt.consequent);
79
- if (stmt.alternate)
80
- kids.push(stmt.alternate);
81
- if (Array.isArray(stmt.body))
82
- kids.push(...stmt.body);
83
- else if (stmt.body && stmt.body.type)
84
- kids.push(stmt.body);
85
- if (Array.isArray(stmt.cases)) {
86
- for (const c of stmt.cases)
87
- if (Array.isArray(c.consequent))
88
- kids.push(...c.consequent);
89
- }
90
- if (stmt.handler)
91
- kids.push(stmt.handler.body);
92
- if (stmt.finalizer)
93
- kids.push(stmt.finalizer.body);
94
- walk(kids);
95
- }
96
- }
97
- walk(ast.body);
98
- return edits;
99
- }
100
- const REWRITER_WRAPPER = 'component __VskRewriter() { ';
101
- const REWRITER_WRAPPER_LEN = REWRITER_WRAPPER.length;
102
- function rewriteTrackDecls(text) {
103
- if (!text.includes('&['))
104
- return text;
105
- const wrapped = REWRITER_WRAPPER + text + ' }';
106
- let ast;
107
- try {
108
- ast = parse(wrapped);
5
+ const FULL_DATA = {
6
+ verification: true,
7
+ completion: true,
8
+ semantic: true,
9
+ navigation: true,
10
+ structure: true,
11
+ format: true,
12
+ };
13
+ const REACTIVE_DATA = { ...FULL_DATA, customData: { vesk: { reactive: true } } };
14
+ class VskGen {
15
+ code = '';
16
+ mappings = [];
17
+ styleRegions = [];
18
+ /** Counts track-decl rewrites so typedCells cell names stay unique within a codegen run. */
19
+ cellCount = 0;
20
+ /** Appends `text`; when a source range is given, records a mapping. */
21
+ add(text, srcStart, srcEnd, data = FULL_DATA) {
22
+ const start = this.code.length;
23
+ this.code += text;
24
+ if (text.length > 0 && srcStart !== undefined && srcEnd !== undefined && srcEnd > srcStart) {
25
+ this.mappings.push({
26
+ sourceOffsets: [srcStart],
27
+ generatedOffsets: [start],
28
+ lengths: [srcEnd - srcStart],
29
+ generatedLengths: [text.length],
30
+ data,
31
+ });
32
+ }
109
33
  }
110
- catch {
111
- return text;
34
+ /** Appends synthetic text with no source mapping. */
35
+ addRaw(text) {
36
+ this.code += text;
112
37
  }
113
- const edits = collectTrackDeclEdits(wrapped, ast, REWRITER_WRAPPER_LEN);
114
- if (edits.length === 0)
115
- return text;
116
- return applyEdits(text, edits);
117
- }
118
- function stripStyleBlocks(text) {
119
- const out = [];
120
- let i = 0;
121
- while (i < text.length) {
122
- if (text[i] === '<' && text.slice(i, i + 6) === '<style') {
123
- const next = i + 6;
124
- if (next >= text.length || isWhitespaceChar(text[next]) || text[next] === '>' || text[next] === '/') {
125
- const openEnd = htmlTagEnd(text, i);
126
- if (openEnd !== -1) {
127
- const closeIdx = text.indexOf('</style>', openEnd);
128
- if (closeIdx !== -1) {
129
- i = closeIdx + '</style>'.length;
130
- continue;
131
- }
132
- }
133
- }
38
+ prepend(text) {
39
+ this.code = text + this.code;
40
+ for (const m of this.mappings) {
41
+ m.generatedOffsets = m.generatedOffsets.map((o) => o + text.length);
134
42
  }
135
- out.push(text[i]);
136
- i++;
137
43
  }
138
- return out.join('');
44
+ recordStyle(start, end, content) {
45
+ this.styleRegions.push({ start, end, content });
46
+ }
139
47
  }
140
48
  function getComponents(ast) {
141
49
  const out = [];
@@ -183,15 +91,394 @@ function getJSXTagName(el) {
183
91
  return name.namespace.name + ':' + name.name.name;
184
92
  return null;
185
93
  }
186
- function tsxBlock(source, body, indent) {
94
+ /** Collapses newline+whitespace runs to single spaces and records the raw index of every kept character. */
95
+ function collapseWithMap(value) {
96
+ let out = '';
97
+ const map = [];
98
+ let i = 0;
99
+ while (i < value.length) {
100
+ const c = value[i];
101
+ if (c === '\n') {
102
+ out += ' ';
103
+ map.push(i);
104
+ while (i < value.length && isWhitespaceChar(value[i]))
105
+ i++;
106
+ continue;
107
+ }
108
+ out += c;
109
+ map.push(i);
110
+ i++;
111
+ }
112
+ return { text: out, map };
113
+ }
114
+ function isStatementMode(node) {
115
+ const stmts = node.body?.body ?? [];
116
+ return !(stmts.length === 1 && stmts[0].type === 'ReturnStatement');
117
+ }
118
+ /** Index of the first whole-word occurrence of `word` in `text`, or -1. */
119
+ function findFirstKeyword(text, word) {
120
+ let i = 0;
121
+ while (i < text.length) {
122
+ if (isIdentStart(text[i])) {
123
+ let j = i + 1;
124
+ while (j < text.length && isIdentChar(text[j]))
125
+ j++;
126
+ if (text.slice(i, j) === word)
127
+ return i;
128
+ i = j;
129
+ continue;
130
+ }
131
+ i++;
132
+ }
133
+ return -1;
134
+ }
135
+ /**
136
+ * Removes a trailing `client` keyword (plus surrounding whitespace) from a
137
+ * component header, replacing it with a single trailing space. Returns the
138
+ * kept text and the length of the source range it covers.
139
+ */
140
+ function stripTrailingClientRange(text) {
141
+ let end = text.length;
142
+ while (end > 0 && isWhitespaceChar(text[end - 1]))
143
+ end--;
144
+ let wordStart = end;
145
+ while (wordStart > 0 && isIdentChar(text[wordStart - 1]))
146
+ wordStart--;
147
+ if (text.slice(wordStart, end) === 'client') {
148
+ return { text: text.slice(0, wordStart).trimEnd() + ' ', end: wordStart };
149
+ }
150
+ return { text, end: text.length };
151
+ }
152
+ /** Index of the first depth-0 whole-word occurrence of `sep` in `text`, or -1 (splitTopLevel semantics). */
153
+ function findTopLevelSep(text, sep) {
154
+ let depth = 0;
155
+ let i = 0;
156
+ while (i < text.length) {
157
+ const c = text[i];
158
+ if (c === '"' || c === "'" || c === '`') {
159
+ let j = i + 1;
160
+ while (j < text.length) {
161
+ const q = text[j];
162
+ if (q === '\\') {
163
+ j += 2;
164
+ continue;
165
+ }
166
+ if (q === c) {
167
+ j++;
168
+ break;
169
+ }
170
+ j++;
171
+ }
172
+ i = j;
173
+ continue;
174
+ }
175
+ if (c === '/' && (text[i + 1] === '/' || text[i + 1] === '*')) {
176
+ let j = i + 2;
177
+ if (text[i + 1] === '/') {
178
+ while (j < text.length && text[j] !== '\n')
179
+ j++;
180
+ }
181
+ else {
182
+ while (j < text.length && !(text[j] === '*' && text[j + 1] === '/'))
183
+ j++;
184
+ j = Math.min(j + 2, text.length);
185
+ }
186
+ i = j;
187
+ continue;
188
+ }
189
+ if (c === '(' || c === '[' || c === '{')
190
+ depth++;
191
+ else if (c === ')' || c === ']' || c === '}') {
192
+ if (depth > 0)
193
+ depth--;
194
+ }
195
+ else if (depth === 0 && c === sep[0] && text.slice(i, i + sep.length) === sep) {
196
+ const before = i === 0 ? ' ' : text[i - 1];
197
+ const after = i + sep.length < text.length ? text[i + sep.length] : ' ';
198
+ if (!isIdentChar(before) && !isIdentChar(after))
199
+ return i;
200
+ }
201
+ i++;
202
+ }
203
+ return -1;
204
+ }
205
+ function trailingWsLen(text) {
206
+ let n = 0;
207
+ while (n < text.length && isWhitespaceChar(text[text.length - 1 - n]))
208
+ n++;
209
+ return n;
210
+ }
211
+ function leadingWsLen(text) {
212
+ let n = 0;
213
+ while (n < text.length && isWhitespaceChar(text[n]))
214
+ n++;
215
+ return n;
216
+ }
217
+ function isTrackDeclStatement(stmt) {
218
+ if (!stmt || stmt.type !== 'VariableDeclaration')
219
+ return false;
220
+ return (stmt.declarations || []).some((d) => d && d.id && d.id.type === 'ArrayPattern' && d.id.lazy === true && d.init);
221
+ }
222
+ /**
223
+ * Emits the rewrite for a `const &[count] = track(0)` declarator. Mirrors
224
+ * the exact text produced by the previous string-level rewrite: the first
225
+ * declarator absorbs the statement's `const`/`let`/`var` keyword, the
226
+ * replacement always ends with `;` unless the source statement already
227
+ * provides a terminator (checked on the emitted statement text, exactly
228
+ * like the old `wrapped[end] === ';'` rule).
229
+ */
230
+ function emitTrackDeclStatement(g, source, stmt, indent, isLast, opts) {
231
+ const fullText = stripTrailingSemicolons(source.slice(stmt.start, stmt.end).trimEnd());
232
+ const text = fullText + (!fullText.endsWith('}') && !isLast ? ';' : '');
233
+ const decls = stmt.declarations || [];
234
+ g.add(indent);
235
+ let prevEnd = stmt.start;
236
+ for (let di = 0; di < decls.length; di++) {
237
+ const decl = decls[di];
238
+ const id = decl.id;
239
+ const isLazy = id && id.type === 'ArrayPattern' && id.lazy === true && decl.init;
240
+ if (!isLazy) {
241
+ g.addRaw(source.slice(prevEnd, decl.start));
242
+ g.add(source.slice(decl.start, decl.end), decl.start, decl.end);
243
+ prevEnd = decl.end;
244
+ continue;
245
+ }
246
+ if (di > 0)
247
+ g.addRaw(source.slice(prevEnd, decl.start));
248
+ const ch = text[decl.end - stmt.start] ?? ' ';
249
+ const elements = (id.elements || []);
250
+ const names = elements
251
+ .map((el) => (el ? el.name : null))
252
+ .filter((n) => typeof n === 'string');
253
+ const initText = source.slice(decl.init.start, decl.init.end);
254
+ if (names.length === 0) {
255
+ g.add('let _ = ');
256
+ g.add(initText, decl.init.start, decl.init.end);
257
+ if (ch !== ';')
258
+ g.addRaw(';');
259
+ prevEnd = decl.end;
260
+ continue;
261
+ }
262
+ const annotation = id.typeAnnotation?.typeAnnotation
263
+ ? source.slice(id.typeAnnotation.typeAnnotation.start, id.typeAnnotation.typeAnnotation.end).trim()
264
+ : decl.init.typeArguments
265
+ ? source.slice(decl.init.typeArguments.start + 1, decl.init.typeArguments.end - 1).trim()
266
+ : '';
267
+ const annStart = id.typeAnnotation?.typeAnnotation
268
+ ? id.typeAnnotation.typeAnnotation.start
269
+ : decl.init.typeArguments
270
+ ? decl.init.typeArguments.start + 1
271
+ : -1;
272
+ const annEnd = id.typeAnnotation?.typeAnnotation
273
+ ? id.typeAnnotation.typeAnnotation.end
274
+ : decl.init.typeArguments
275
+ ? decl.init.typeArguments.end - 1
276
+ : -1;
277
+ const first = names[0];
278
+ const firstRange = [elements[0].start, elements[0].end];
279
+ const dropLastSemi = ch === ';';
280
+ if (opts.typedCells) {
281
+ const cellName = g.cellCount === 0 ? '__cell' : `__cell${g.cellCount}`;
282
+ g.cellCount++;
283
+ // The cell holds a Tracked<T>, NOT a T — leave its type to inference
284
+ // from `track<T>(init)` so raw-cell aliases stay correctly typed too.
285
+ // Annotating it with the value annotation made `&[v, cell]` emit
286
+ // `cell: T = <Tracked>` (unsound), and broke passing `cell` to APIs
287
+ // expecting `Tracked<unknown>` (e.g. useFetch `into`).
288
+ g.add(`const ${cellName} = `);
289
+ g.add(initText, decl.init.start, decl.init.end);
290
+ g.add(';');
291
+ // First binding: the VALUE read from the cell.
292
+ g.add(' let ');
293
+ g.add(first, firstRange[0], firstRange[1], REACTIVE_DATA);
294
+ if (annotation) {
295
+ g.add(': ');
296
+ g.add(annotation, annStart, annEnd);
297
+ }
298
+ g.add(` = ${cellName}.get();`);
299
+ // Raw-cell aliases: inferred Tracked<T>, never the value annotation.
300
+ for (let n = 1; n < names.length; n++) {
301
+ g.add(' let ');
302
+ g.add(names[n], elements[n].start, elements[n].end, REACTIVE_DATA);
303
+ g.add(` = ${cellName};`);
304
+ }
305
+ if (dropLastSemi)
306
+ g.code = g.code.slice(0, -1);
307
+ }
308
+ else {
309
+ if (annotation) {
310
+ g.add('let ');
311
+ g.add(first, firstRange[0], firstRange[1], REACTIVE_DATA);
312
+ g.add(': ');
313
+ g.add(annotation, annStart, annEnd);
314
+ g.add(' = (');
315
+ g.add(initText, decl.init.start, decl.init.end);
316
+ g.add(' as unknown as ');
317
+ g.add(annotation, annStart, annEnd);
318
+ g.add(');');
319
+ }
320
+ else {
321
+ g.add('let ');
322
+ g.add(first, firstRange[0], firstRange[1], REACTIVE_DATA);
323
+ g.add(': any = ');
324
+ g.add(initText, decl.init.start, decl.init.end);
325
+ g.add(';');
326
+ }
327
+ for (let n = 1; n < names.length; n++) {
328
+ g.add(' let ');
329
+ g.add(names[n], elements[n].start, elements[n].end, REACTIVE_DATA);
330
+ g.add(': any = ');
331
+ g.add(first, firstRange[0], firstRange[1]);
332
+ g.add(';');
333
+ if (n === names.length - 1 && dropLastSemi)
334
+ g.code = g.code.slice(0, -1);
335
+ }
336
+ if (names.length === 1 && dropLastSemi)
337
+ g.code = g.code.slice(0, -1);
338
+ }
339
+ prevEnd = decl.end;
340
+ }
341
+ if (!isLast)
342
+ g.addRaw(';');
343
+ }
344
+ function emitBlock(g, source, body, indent, opts) {
187
345
  const stmts = body && body.type === 'BlockStatement'
188
346
  ? body.body
189
347
  : body ? (Array.isArray(body) ? body : [body]) : [];
190
- const inner = tsxEmitBody(source, stmts, indent + ' ');
191
- return `{\n${inner}\n${indent}}`;
348
+ g.addRaw('{\n');
349
+ emitBody(g, source, stmts, indent + ' ', opts);
350
+ g.addRaw('\n');
351
+ g.add(indent);
352
+ g.addRaw('}');
192
353
  }
193
- function tsxEmitJSXChildren(source, children) {
194
- const parts = [];
354
+ function emitJSXAttr(g, source, attr) {
355
+ if (attr.type === 'JSXSpreadAttribute') {
356
+ g.add(source.slice(attr.start, attr.end), attr.start, attr.end);
357
+ return;
358
+ }
359
+ const name = attr.name;
360
+ g.add(source.slice(name.start, name.end), name.start, name.end);
361
+ const v = attr.value;
362
+ if (!v)
363
+ return;
364
+ g.add(source.slice(name.end, v.start), name.end, v.start);
365
+ if (v.type === 'JSXExpressionContainer') {
366
+ g.add(source.slice(v.start, v.expression.start), v.start, v.expression.start);
367
+ g.add(source.slice(v.expression.start, v.expression.end), v.expression.start, v.expression.end);
368
+ g.add(source.slice(v.expression.end, v.end), v.expression.end, v.end);
369
+ }
370
+ else {
371
+ g.add(source.slice(v.start, v.end), v.start, v.end);
372
+ }
373
+ }
374
+ function emitJSXElement(g, source, el, opts) {
375
+ const op = el.openingElement;
376
+ const name = op.name;
377
+ let prev;
378
+ if (name) {
379
+ g.add(source.slice(op.start, name.start), op.start, name.start);
380
+ g.add(source.slice(name.start, name.end), name.start, name.end);
381
+ prev = name.end;
382
+ }
383
+ else {
384
+ prev = op.start + 1;
385
+ }
386
+ for (const attr of op.attributes ?? []) {
387
+ g.add(source.slice(prev, attr.start), prev, attr.start);
388
+ emitJSXAttr(g, source, attr);
389
+ prev = attr.end;
390
+ }
391
+ const tail = source.slice(prev, op.end);
392
+ if (op.selfClosing && !tail.includes('/')) {
393
+ const gt = tail.lastIndexOf('>');
394
+ const head = gt === -1 ? tail : tail.slice(0, gt);
395
+ if (head)
396
+ g.add(head, prev, prev + head.length);
397
+ g.addRaw('/>');
398
+ }
399
+ else {
400
+ g.add(tail, prev, op.end);
401
+ }
402
+ if (op.selfClosing)
403
+ return;
404
+ emitJSXChildren(g, source, el.children ?? [], opts);
405
+ if (el.closingElement) {
406
+ g.add(source.slice(el.closingElement.start, el.closingElement.end), el.closingElement.start, el.closingElement.end);
407
+ }
408
+ }
409
+ function emitJSXExprBody(g, source, expr, opts) {
410
+ if (expr.type === 'JSXElement')
411
+ return emitJSXElement(g, source, expr, opts);
412
+ if (expr.type === 'JSXFragment')
413
+ return emitJSXFragment(g, source, expr, opts);
414
+ g.add(source.slice(expr.start, expr.end), expr.start, expr.end);
415
+ }
416
+ function emitJSXFragment(g, source, frag, opts) {
417
+ g.addRaw('<>');
418
+ emitJSXChildren(g, source, frag.children ?? [], opts);
419
+ g.addRaw('</>');
420
+ }
421
+ /**
422
+ * Emits the `.map(...)` scaffolding for a `for (... of ...)` / `for (... in
423
+ * ...)` clause inside JSX text, mapping the array/object expression to its
424
+ * source range. `clauseStart` is the collapsed-text offset of the clause's
425
+ * first character (inside `text`) and `map` translates collapsed offsets
426
+ * back to raw source offsets.
427
+ */
428
+ function emitForClauseMap(g, source, child, text, map, clauseStart, bodyEmit, emptyEmit) {
429
+ const clauseEnd = findBalancedEnd(text, clauseStart - 1);
430
+ const clause = text.slice(clauseStart, clauseEnd);
431
+ const ofIdx = findTopLevelSep(clause, 'of');
432
+ const inIdx = ofIdx === -1 ? findTopLevelSep(clause, 'in') : -1;
433
+ if (ofIdx === -1 && inIdx === -1)
434
+ return false;
435
+ const sepIdx = ofIdx === -1 ? inIdx : ofIdx;
436
+ const lhs = clause.slice(0, sepIdx);
437
+ const rhs = clause.slice(sepIdx + 2);
438
+ const arrExpr = rhs.trim();
439
+ const arrStartCollapsed = clauseStart + sepIdx + 2 + leadingWsLen(rhs);
440
+ const arrEndCollapsed = clauseStart + sepIdx + 2 + rhs.length - trailingWsLen(rhs);
441
+ const arrStart = arrStartCollapsed < map.length ? child.start + map[arrStartCollapsed] : child.start;
442
+ const arrEnd = arrEndCollapsed > 0 && arrEndCollapsed - 1 < map.length
443
+ ? child.start + map[arrEndCollapsed - 1] + 1
444
+ : child.start;
445
+ const itemVar = stripDeclKeyword(lhs).trim();
446
+ if (ofIdx !== -1) {
447
+ if (emptyEmit) {
448
+ g.addRaw('{ (() => { const _v = ');
449
+ g.add(arrExpr, arrStart, arrEnd);
450
+ g.addRaw('; return _v.length === 0 ? (');
451
+ emptyEmit();
452
+ g.addRaw(') : _v.map((');
453
+ g.addRaw(itemVar);
454
+ g.addRaw(': (typeof _v)[number]) => (');
455
+ bodyEmit();
456
+ g.addRaw(')); })() }');
457
+ }
458
+ else {
459
+ g.addRaw('{ ');
460
+ g.add(arrExpr, arrStart, arrEnd);
461
+ g.addRaw('.map((');
462
+ g.addRaw(itemVar);
463
+ g.addRaw(': (typeof ');
464
+ g.add(arrExpr, arrStart, arrEnd);
465
+ g.addRaw(')[number]) => (');
466
+ bodyEmit();
467
+ g.addRaw(')) }');
468
+ }
469
+ }
470
+ else {
471
+ g.addRaw('{ Object.keys(');
472
+ g.add(arrExpr, arrStart, arrEnd);
473
+ g.addRaw(').map((');
474
+ g.addRaw(itemVar);
475
+ g.addRaw(') => (');
476
+ bodyEmit();
477
+ g.addRaw(')) }');
478
+ }
479
+ return true;
480
+ }
481
+ function emitJSXChildren(g, source, children, opts) {
195
482
  let i = 0;
196
483
  let forPending = false;
197
484
  while (i < children.length) {
@@ -199,7 +486,7 @@ function tsxEmitJSXChildren(source, children) {
199
486
  if (child.type !== 'JSXText')
200
487
  forPending = false;
201
488
  if (child.type === 'JSXText') {
202
- const text = collapseNewlineWhitespace(child.value);
489
+ const { text, map } = collapseWithMap(child.value);
203
490
  const trimmed = text.trim();
204
491
  if (!trimmed || trimmed.startsWith('//')) {
205
492
  i++;
@@ -210,56 +497,42 @@ function tsxEmitJSXChildren(source, children) {
210
497
  if (forDecl !== null && nextChild !== undefined && (nextChild.type === 'JSXExpressionContainer' ||
211
498
  nextChild.type === 'JSXElement' ||
212
499
  nextChild.type === 'JSXFragment')) {
213
- let bodyText;
214
- if (nextChild.type === 'JSXExpressionContainer') {
215
- const exprNode = nextChild.expression;
216
- if (exprNode.type === 'JSXEmptyExpression') {
217
- parts.push(text);
218
- i++;
219
- continue;
500
+ const trimmedStartCollapsed = leadingWsLen(text);
501
+ const parenIdx = skipWhitespace(text, trimmedStartCollapsed + 3);
502
+ if (text[parenIdx] === '(') {
503
+ const clauseStartCollapsed = parenIdx + 1;
504
+ let consumed = 2;
505
+ let emptyEmit = null;
506
+ const emptyTextNode = children[i + 2];
507
+ const emptyContainer = children[i + 3];
508
+ if (emptyTextNode && emptyTextNode.type === 'JSXText' &&
509
+ ['#empty', 'empty'].includes(emptyTextNode.value.trim()) &&
510
+ emptyContainer && emptyContainer.type === 'JSXExpressionContainer' &&
511
+ emptyContainer.expression.type !== 'JSXEmptyExpression') {
512
+ consumed = 4;
513
+ emptyEmit = () => emitJSXExprBody(g, source, emptyContainer.expression, opts);
220
514
  }
221
- bodyText = tsxEmitJSXExprBody(source, exprNode);
222
- }
223
- else if (nextChild.type === 'JSXFragment') {
224
- bodyText = tsxEmitJSXFragment(source, nextChild);
225
- }
226
- else {
227
- bodyText = tsxEmitJSXElement(source, nextChild);
228
- }
229
- let emptyText = '';
230
- let consumed = 2;
231
- const emptyTextNode = children[i + 2];
232
- const emptyContainer = children[i + 3];
233
- if (emptyTextNode && emptyTextNode.type === 'JSXText' &&
234
- ['#empty', 'empty'].includes(emptyTextNode.value.trim()) &&
235
- emptyContainer && emptyContainer.type === 'JSXExpressionContainer' &&
236
- emptyContainer.expression.type !== 'JSXEmptyExpression') {
237
- emptyText = tsxEmitJSXExprBody(source, emptyContainer.expression);
238
- consumed = 4;
239
- }
240
- const ofParts = splitTopLevel(forDecl, 'of');
241
- if (ofParts.length === 2) {
242
- const itemVar = stripDeclKeyword(ofParts[0]).trim();
243
- const arrExpr = ofParts[1].trim();
244
- const map = `{ ${arrExpr}.map((${itemVar}: (typeof ${arrExpr})[number]) => (${bodyText})) }`;
245
- if (emptyText) {
246
- parts.push(`{ (() => { const _v = ${arrExpr}; return _v.length === 0 ? (${emptyText}) : _v.map((${itemVar}: (typeof _v)[number]) => (${bodyText})); })() }`);
515
+ let matched = false;
516
+ if (nextChild.type === 'JSXExpressionContainer') {
517
+ const exprNode = nextChild.expression;
518
+ if (exprNode.type === 'JSXEmptyExpression') {
519
+ g.add(text, child.start, child.end);
520
+ i++;
521
+ continue;
522
+ }
523
+ matched = emitForClauseMap(g, source, child, text, map, clauseStartCollapsed, () => emitJSXExprBody(g, source, exprNode, opts), emptyEmit);
524
+ }
525
+ else if (nextChild.type === 'JSXFragment') {
526
+ matched = emitForClauseMap(g, source, child, text, map, clauseStartCollapsed, () => emitJSXFragment(g, source, nextChild, opts), emptyEmit);
247
527
  }
248
528
  else {
249
- parts.push(map);
529
+ matched = emitForClauseMap(g, source, child, text, map, clauseStartCollapsed, () => emitJSXElement(g, source, nextChild, opts), emptyEmit);
530
+ }
531
+ if (matched) {
532
+ i += consumed;
533
+ forPending = true;
534
+ continue;
250
535
  }
251
- i += consumed;
252
- forPending = true;
253
- continue;
254
- }
255
- const inParts = splitTopLevel(forDecl, 'in');
256
- if (inParts.length === 2) {
257
- const itemVar = stripDeclKeyword(inParts[0]).trim();
258
- const objExpr = inParts[1].trim();
259
- parts.push(`{ Object.keys(${objExpr}).map((${itemVar}) => (${bodyText})) }`);
260
- i += consumed;
261
- forPending = true;
262
- continue;
263
536
  }
264
537
  }
265
538
  if (forPending && trimmed === '}') {
@@ -268,7 +541,7 @@ function tsxEmitJSXChildren(source, children) {
268
541
  continue;
269
542
  }
270
543
  forPending = false;
271
- parts.push(text);
544
+ g.add(text, child.start, child.end);
272
545
  i++;
273
546
  }
274
547
  else if (child.type === 'JSXExpressionContainer') {
@@ -276,165 +549,227 @@ function tsxEmitJSXChildren(source, children) {
276
549
  i++;
277
550
  continue;
278
551
  }
279
- parts.push(`{${source.slice(child.expression.start, child.expression.end)}}`);
552
+ g.addRaw('{');
553
+ g.add(source.slice(child.expression.start, child.expression.end), child.expression.start, child.expression.end);
554
+ g.addRaw('}');
280
555
  i++;
281
556
  }
282
557
  else if (child.type === 'JSXElement') {
283
558
  if (getJSXTagName(child) === 'style') {
559
+ const openEnd = child.openingElement?.end ?? child.start;
560
+ const closeStart = child.closingElement?.start ?? child.end;
561
+ g.recordStyle(child.start, child.end, source.slice(openEnd, closeStart));
284
562
  i++;
285
563
  continue;
286
564
  }
287
- parts.push(tsxEmitJSXElement(source, child));
565
+ emitJSXElement(g, source, child, opts);
288
566
  i++;
289
567
  }
290
568
  else if (child.type === 'JSXFragment') {
291
- parts.push(tsxEmitJSXFragment(source, child));
569
+ emitJSXFragment(g, source, child, opts);
292
570
  i++;
293
571
  }
294
572
  else {
295
573
  i++;
296
574
  }
297
575
  }
298
- return parts.join('');
299
576
  }
300
- function tsxEmitJSXExprBody(source, expr) {
301
- if (expr.type === 'JSXElement')
302
- return tsxEmitJSXElement(source, expr);
303
- if (expr.type === 'JSXFragment')
304
- return tsxEmitJSXFragment(source, expr);
305
- return source.slice(expr.start, expr.end);
306
- }
307
- function tsxEmitJSXElement(source, el) {
308
- const opening = source.slice(el.openingElement.start, el.openingElement.end);
309
- if (el.openingElement.selfClosing)
310
- return opening;
311
- const inner = tsxEmitJSXChildren(source, el.children ?? []);
312
- const closing = el.closingElement ? source.slice(el.closingElement.start, el.closingElement.end) : '';
313
- return opening + inner + closing;
314
- }
315
- function tsxEmitJSXFragment(source, frag) {
316
- const inner = tsxEmitJSXChildren(source, frag.children ?? []);
317
- return '<>' + inner + '</>';
318
- }
319
- function tsxEmitIf(source, stmt, indent) {
320
- const cond = source.slice(stmt.test.start, stmt.test.end);
321
- let out = `${indent}if (${cond}) ${tsxBlock(source, stmt.consequent, indent)}`;
577
+ function emitIf(g, source, stmt, indent, opts) {
578
+ g.add(indent);
579
+ g.addRaw('if (');
580
+ g.add(source.slice(stmt.test.start, stmt.test.end), stmt.test.start, stmt.test.end);
581
+ g.addRaw(') ');
582
+ emitBlock(g, source, stmt.consequent, indent, opts);
322
583
  if (stmt.alternate) {
323
- const altBlock = stmt.alternate.type === 'BlockStatement'
324
- ? tsxBlock(source, stmt.alternate, indent)
325
- : tsxBlock(source, [stmt.alternate], indent);
326
- out += ` else ${altBlock}`;
584
+ g.addRaw(' else ');
585
+ emitBlock(g, source, stmt.alternate, indent, opts);
327
586
  }
328
- return out;
329
587
  }
330
- function tsxEmitForOf(source, stmt, indent) {
331
- const left = source.slice(stmt.left.start, stmt.left.end);
332
- const right = source.slice(stmt.right.start, stmt.right.end);
333
- return `${indent}for (${left} of ${right}) ${tsxBlock(source, stmt.body, indent)}`;
334
- }
335
- function tsxEmitForIn(source, stmt, indent) {
336
- const left = source.slice(stmt.left.start, stmt.left.end);
337
- const right = source.slice(stmt.right.start, stmt.right.end);
338
- return `${indent}for (${left} in ${right}) ${tsxBlock(source, stmt.body, indent)}`;
339
- }
340
- function tsxEmitFor(source, stmt, indent) {
341
- const init = stmt.init ? source.slice(stmt.init.start, stmt.init.end) : '';
342
- const test = stmt.test ? source.slice(stmt.test.start, stmt.test.end) : '';
343
- const update = stmt.update ? source.slice(stmt.update.start, stmt.update.end) : '';
344
- return `${indent}for (${init}; ${test}; ${update}) ${tsxBlock(source, stmt.body, indent)}`;
345
- }
346
- function tsxEmitWhile(source, stmt, indent) {
347
- const test = source.slice(stmt.test.start, stmt.test.end);
348
- return `${indent}while (${test}) ${tsxBlock(source, stmt.body, indent)}`;
349
- }
350
- function tsxEmitDoWhile(source, stmt, indent) {
351
- const test = source.slice(stmt.test.start, stmt.test.end);
352
- return `${indent}do ${tsxBlock(source, stmt.body, indent)} while (${test});`;
353
- }
354
- function tsxEmitSwitch(source, stmt, indent) {
355
- const disc = source.slice(stmt.discriminant.start, stmt.discriminant.end);
356
- const lines = [`${indent}switch (${disc}) {`];
588
+ function emitForOf(g, source, stmt, indent, opts) {
589
+ g.add(indent);
590
+ g.addRaw('for (');
591
+ g.add(source.slice(stmt.left.start, stmt.left.end), stmt.left.start, stmt.left.end);
592
+ g.addRaw(' of ');
593
+ g.add(source.slice(stmt.right.start, stmt.right.end), stmt.right.start, stmt.right.end);
594
+ g.addRaw(') ');
595
+ emitBlock(g, source, stmt.body, indent, opts);
596
+ }
597
+ function emitForIn(g, source, stmt, indent, opts) {
598
+ g.add(indent);
599
+ g.addRaw('for (');
600
+ g.add(source.slice(stmt.left.start, stmt.left.end), stmt.left.start, stmt.left.end);
601
+ g.addRaw(' in ');
602
+ g.add(source.slice(stmt.right.start, stmt.right.end), stmt.right.start, stmt.right.end);
603
+ g.addRaw(') ');
604
+ emitBlock(g, source, stmt.body, indent, opts);
605
+ }
606
+ function emitFor(g, source, stmt, indent, opts) {
607
+ g.add(indent);
608
+ g.addRaw('for (');
609
+ if (stmt.init)
610
+ g.add(source.slice(stmt.init.start, stmt.init.end), stmt.init.start, stmt.init.end);
611
+ g.addRaw('; ');
612
+ if (stmt.test)
613
+ g.add(source.slice(stmt.test.start, stmt.test.end), stmt.test.start, stmt.test.end);
614
+ g.addRaw('; ');
615
+ if (stmt.update)
616
+ g.add(source.slice(stmt.update.start, stmt.update.end), stmt.update.start, stmt.update.end);
617
+ g.addRaw(') ');
618
+ emitBlock(g, source, stmt.body, indent, opts);
619
+ }
620
+ function emitWhile(g, source, stmt, indent, opts) {
621
+ g.add(indent);
622
+ g.addRaw('while (');
623
+ g.add(source.slice(stmt.test.start, stmt.test.end), stmt.test.start, stmt.test.end);
624
+ g.addRaw(') ');
625
+ emitBlock(g, source, stmt.body, indent, opts);
626
+ }
627
+ function emitDoWhile(g, source, stmt, indent, opts) {
628
+ g.add(indent);
629
+ g.addRaw('do ');
630
+ emitBlock(g, source, stmt.body, indent, opts);
631
+ g.addRaw(' while (');
632
+ g.add(source.slice(stmt.test.start, stmt.test.end), stmt.test.start, stmt.test.end);
633
+ g.addRaw(');');
634
+ }
635
+ function emitSwitch(g, source, stmt, indent, opts) {
636
+ g.add(indent);
637
+ g.addRaw('switch (');
638
+ g.add(source.slice(stmt.discriminant.start, stmt.discriminant.end), stmt.discriminant.start, stmt.discriminant.end);
639
+ g.addRaw(') {');
640
+ let firstCase = true;
357
641
  for (const c of stmt.cases ?? []) {
642
+ g.addRaw(firstCase ? '\n' : '\n');
643
+ firstCase = false;
644
+ g.add(indent);
645
+ g.addRaw(' ');
358
646
  if (c.test) {
359
- lines.push(`${indent} case ${source.slice(c.test.start, c.test.end)}:`);
647
+ g.addRaw('case ');
648
+ g.add(source.slice(c.test.start, c.test.end), c.test.start, c.test.end);
649
+ g.addRaw(':');
360
650
  }
361
651
  else {
362
- lines.push(`${indent} default:`);
652
+ g.addRaw('default:');
363
653
  }
364
- const bodyText = tsxEmitBody(source, c.consequent, indent + ' ');
365
- if (bodyText)
366
- lines.push(bodyText);
654
+ const before = g.code.length;
655
+ emitBody(g, source, c.consequent, indent + ' ', opts);
656
+ if (g.code.length > before)
657
+ g.addRaw('\n');
367
658
  }
368
- lines.push(`${indent}}`);
369
- return lines.join('\n');
659
+ g.addRaw('\n');
660
+ g.add(indent);
661
+ g.addRaw('}');
370
662
  }
371
- function tsxEmitTry(source, stmt, indent) {
372
- let out = `${indent}try ${tsxBlock(source, stmt.block, indent)}`;
663
+ function emitTry(g, source, stmt, indent, opts) {
664
+ g.add(indent);
665
+ g.addRaw('try ');
666
+ emitBlock(g, source, stmt.block, indent, opts);
373
667
  if (stmt.handler) {
374
- const param = stmt.handler.param ? source.slice(stmt.handler.param.start, stmt.handler.param.end) : '';
375
- out += ` catch${param ? ` (${param})` : ''} ${tsxBlock(source, stmt.handler.body, indent)}`;
668
+ g.addRaw(' catch');
669
+ if (stmt.handler.param) {
670
+ g.addRaw(' (');
671
+ g.add(source.slice(stmt.handler.param.start, stmt.handler.param.end), stmt.handler.param.start, stmt.handler.param.end);
672
+ g.addRaw(')');
673
+ }
674
+ g.addRaw(' ');
675
+ emitBlock(g, source, stmt.handler.body, indent, opts);
376
676
  }
377
677
  if (stmt.finalizer) {
378
- out += ` finally ${tsxBlock(source, stmt.finalizer, indent)}`;
678
+ g.addRaw(' finally ');
679
+ emitBlock(g, source, stmt.finalizer, indent, opts);
379
680
  }
380
- return out;
381
681
  }
382
- function tsxEmitStatement(source, stmt, indent, isLast) {
682
+ function emitPlainStatement(g, source, stmt, indent, isLast) {
683
+ let text = stripTrailingSemicolons(source.slice(stmt.start, stmt.end).trimEnd());
684
+ if (text === '')
685
+ return;
686
+ g.add(indent);
687
+ g.add(text, stmt.start, stmt.end);
688
+ if (!text.endsWith('}') && !isLast)
689
+ g.addRaw(';');
690
+ }
691
+ function emitStatement(g, source, stmt, indent, isLast, opts) {
383
692
  switch (stmt.type) {
384
693
  case 'JSXElement':
385
- if (getJSXTagName(stmt) === 'style')
386
- return '';
387
- return indent + tsxEmitJSXElement(source, stmt) + (isLast ? '' : ';');
694
+ if (getJSXTagName(stmt) === 'style') {
695
+ const openEnd = stmt.openingElement?.end ?? stmt.start;
696
+ const closeStart = stmt.closingElement?.start ?? stmt.end;
697
+ g.recordStyle(stmt.start, stmt.end, source.slice(openEnd, closeStart));
698
+ return;
699
+ }
700
+ g.add(indent);
701
+ emitJSXElement(g, source, stmt, opts);
702
+ if (!isLast)
703
+ g.addRaw(';');
704
+ return;
388
705
  case 'JSXFragment':
389
- return indent + tsxEmitJSXFragment(source, stmt) + (isLast ? '' : ';');
706
+ g.add(indent);
707
+ emitJSXFragment(g, source, stmt, opts);
708
+ if (!isLast)
709
+ g.addRaw(';');
710
+ return;
390
711
  case 'JSXExpressionContainer': {
391
712
  if (stmt.expression.type === 'JSXEmptyExpression')
392
- return '';
393
- const text = `{${source.slice(stmt.expression.start, stmt.expression.end)}}`;
394
- return indent + text + (isLast ? '' : ';');
713
+ return;
714
+ g.add(indent);
715
+ g.addRaw('{');
716
+ g.add(source.slice(stmt.expression.start, stmt.expression.end), stmt.expression.start, stmt.expression.end);
717
+ g.addRaw('}');
718
+ if (!isLast)
719
+ g.addRaw(';');
720
+ return;
395
721
  }
396
722
  case 'VeskBlock': {
397
723
  if (stmt.tag === 'empty')
398
- return '';
399
- const lines = [];
724
+ return;
400
725
  const innerStmts = stmt.body ?? [];
726
+ let firstLine = true;
401
727
  for (let i = 0; i < innerStmts.length; i++) {
402
- const t = tsxEmitStatement(source, innerStmts[i], indent, isLast && i === innerStmts.length - 1);
403
- if (t !== '')
404
- lines.push(t);
728
+ const before = g.code.length;
729
+ emitStatement(g, source, innerStmts[i], indent, isLast && i === innerStmts.length - 1, opts);
730
+ if (g.code.length > before) {
731
+ if (!firstLine)
732
+ g.addRaw('\n');
733
+ firstLine = false;
734
+ }
405
735
  }
406
- return lines.join('\n');
736
+ return;
407
737
  }
738
+ case 'VariableDeclaration':
739
+ if (isTrackDeclStatement(stmt)) {
740
+ emitTrackDeclStatement(g, source, stmt, indent, isLast, opts);
741
+ return;
742
+ }
743
+ return emitPlainStatement(g, source, stmt, indent, isLast);
408
744
  case 'IfStatement':
409
- return tsxEmitIf(source, stmt, indent);
745
+ return emitIf(g, source, stmt, indent, opts);
410
746
  case 'ForOfStatement':
411
- return tsxEmitForOf(source, stmt, indent);
747
+ return emitForOf(g, source, stmt, indent, opts);
412
748
  case 'ForInStatement':
413
- return tsxEmitForIn(source, stmt, indent);
749
+ return emitForIn(g, source, stmt, indent, opts);
414
750
  case 'ForStatement':
415
- return tsxEmitFor(source, stmt, indent);
751
+ return emitFor(g, source, stmt, indent, opts);
416
752
  case 'WhileStatement':
417
- return tsxEmitWhile(source, stmt, indent);
753
+ return emitWhile(g, source, stmt, indent, opts);
418
754
  case 'DoWhileStatement':
419
- return tsxEmitDoWhile(source, stmt, indent);
755
+ return emitDoWhile(g, source, stmt, indent, opts);
420
756
  case 'SwitchStatement':
421
- return tsxEmitSwitch(source, stmt, indent);
757
+ return emitSwitch(g, source, stmt, indent, opts);
422
758
  case 'TryStatement':
423
- return tsxEmitTry(source, stmt, indent);
759
+ return emitTry(g, source, stmt, indent, opts);
424
760
  case 'LabeledStatement': {
425
- const body = tsxEmitStatement(source, stmt.body, indent, isLast);
426
- return `${indent}${stmt.label.name}:\n${body}`;
427
- }
428
- default: {
429
- let text = stripTrailingSemicolons(source.slice(stmt.start, stmt.end).trimEnd());
430
- if (text === '')
431
- return '';
432
- return indent + text + (text.endsWith('}') || isLast ? '' : ';');
761
+ g.add(indent);
762
+ g.add(stmt.label.name, stmt.label.start, stmt.label.end);
763
+ g.addRaw(':\n');
764
+ emitStatement(g, source, stmt.body, indent, isLast, opts);
765
+ return;
433
766
  }
767
+ default:
768
+ return emitPlainStatement(g, source, stmt, indent, isLast);
434
769
  }
435
770
  }
436
- function tsxEmitBody(source, stmts, indent) {
437
- const lines = [];
771
+ function emitBody(g, source, stmts, indent, opts) {
772
+ let firstLine = true;
438
773
  let i = 0;
439
774
  while (i < stmts.length) {
440
775
  const stmt = stmts[i];
@@ -442,107 +777,254 @@ function tsxEmitBody(source, stmts, indent) {
442
777
  if (stmt.type === 'ForOfStatement' &&
443
778
  stmts[i + 1] && stmts[i + 1].type === 'VeskBlock' && stmts[i + 1].tag === 'empty') {
444
779
  const empty = stmts[i + 1];
445
- lines.push(tsxEmitForOf(source, stmt, indent));
780
+ if (!firstLine)
781
+ g.addRaw('\n');
782
+ firstLine = false;
783
+ emitForOf(g, source, stmt, indent, opts);
446
784
  const inner = empty.body ?? [];
447
785
  if (inner.length > 0) {
448
- const emptyText = tsxEmitBody(source, inner, indent + ' ');
449
- const arrExpr = source.slice(stmt.right.start, stmt.right.end);
450
- lines.push(`${indent}${arrExpr}.length === 0 && (() => {`);
451
- lines.push(emptyText);
452
- lines.push(`${indent}})();`);
786
+ g.addRaw('\n');
787
+ g.add(indent);
788
+ g.add(source.slice(stmt.right.start, stmt.right.end), stmt.right.start, stmt.right.end);
789
+ g.addRaw('.length === 0 && (() => {');
790
+ g.addRaw('\n');
791
+ emitBody(g, source, inner, indent + ' ', opts);
792
+ g.addRaw('\n');
793
+ g.add(indent);
794
+ g.addRaw('})();');
453
795
  }
454
796
  i += 2;
455
797
  continue;
456
798
  }
457
- const text = tsxEmitStatement(source, stmt, indent, isLast);
458
- if (text !== '')
459
- lines.push(text);
799
+ const before = g.code.length;
800
+ emitStatement(g, source, stmt, indent, isLast, opts);
801
+ if (g.code.length > before) {
802
+ if (!firstLine)
803
+ g.addRaw('\n');
804
+ firstLine = false;
805
+ }
460
806
  i++;
461
807
  }
462
- return lines.join('\n');
463
808
  }
464
- function isStatementMode(node) {
465
- const stmts = node.body?.body ?? [];
466
- return !(stmts.length === 1 && stmts[0].type === 'ReturnStatement');
809
+ /**
810
+ * Emits `source[start, end)` as a mapped chunk, skipping any `<style>`
811
+ * blocks (recorded as style regions). Used for expression-mode bodies and
812
+ * the gaps around statements/expressions.
813
+ */
814
+ function emitChunkMapped(g, source, start, end) {
815
+ if (end <= start)
816
+ return;
817
+ let pos = start;
818
+ let i = start;
819
+ while (i < end) {
820
+ if (source[i] === '<' && source.slice(i, i + 6) === '<style') {
821
+ const next = i + 6;
822
+ if (next < end && (isWhitespaceChar(source[next]) || source[next] === '>' || source[next] === '/')) {
823
+ const openEnd = htmlTagEnd(source, i);
824
+ if (openEnd !== -1 && openEnd <= end) {
825
+ const closeIdx = source.indexOf('</style>', openEnd);
826
+ if (closeIdx !== -1 && closeIdx + 8 <= end) {
827
+ g.add(source.slice(pos, i), pos, i);
828
+ g.recordStyle(i, closeIdx + 8, source.slice(openEnd, closeIdx));
829
+ pos = closeIdx + 8;
830
+ i = closeIdx + 8;
831
+ continue;
832
+ }
833
+ }
834
+ }
835
+ }
836
+ i++;
837
+ }
838
+ g.add(source.slice(pos, end), pos, end);
467
839
  }
468
- /** Replaces the first whole-word occurrence of `word` in `text`. */
469
- function replaceFirstKeyword(text, word, replacement) {
470
- let i = 0;
471
- while (i < text.length) {
472
- if (isIdentStart(text[i])) {
473
- let j = i + 1;
474
- while (j < text.length && isIdentChar(text[j]))
475
- j++;
476
- if (text.slice(i, j) === word)
477
- return text.slice(0, i) + replacement + text.slice(j);
478
- i = j;
840
+ function emitReturn(g, source, stmt, opts) {
841
+ g.add(source.slice(stmt.start, stmt.start + 6), stmt.start, stmt.start + 6);
842
+ const arg = stmt.argument;
843
+ if (!arg) {
844
+ emitChunkMapped(g, source, stmt.start + 6, stmt.end);
845
+ return;
846
+ }
847
+ emitChunkMapped(g, source, stmt.start + 6, arg.start);
848
+ if (arg.type === 'JSXElement')
849
+ emitJSXElement(g, source, arg, opts);
850
+ else if (arg.type === 'JSXFragment')
851
+ emitJSXFragment(g, source, arg, opts);
852
+ else
853
+ emitChunkMapped(g, source, arg.start, arg.end);
854
+ emitChunkMapped(g, source, arg.end, stmt.end);
855
+ }
856
+ /**
857
+ * Emits a statement-mode component body (bare JSX, control flow,
858
+ * guard-clause returns). The body text starts directly with the first
859
+ * statement (no leading whitespace) and the `{ ... }` wrapping is emitted
860
+ * by the caller.
861
+ */
862
+ function emitBodyCore(g, source, stmts, opts) {
863
+ emitBody(g, source, stmts, '', opts);
864
+ }
865
+ /** Emits an expression-mode component body (`return <jsx>` or a bare expression). */
866
+ function emitExpressionBody(g, source, coreStart, coreEnd, stmts, opts) {
867
+ let pos = coreStart;
868
+ for (const stmt of stmts) {
869
+ if (!stmt)
479
870
  continue;
871
+ emitChunkMapped(g, source, pos, stmt.start);
872
+ pos = stmt.start;
873
+ if (isTrackDeclStatement(stmt)) {
874
+ emitTrackDeclStatement(g, source, stmt, '', true, opts);
480
875
  }
481
- i++;
876
+ else if (stmt.type === 'ReturnStatement') {
877
+ emitReturn(g, source, stmt, opts);
878
+ }
879
+ else {
880
+ emitChunkMapped(g, source, stmt.start, stmt.end);
881
+ }
882
+ pos = stmt.end;
482
883
  }
483
- return text;
884
+ emitChunkMapped(g, source, pos, coreEnd);
484
885
  }
485
886
  /**
486
- * Removes a trailing `client` keyword from a component header (the header is
487
- * the text from the declaration start up to the body `{`), so `component Foo
488
- * client {` becomes `component Foo {`. Returns the text with the keyword
489
- * replaced by a single space.
887
+ * Emits `text` (covering source range [base, end)) with its first
888
+ * identifier split into its own mapping so hover/rename/definition land
889
+ * exactly on the component name.
490
890
  */
491
- function stripTrailingClient(text) {
492
- let end = text.length;
493
- while (end > 0 && isWhitespaceChar(text[end - 1]))
494
- end--;
495
- let wordStart = end;
496
- while (wordStart > 0 && isIdentChar(text[wordStart - 1]))
497
- wordStart--;
498
- if (text.slice(wordStart, end) === 'client') {
499
- return text.slice(0, wordStart).trimEnd() + ' ';
891
+ function emitRestMapped(g, text, base, end) {
892
+ let i = 0;
893
+ while (i < text.length && !isIdentStart(text[i]))
894
+ i++;
895
+ if (i >= text.length) {
896
+ g.add(text, base, end);
897
+ return;
500
898
  }
501
- return text;
899
+ let j = i + 1;
900
+ while (j < text.length && isIdentChar(text[j]))
901
+ j++;
902
+ g.add(text.slice(0, i), base, base + i);
903
+ g.add(text.slice(i, j), base + i, base + j);
904
+ g.add(text.slice(j), base + j, end);
502
905
  }
503
- export function vskToTsx(source) {
906
+ function emitComponentHeader(g, source, start, bodyStart) {
907
+ const h = source.slice(start, bodyStart);
908
+ const k = findFirstKeyword(h, 'component');
909
+ if (k === -1) {
910
+ const { text, end } = stripTrailingClientRange(h);
911
+ if (!text.includes('(')) {
912
+ const trimmed = text.trimEnd();
913
+ g.add(trimmed, start, start + end - (text.length - trimmed.length));
914
+ g.addRaw('() ');
915
+ }
916
+ else {
917
+ g.add(text, start, start + end);
918
+ }
919
+ return;
920
+ }
921
+ g.add(h.slice(0, k), start, start + k);
922
+ g.add('function', start + k, start + k + 'component'.length);
923
+ const restBase = start + k + 'component'.length;
924
+ const { text: rest, end } = stripTrailingClientRange(h.slice(k + 'component'.length));
925
+ const transformed = h.slice(0, k) + 'function' + rest;
926
+ if (!transformed.includes('(')) {
927
+ const trimmed = rest.trimEnd();
928
+ emitRestMapped(g, trimmed, restBase, restBase + end - (rest.length - trimmed.length));
929
+ g.addRaw('() ');
930
+ }
931
+ else {
932
+ emitRestMapped(g, rest, restBase, restBase + end);
933
+ }
934
+ }
935
+ function emitComponent(g, source, node, start, opts) {
936
+ const body = node.body;
937
+ if (!body || body.start == null || body.end == null)
938
+ return;
939
+ emitComponentHeader(g, source, start, body.start);
940
+ const statementMode = isStatementMode(node);
941
+ const stmts = body.body ?? [];
942
+ if (statementMode) {
943
+ g.addRaw('{ ');
944
+ emitBodyCore(g, source, stmts, opts);
945
+ g.addRaw(' }');
946
+ }
947
+ else {
948
+ const raw = source.slice(body.start, body.end);
949
+ const rawLen = raw.length;
950
+ let s = 0;
951
+ let e = rawLen;
952
+ while (s < e && isWhitespaceChar(raw[s]))
953
+ s++;
954
+ while (e > s && isWhitespaceChar(raw[e - 1]))
955
+ e--;
956
+ const hasBraces = raw[s] === '{' && raw[e - 1] === '}';
957
+ let coreStart;
958
+ let coreEnd;
959
+ if (hasBraces) {
960
+ coreStart = body.start + s + 1;
961
+ coreEnd = body.start + e - 1;
962
+ }
963
+ else {
964
+ coreStart = body.start + s;
965
+ coreEnd = body.start + e;
966
+ }
967
+ while (coreStart < coreEnd && isWhitespaceChar(source[coreStart]))
968
+ coreStart++;
969
+ while (coreEnd > coreStart && isWhitespaceChar(source[coreEnd - 1]))
970
+ coreEnd--;
971
+ if (hasBraces)
972
+ g.addRaw('{ ');
973
+ emitExpressionBody(g, source, coreStart, coreEnd, stmts, opts);
974
+ if (hasBraces)
975
+ g.addRaw(' }');
976
+ }
977
+ }
978
+ function isModuleAst(ast) {
979
+ return (ast.body || []).some((s) => s.type === 'ImportDeclaration' ||
980
+ s.type === 'ExportNamedDeclaration' ||
981
+ s.type === 'ExportDefaultDeclaration' ||
982
+ s.type === 'ExportAllDeclaration');
983
+ }
984
+ export function compileVskCodegen(source, opts = {}) {
504
985
  let ast;
505
986
  try {
506
987
  ast = parse(source);
507
988
  }
508
- catch {
509
- return source;
989
+ catch (e) {
990
+ const pos = e.pos ?? 0;
991
+ return {
992
+ code: source,
993
+ mappings: [
994
+ {
995
+ sourceOffsets: [0],
996
+ generatedOffsets: [0],
997
+ lengths: [source.length],
998
+ generatedLengths: [source.length],
999
+ data: FULL_DATA,
1000
+ },
1001
+ ],
1002
+ styleRegions: [],
1003
+ errors: [{ message: e.message, start: pos, end: pos + 1 }],
1004
+ };
510
1005
  }
511
1006
  const clean = blankForClauses(source, ast);
512
- const edits = [];
513
- for (const { node, start } of getComponents(ast)) {
1007
+ const g = new VskGen();
1008
+ const components = getComponents(ast).sort((a, b) => a.start - b.start);
1009
+ let pos = 0;
1010
+ for (const { node, start } of components) {
514
1011
  const body = node.body;
515
1012
  if (!body || body.start == null || body.end == null)
516
1013
  continue;
517
- let transformed = stripTrailingClient(replaceFirstKeyword(clean.slice(start, body.start), 'component', 'function'));
518
- if (!transformed.includes('(')) {
519
- transformed = transformed.trimEnd() + '() ';
520
- }
521
- const statementMode = isStatementMode(node);
522
- let wrapped;
523
- if (statementMode) {
524
- const bodyText = rewriteTrackDecls(tsxEmitBody(clean, body.body ?? [], ''));
525
- wrapped = '{ ' + bodyText.trim() + ' }';
526
- }
527
- else {
528
- const raw = clean.slice(body.start, body.end);
529
- const inner = raw.trim();
530
- const hasBraces = inner.startsWith('{') && inner.endsWith('}');
531
- const core = hasBraces ? inner.slice(1, -1) : inner;
532
- const bodyText = rewriteTrackDecls(stripStyleBlocks(core));
533
- wrapped = hasBraces ? '{ ' + bodyText.trim() + ' }' : bodyText;
534
- }
535
- edits.push({ start, end: body.end, text: transformed + wrapped });
1014
+ g.add(clean.slice(pos, start), pos, start);
1015
+ emitComponent(g, clean, node, start, opts);
1016
+ pos = body.end;
536
1017
  }
537
- const out = applyEdits(source, edits);
538
- const isModule = (ast.body || []).some((s) => s.type === 'ImportDeclaration' ||
539
- s.type === 'ExportNamedDeclaration' ||
540
- s.type === 'ExportDefaultDeclaration' ||
541
- s.type === 'ExportAllDeclaration');
542
- if (isModule && containsIdentifier(out, 'Head') && !isIdentifierImported(out, 'Head')) {
543
- return `declare const Head: (props: { children?: unknown }) => unknown;\n${out}`;
1018
+ g.add(clean.slice(pos), pos, source.length);
1019
+ let code = g.code;
1020
+ if (isModuleAst(ast) && containsIdentifier(code, 'Head') && !isIdentifierImported(code, 'Head')) {
1021
+ g.prepend('declare const Head: (props: { children?: Component }) => Component;\n');
1022
+ code = g.code;
544
1023
  }
545
- return out;
1024
+ return { code, mappings: g.mappings, styleRegions: g.styleRegions, errors: [] };
1025
+ }
1026
+ export function vskToTsx(source) {
1027
+ return compileVskCodegen(source).code;
546
1028
  }
547
1029
  function isTypeDecl(stmt) {
548
1030
  return (stmt.type === 'TSInterfaceDeclaration' ||
@@ -585,7 +1067,7 @@ export function generateVskDts(source) {
585
1067
  const hasType = propsType !== null && propsType !== undefined;
586
1068
  const collision = typeDeclNames.has(typeName);
587
1069
  const signature = hasType
588
- ? (collision ? propsType : typeName) + ' & { children?: unknown }'
1070
+ ? (collision ? propsType : typeName) + ' & { children?: Component }'
589
1071
  : 'any';
590
1072
  if (hasType && !collision) {
591
1073
  lines.push(`export type ${typeName} = ${propsType};`, '');
@@ -594,10 +1076,10 @@ export function generateVskDts(source) {
594
1076
  lines.push(`export type ${typeName} = any;`, '');
595
1077
  }
596
1078
  if (defaultExport) {
597
- lines.push(`export default function ${name}(props: ${signature}): unknown;`, '');
1079
+ lines.push(`export default function ${name}(props: ${signature}): Component;`, '');
598
1080
  }
599
1081
  else {
600
- lines.push(`export declare function ${name}(props: ${signature}): unknown;`, '');
1082
+ lines.push(`export declare function ${name}(props: ${signature}): Component;`, '');
601
1083
  }
602
1084
  }
603
1085
  return lines.join('\n');