@vesk/compiler 0.1.1 → 0.1.14

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/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,384 @@ 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
+ g.add(source.slice(prev, op.end), prev, op.end);
392
+ if (op.selfClosing)
393
+ return;
394
+ emitJSXChildren(g, source, el.children ?? [], opts);
395
+ if (el.closingElement) {
396
+ g.add(source.slice(el.closingElement.start, el.closingElement.end), el.closingElement.start, el.closingElement.end);
397
+ }
398
+ }
399
+ function emitJSXExprBody(g, source, expr, opts) {
400
+ if (expr.type === 'JSXElement')
401
+ return emitJSXElement(g, source, expr, opts);
402
+ if (expr.type === 'JSXFragment')
403
+ return emitJSXFragment(g, source, expr, opts);
404
+ g.add(source.slice(expr.start, expr.end), expr.start, expr.end);
405
+ }
406
+ function emitJSXFragment(g, source, frag, opts) {
407
+ g.addRaw('<>');
408
+ emitJSXChildren(g, source, frag.children ?? [], opts);
409
+ g.addRaw('</>');
410
+ }
411
+ /**
412
+ * Emits the `.map(...)` scaffolding for a `for (... of ...)` / `for (... in
413
+ * ...)` clause inside JSX text, mapping the array/object expression to its
414
+ * source range. `clauseStart` is the collapsed-text offset of the clause's
415
+ * first character (inside `text`) and `map` translates collapsed offsets
416
+ * back to raw source offsets.
417
+ */
418
+ function emitForClauseMap(g, source, child, text, map, clauseStart, bodyEmit, emptyEmit) {
419
+ const clauseEnd = findBalancedEnd(text, clauseStart - 1);
420
+ const clause = text.slice(clauseStart, clauseEnd);
421
+ const ofIdx = findTopLevelSep(clause, 'of');
422
+ const inIdx = ofIdx === -1 ? findTopLevelSep(clause, 'in') : -1;
423
+ if (ofIdx === -1 && inIdx === -1)
424
+ return false;
425
+ const sepIdx = ofIdx === -1 ? inIdx : ofIdx;
426
+ const lhs = clause.slice(0, sepIdx);
427
+ const rhs = clause.slice(sepIdx + 2);
428
+ const arrExpr = rhs.trim();
429
+ const arrStartCollapsed = clauseStart + sepIdx + 2 + leadingWsLen(rhs);
430
+ const arrEndCollapsed = clauseStart + sepIdx + 2 + rhs.length - trailingWsLen(rhs);
431
+ const arrStart = arrStartCollapsed < map.length ? child.start + map[arrStartCollapsed] : child.start;
432
+ const arrEnd = arrEndCollapsed > 0 && arrEndCollapsed - 1 < map.length
433
+ ? child.start + map[arrEndCollapsed - 1] + 1
434
+ : child.start;
435
+ const itemVar = stripDeclKeyword(lhs).trim();
436
+ if (ofIdx !== -1) {
437
+ if (emptyEmit) {
438
+ g.addRaw('{ (() => { const _v = ');
439
+ g.add(arrExpr, arrStart, arrEnd);
440
+ g.addRaw('; return _v.length === 0 ? (');
441
+ emptyEmit();
442
+ g.addRaw(') : _v.map((');
443
+ g.addRaw(itemVar);
444
+ g.addRaw(': (typeof _v)[number]) => (');
445
+ bodyEmit();
446
+ g.addRaw(')); })() }');
447
+ }
448
+ else {
449
+ g.addRaw('{ ');
450
+ g.add(arrExpr, arrStart, arrEnd);
451
+ g.addRaw('.map((');
452
+ g.addRaw(itemVar);
453
+ g.addRaw(': (typeof ');
454
+ g.add(arrExpr, arrStart, arrEnd);
455
+ g.addRaw(')[number]) => (');
456
+ bodyEmit();
457
+ g.addRaw(')) }');
458
+ }
459
+ }
460
+ else {
461
+ g.addRaw('{ Object.keys(');
462
+ g.add(arrExpr, arrStart, arrEnd);
463
+ g.addRaw(').map((');
464
+ g.addRaw(itemVar);
465
+ g.addRaw(') => (');
466
+ bodyEmit();
467
+ g.addRaw(')) }');
468
+ }
469
+ return true;
470
+ }
471
+ function emitJSXChildren(g, source, children, opts) {
195
472
  let i = 0;
196
473
  let forPending = false;
197
474
  while (i < children.length) {
@@ -199,7 +476,7 @@ function tsxEmitJSXChildren(source, children) {
199
476
  if (child.type !== 'JSXText')
200
477
  forPending = false;
201
478
  if (child.type === 'JSXText') {
202
- const text = collapseNewlineWhitespace(child.value);
479
+ const { text, map } = collapseWithMap(child.value);
203
480
  const trimmed = text.trim();
204
481
  if (!trimmed || trimmed.startsWith('//')) {
205
482
  i++;
@@ -210,56 +487,42 @@ function tsxEmitJSXChildren(source, children) {
210
487
  if (forDecl !== null && nextChild !== undefined && (nextChild.type === 'JSXExpressionContainer' ||
211
488
  nextChild.type === 'JSXElement' ||
212
489
  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;
490
+ const trimmedStartCollapsed = leadingWsLen(text);
491
+ const parenIdx = skipWhitespace(text, trimmedStartCollapsed + 3);
492
+ if (text[parenIdx] === '(') {
493
+ const clauseStartCollapsed = parenIdx + 1;
494
+ let consumed = 2;
495
+ let emptyEmit = null;
496
+ const emptyTextNode = children[i + 2];
497
+ const emptyContainer = children[i + 3];
498
+ if (emptyTextNode && emptyTextNode.type === 'JSXText' &&
499
+ ['#empty', 'empty'].includes(emptyTextNode.value.trim()) &&
500
+ emptyContainer && emptyContainer.type === 'JSXExpressionContainer' &&
501
+ emptyContainer.expression.type !== 'JSXEmptyExpression') {
502
+ consumed = 4;
503
+ emptyEmit = () => emitJSXExprBody(g, source, emptyContainer.expression, opts);
220
504
  }
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})); })() }`);
505
+ let matched = false;
506
+ if (nextChild.type === 'JSXExpressionContainer') {
507
+ const exprNode = nextChild.expression;
508
+ if (exprNode.type === 'JSXEmptyExpression') {
509
+ g.add(text, child.start, child.end);
510
+ i++;
511
+ continue;
512
+ }
513
+ matched = emitForClauseMap(g, source, child, text, map, clauseStartCollapsed, () => emitJSXExprBody(g, source, exprNode, opts), emptyEmit);
514
+ }
515
+ else if (nextChild.type === 'JSXFragment') {
516
+ matched = emitForClauseMap(g, source, child, text, map, clauseStartCollapsed, () => emitJSXFragment(g, source, nextChild, opts), emptyEmit);
247
517
  }
248
518
  else {
249
- parts.push(map);
519
+ matched = emitForClauseMap(g, source, child, text, map, clauseStartCollapsed, () => emitJSXElement(g, source, nextChild, opts), emptyEmit);
520
+ }
521
+ if (matched) {
522
+ i += consumed;
523
+ forPending = true;
524
+ continue;
250
525
  }
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
526
  }
264
527
  }
265
528
  if (forPending && trimmed === '}') {
@@ -268,7 +531,7 @@ function tsxEmitJSXChildren(source, children) {
268
531
  continue;
269
532
  }
270
533
  forPending = false;
271
- parts.push(text);
534
+ g.add(text, child.start, child.end);
272
535
  i++;
273
536
  }
274
537
  else if (child.type === 'JSXExpressionContainer') {
@@ -276,165 +539,227 @@ function tsxEmitJSXChildren(source, children) {
276
539
  i++;
277
540
  continue;
278
541
  }
279
- parts.push(`{${source.slice(child.expression.start, child.expression.end)}}`);
542
+ g.addRaw('{');
543
+ g.add(source.slice(child.expression.start, child.expression.end), child.expression.start, child.expression.end);
544
+ g.addRaw('}');
280
545
  i++;
281
546
  }
282
547
  else if (child.type === 'JSXElement') {
283
548
  if (getJSXTagName(child) === 'style') {
549
+ const openEnd = child.openingElement?.end ?? child.start;
550
+ const closeStart = child.closingElement?.start ?? child.end;
551
+ g.recordStyle(child.start, child.end, source.slice(openEnd, closeStart));
284
552
  i++;
285
553
  continue;
286
554
  }
287
- parts.push(tsxEmitJSXElement(source, child));
555
+ emitJSXElement(g, source, child, opts);
288
556
  i++;
289
557
  }
290
558
  else if (child.type === 'JSXFragment') {
291
- parts.push(tsxEmitJSXFragment(source, child));
559
+ emitJSXFragment(g, source, child, opts);
292
560
  i++;
293
561
  }
294
562
  else {
295
563
  i++;
296
564
  }
297
565
  }
298
- return parts.join('');
299
566
  }
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)}`;
567
+ function emitIf(g, source, stmt, indent, opts) {
568
+ g.add(indent);
569
+ g.addRaw('if (');
570
+ g.add(source.slice(stmt.test.start, stmt.test.end), stmt.test.start, stmt.test.end);
571
+ g.addRaw(') ');
572
+ emitBlock(g, source, stmt.consequent, indent, opts);
322
573
  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}`;
574
+ g.addRaw(' else ');
575
+ emitBlock(g, source, stmt.alternate, indent, opts);
327
576
  }
328
- return out;
329
577
  }
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}) {`];
578
+ function emitForOf(g, source, stmt, indent, opts) {
579
+ g.add(indent);
580
+ g.addRaw('for (');
581
+ g.add(source.slice(stmt.left.start, stmt.left.end), stmt.left.start, stmt.left.end);
582
+ g.addRaw(' of ');
583
+ g.add(source.slice(stmt.right.start, stmt.right.end), stmt.right.start, stmt.right.end);
584
+ g.addRaw(') ');
585
+ emitBlock(g, source, stmt.body, indent, opts);
586
+ }
587
+ function emitForIn(g, source, stmt, indent, opts) {
588
+ g.add(indent);
589
+ g.addRaw('for (');
590
+ g.add(source.slice(stmt.left.start, stmt.left.end), stmt.left.start, stmt.left.end);
591
+ g.addRaw(' in ');
592
+ g.add(source.slice(stmt.right.start, stmt.right.end), stmt.right.start, stmt.right.end);
593
+ g.addRaw(') ');
594
+ emitBlock(g, source, stmt.body, indent, opts);
595
+ }
596
+ function emitFor(g, source, stmt, indent, opts) {
597
+ g.add(indent);
598
+ g.addRaw('for (');
599
+ if (stmt.init)
600
+ g.add(source.slice(stmt.init.start, stmt.init.end), stmt.init.start, stmt.init.end);
601
+ g.addRaw('; ');
602
+ if (stmt.test)
603
+ g.add(source.slice(stmt.test.start, stmt.test.end), stmt.test.start, stmt.test.end);
604
+ g.addRaw('; ');
605
+ if (stmt.update)
606
+ g.add(source.slice(stmt.update.start, stmt.update.end), stmt.update.start, stmt.update.end);
607
+ g.addRaw(') ');
608
+ emitBlock(g, source, stmt.body, indent, opts);
609
+ }
610
+ function emitWhile(g, source, stmt, indent, opts) {
611
+ g.add(indent);
612
+ g.addRaw('while (');
613
+ g.add(source.slice(stmt.test.start, stmt.test.end), stmt.test.start, stmt.test.end);
614
+ g.addRaw(') ');
615
+ emitBlock(g, source, stmt.body, indent, opts);
616
+ }
617
+ function emitDoWhile(g, source, stmt, indent, opts) {
618
+ g.add(indent);
619
+ g.addRaw('do ');
620
+ emitBlock(g, source, stmt.body, indent, opts);
621
+ g.addRaw(' while (');
622
+ g.add(source.slice(stmt.test.start, stmt.test.end), stmt.test.start, stmt.test.end);
623
+ g.addRaw(');');
624
+ }
625
+ function emitSwitch(g, source, stmt, indent, opts) {
626
+ g.add(indent);
627
+ g.addRaw('switch (');
628
+ g.add(source.slice(stmt.discriminant.start, stmt.discriminant.end), stmt.discriminant.start, stmt.discriminant.end);
629
+ g.addRaw(') {');
630
+ let firstCase = true;
357
631
  for (const c of stmt.cases ?? []) {
632
+ g.addRaw(firstCase ? '\n' : '\n');
633
+ firstCase = false;
634
+ g.add(indent);
635
+ g.addRaw(' ');
358
636
  if (c.test) {
359
- lines.push(`${indent} case ${source.slice(c.test.start, c.test.end)}:`);
637
+ g.addRaw('case ');
638
+ g.add(source.slice(c.test.start, c.test.end), c.test.start, c.test.end);
639
+ g.addRaw(':');
360
640
  }
361
641
  else {
362
- lines.push(`${indent} default:`);
642
+ g.addRaw('default:');
363
643
  }
364
- const bodyText = tsxEmitBody(source, c.consequent, indent + ' ');
365
- if (bodyText)
366
- lines.push(bodyText);
644
+ const before = g.code.length;
645
+ emitBody(g, source, c.consequent, indent + ' ', opts);
646
+ if (g.code.length > before)
647
+ g.addRaw('\n');
367
648
  }
368
- lines.push(`${indent}}`);
369
- return lines.join('\n');
649
+ g.addRaw('\n');
650
+ g.add(indent);
651
+ g.addRaw('}');
370
652
  }
371
- function tsxEmitTry(source, stmt, indent) {
372
- let out = `${indent}try ${tsxBlock(source, stmt.block, indent)}`;
653
+ function emitTry(g, source, stmt, indent, opts) {
654
+ g.add(indent);
655
+ g.addRaw('try ');
656
+ emitBlock(g, source, stmt.block, indent, opts);
373
657
  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)}`;
658
+ g.addRaw(' catch');
659
+ if (stmt.handler.param) {
660
+ g.addRaw(' (');
661
+ g.add(source.slice(stmt.handler.param.start, stmt.handler.param.end), stmt.handler.param.start, stmt.handler.param.end);
662
+ g.addRaw(')');
663
+ }
664
+ g.addRaw(' ');
665
+ emitBlock(g, source, stmt.handler.body, indent, opts);
376
666
  }
377
667
  if (stmt.finalizer) {
378
- out += ` finally ${tsxBlock(source, stmt.finalizer, indent)}`;
668
+ g.addRaw(' finally ');
669
+ emitBlock(g, source, stmt.finalizer, indent, opts);
379
670
  }
380
- return out;
381
671
  }
382
- function tsxEmitStatement(source, stmt, indent, isLast) {
672
+ function emitPlainStatement(g, source, stmt, indent, isLast) {
673
+ let text = stripTrailingSemicolons(source.slice(stmt.start, stmt.end).trimEnd());
674
+ if (text === '')
675
+ return;
676
+ g.add(indent);
677
+ g.add(text, stmt.start, stmt.end);
678
+ if (!text.endsWith('}') && !isLast)
679
+ g.addRaw(';');
680
+ }
681
+ function emitStatement(g, source, stmt, indent, isLast, opts) {
383
682
  switch (stmt.type) {
384
683
  case 'JSXElement':
385
- if (getJSXTagName(stmt) === 'style')
386
- return '';
387
- return indent + tsxEmitJSXElement(source, stmt) + (isLast ? '' : ';');
684
+ if (getJSXTagName(stmt) === 'style') {
685
+ const openEnd = stmt.openingElement?.end ?? stmt.start;
686
+ const closeStart = stmt.closingElement?.start ?? stmt.end;
687
+ g.recordStyle(stmt.start, stmt.end, source.slice(openEnd, closeStart));
688
+ return;
689
+ }
690
+ g.add(indent);
691
+ emitJSXElement(g, source, stmt, opts);
692
+ if (!isLast)
693
+ g.addRaw(';');
694
+ return;
388
695
  case 'JSXFragment':
389
- return indent + tsxEmitJSXFragment(source, stmt) + (isLast ? '' : ';');
696
+ g.add(indent);
697
+ emitJSXFragment(g, source, stmt, opts);
698
+ if (!isLast)
699
+ g.addRaw(';');
700
+ return;
390
701
  case 'JSXExpressionContainer': {
391
702
  if (stmt.expression.type === 'JSXEmptyExpression')
392
- return '';
393
- const text = `{${source.slice(stmt.expression.start, stmt.expression.end)}}`;
394
- return indent + text + (isLast ? '' : ';');
703
+ return;
704
+ g.add(indent);
705
+ g.addRaw('{');
706
+ g.add(source.slice(stmt.expression.start, stmt.expression.end), stmt.expression.start, stmt.expression.end);
707
+ g.addRaw('}');
708
+ if (!isLast)
709
+ g.addRaw(';');
710
+ return;
395
711
  }
396
712
  case 'VeskBlock': {
397
713
  if (stmt.tag === 'empty')
398
- return '';
399
- const lines = [];
714
+ return;
400
715
  const innerStmts = stmt.body ?? [];
716
+ let firstLine = true;
401
717
  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);
718
+ const before = g.code.length;
719
+ emitStatement(g, source, innerStmts[i], indent, isLast && i === innerStmts.length - 1, opts);
720
+ if (g.code.length > before) {
721
+ if (!firstLine)
722
+ g.addRaw('\n');
723
+ firstLine = false;
724
+ }
405
725
  }
406
- return lines.join('\n');
726
+ return;
407
727
  }
728
+ case 'VariableDeclaration':
729
+ if (isTrackDeclStatement(stmt)) {
730
+ emitTrackDeclStatement(g, source, stmt, indent, isLast, opts);
731
+ return;
732
+ }
733
+ return emitPlainStatement(g, source, stmt, indent, isLast);
408
734
  case 'IfStatement':
409
- return tsxEmitIf(source, stmt, indent);
735
+ return emitIf(g, source, stmt, indent, opts);
410
736
  case 'ForOfStatement':
411
- return tsxEmitForOf(source, stmt, indent);
737
+ return emitForOf(g, source, stmt, indent, opts);
412
738
  case 'ForInStatement':
413
- return tsxEmitForIn(source, stmt, indent);
739
+ return emitForIn(g, source, stmt, indent, opts);
414
740
  case 'ForStatement':
415
- return tsxEmitFor(source, stmt, indent);
741
+ return emitFor(g, source, stmt, indent, opts);
416
742
  case 'WhileStatement':
417
- return tsxEmitWhile(source, stmt, indent);
743
+ return emitWhile(g, source, stmt, indent, opts);
418
744
  case 'DoWhileStatement':
419
- return tsxEmitDoWhile(source, stmt, indent);
745
+ return emitDoWhile(g, source, stmt, indent, opts);
420
746
  case 'SwitchStatement':
421
- return tsxEmitSwitch(source, stmt, indent);
747
+ return emitSwitch(g, source, stmt, indent, opts);
422
748
  case 'TryStatement':
423
- return tsxEmitTry(source, stmt, indent);
749
+ return emitTry(g, source, stmt, indent, opts);
424
750
  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 ? '' : ';');
751
+ g.add(indent);
752
+ g.add(stmt.label.name, stmt.label.start, stmt.label.end);
753
+ g.addRaw(':\n');
754
+ emitStatement(g, source, stmt.body, indent, isLast, opts);
755
+ return;
433
756
  }
757
+ default:
758
+ return emitPlainStatement(g, source, stmt, indent, isLast);
434
759
  }
435
760
  }
436
- function tsxEmitBody(source, stmts, indent) {
437
- const lines = [];
761
+ function emitBody(g, source, stmts, indent, opts) {
762
+ let firstLine = true;
438
763
  let i = 0;
439
764
  while (i < stmts.length) {
440
765
  const stmt = stmts[i];
@@ -442,107 +767,254 @@ function tsxEmitBody(source, stmts, indent) {
442
767
  if (stmt.type === 'ForOfStatement' &&
443
768
  stmts[i + 1] && stmts[i + 1].type === 'VeskBlock' && stmts[i + 1].tag === 'empty') {
444
769
  const empty = stmts[i + 1];
445
- lines.push(tsxEmitForOf(source, stmt, indent));
770
+ if (!firstLine)
771
+ g.addRaw('\n');
772
+ firstLine = false;
773
+ emitForOf(g, source, stmt, indent, opts);
446
774
  const inner = empty.body ?? [];
447
775
  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}})();`);
776
+ g.addRaw('\n');
777
+ g.add(indent);
778
+ g.add(source.slice(stmt.right.start, stmt.right.end), stmt.right.start, stmt.right.end);
779
+ g.addRaw('.length === 0 && (() => {');
780
+ g.addRaw('\n');
781
+ emitBody(g, source, inner, indent + ' ', opts);
782
+ g.addRaw('\n');
783
+ g.add(indent);
784
+ g.addRaw('})();');
453
785
  }
454
786
  i += 2;
455
787
  continue;
456
788
  }
457
- const text = tsxEmitStatement(source, stmt, indent, isLast);
458
- if (text !== '')
459
- lines.push(text);
789
+ const before = g.code.length;
790
+ emitStatement(g, source, stmt, indent, isLast, opts);
791
+ if (g.code.length > before) {
792
+ if (!firstLine)
793
+ g.addRaw('\n');
794
+ firstLine = false;
795
+ }
460
796
  i++;
461
797
  }
462
- return lines.join('\n');
463
798
  }
464
- function isStatementMode(node) {
465
- const stmts = node.body?.body ?? [];
466
- return !(stmts.length === 1 && stmts[0].type === 'ReturnStatement');
799
+ /**
800
+ * Emits `source[start, end)` as a mapped chunk, skipping any `<style>`
801
+ * blocks (recorded as style regions). Used for expression-mode bodies and
802
+ * the gaps around statements/expressions.
803
+ */
804
+ function emitChunkMapped(g, source, start, end) {
805
+ if (end <= start)
806
+ return;
807
+ let pos = start;
808
+ let i = start;
809
+ while (i < end) {
810
+ if (source[i] === '<' && source.slice(i, i + 6) === '<style') {
811
+ const next = i + 6;
812
+ if (next < end && (isWhitespaceChar(source[next]) || source[next] === '>' || source[next] === '/')) {
813
+ const openEnd = htmlTagEnd(source, i);
814
+ if (openEnd !== -1 && openEnd <= end) {
815
+ const closeIdx = source.indexOf('</style>', openEnd);
816
+ if (closeIdx !== -1 && closeIdx + 8 <= end) {
817
+ g.add(source.slice(pos, i), pos, i);
818
+ g.recordStyle(i, closeIdx + 8, source.slice(openEnd, closeIdx));
819
+ pos = closeIdx + 8;
820
+ i = closeIdx + 8;
821
+ continue;
822
+ }
823
+ }
824
+ }
825
+ }
826
+ i++;
827
+ }
828
+ g.add(source.slice(pos, end), pos, end);
467
829
  }
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;
830
+ function emitReturn(g, source, stmt, opts) {
831
+ g.add(source.slice(stmt.start, stmt.start + 6), stmt.start, stmt.start + 6);
832
+ const arg = stmt.argument;
833
+ if (!arg) {
834
+ emitChunkMapped(g, source, stmt.start + 6, stmt.end);
835
+ return;
836
+ }
837
+ emitChunkMapped(g, source, stmt.start + 6, arg.start);
838
+ if (arg.type === 'JSXElement')
839
+ emitJSXElement(g, source, arg, opts);
840
+ else if (arg.type === 'JSXFragment')
841
+ emitJSXFragment(g, source, arg, opts);
842
+ else
843
+ emitChunkMapped(g, source, arg.start, arg.end);
844
+ emitChunkMapped(g, source, arg.end, stmt.end);
845
+ }
846
+ /**
847
+ * Emits a statement-mode component body (bare JSX, control flow,
848
+ * guard-clause returns). The body text starts directly with the first
849
+ * statement (no leading whitespace) and the `{ ... }` wrapping is emitted
850
+ * by the caller.
851
+ */
852
+ function emitBodyCore(g, source, stmts, opts) {
853
+ emitBody(g, source, stmts, '', opts);
854
+ }
855
+ /** Emits an expression-mode component body (`return <jsx>` or a bare expression). */
856
+ function emitExpressionBody(g, source, coreStart, coreEnd, stmts, opts) {
857
+ let pos = coreStart;
858
+ for (const stmt of stmts) {
859
+ if (!stmt)
479
860
  continue;
861
+ emitChunkMapped(g, source, pos, stmt.start);
862
+ pos = stmt.start;
863
+ if (isTrackDeclStatement(stmt)) {
864
+ emitTrackDeclStatement(g, source, stmt, '', true, opts);
480
865
  }
481
- i++;
866
+ else if (stmt.type === 'ReturnStatement') {
867
+ emitReturn(g, source, stmt, opts);
868
+ }
869
+ else {
870
+ emitChunkMapped(g, source, stmt.start, stmt.end);
871
+ }
872
+ pos = stmt.end;
482
873
  }
483
- return text;
874
+ emitChunkMapped(g, source, pos, coreEnd);
484
875
  }
485
876
  /**
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.
877
+ * Emits `text` (covering source range [base, end)) with its first
878
+ * identifier split into its own mapping so hover/rename/definition land
879
+ * exactly on the component name.
490
880
  */
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() + ' ';
881
+ function emitRestMapped(g, text, base, end) {
882
+ let i = 0;
883
+ while (i < text.length && !isIdentStart(text[i]))
884
+ i++;
885
+ if (i >= text.length) {
886
+ g.add(text, base, end);
887
+ return;
500
888
  }
501
- return text;
889
+ let j = i + 1;
890
+ while (j < text.length && isIdentChar(text[j]))
891
+ j++;
892
+ g.add(text.slice(0, i), base, base + i);
893
+ g.add(text.slice(i, j), base + i, base + j);
894
+ g.add(text.slice(j), base + j, end);
502
895
  }
503
- export function vskToTsx(source) {
896
+ function emitComponentHeader(g, source, start, bodyStart) {
897
+ const h = source.slice(start, bodyStart);
898
+ const k = findFirstKeyword(h, 'component');
899
+ if (k === -1) {
900
+ const { text, end } = stripTrailingClientRange(h);
901
+ if (!text.includes('(')) {
902
+ const trimmed = text.trimEnd();
903
+ g.add(trimmed, start, start + end - (text.length - trimmed.length));
904
+ g.addRaw('() ');
905
+ }
906
+ else {
907
+ g.add(text, start, start + end);
908
+ }
909
+ return;
910
+ }
911
+ g.add(h.slice(0, k), start, start + k);
912
+ g.add('function', start + k, start + k + 'component'.length);
913
+ const restBase = start + k + 'component'.length;
914
+ const { text: rest, end } = stripTrailingClientRange(h.slice(k + 'component'.length));
915
+ const transformed = h.slice(0, k) + 'function' + rest;
916
+ if (!transformed.includes('(')) {
917
+ const trimmed = rest.trimEnd();
918
+ emitRestMapped(g, trimmed, restBase, restBase + end - (rest.length - trimmed.length));
919
+ g.addRaw('() ');
920
+ }
921
+ else {
922
+ emitRestMapped(g, rest, restBase, restBase + end);
923
+ }
924
+ }
925
+ function emitComponent(g, source, node, start, opts) {
926
+ const body = node.body;
927
+ if (!body || body.start == null || body.end == null)
928
+ return;
929
+ emitComponentHeader(g, source, start, body.start);
930
+ const statementMode = isStatementMode(node);
931
+ const stmts = body.body ?? [];
932
+ if (statementMode) {
933
+ g.addRaw('{ ');
934
+ emitBodyCore(g, source, stmts, opts);
935
+ g.addRaw(' }');
936
+ }
937
+ else {
938
+ const raw = source.slice(body.start, body.end);
939
+ const rawLen = raw.length;
940
+ let s = 0;
941
+ let e = rawLen;
942
+ while (s < e && isWhitespaceChar(raw[s]))
943
+ s++;
944
+ while (e > s && isWhitespaceChar(raw[e - 1]))
945
+ e--;
946
+ const hasBraces = raw[s] === '{' && raw[e - 1] === '}';
947
+ let coreStart;
948
+ let coreEnd;
949
+ if (hasBraces) {
950
+ coreStart = body.start + s + 1;
951
+ coreEnd = body.start + e - 1;
952
+ }
953
+ else {
954
+ coreStart = body.start + s;
955
+ coreEnd = body.start + e;
956
+ }
957
+ while (coreStart < coreEnd && isWhitespaceChar(source[coreStart]))
958
+ coreStart++;
959
+ while (coreEnd > coreStart && isWhitespaceChar(source[coreEnd - 1]))
960
+ coreEnd--;
961
+ if (hasBraces)
962
+ g.addRaw('{ ');
963
+ emitExpressionBody(g, source, coreStart, coreEnd, stmts, opts);
964
+ if (hasBraces)
965
+ g.addRaw(' }');
966
+ }
967
+ }
968
+ function isModuleAst(ast) {
969
+ return (ast.body || []).some((s) => s.type === 'ImportDeclaration' ||
970
+ s.type === 'ExportNamedDeclaration' ||
971
+ s.type === 'ExportDefaultDeclaration' ||
972
+ s.type === 'ExportAllDeclaration');
973
+ }
974
+ export function compileVskCodegen(source, opts = {}) {
504
975
  let ast;
505
976
  try {
506
977
  ast = parse(source);
507
978
  }
508
- catch {
509
- return source;
979
+ catch (e) {
980
+ const pos = e.pos ?? 0;
981
+ return {
982
+ code: source,
983
+ mappings: [
984
+ {
985
+ sourceOffsets: [0],
986
+ generatedOffsets: [0],
987
+ lengths: [source.length],
988
+ generatedLengths: [source.length],
989
+ data: FULL_DATA,
990
+ },
991
+ ],
992
+ styleRegions: [],
993
+ errors: [{ message: e.message, start: pos, end: pos + 1 }],
994
+ };
510
995
  }
511
996
  const clean = blankForClauses(source, ast);
512
- const edits = [];
513
- for (const { node, start } of getComponents(ast)) {
997
+ const g = new VskGen();
998
+ const components = getComponents(ast).sort((a, b) => a.start - b.start);
999
+ let pos = 0;
1000
+ for (const { node, start } of components) {
514
1001
  const body = node.body;
515
1002
  if (!body || body.start == null || body.end == null)
516
1003
  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 });
1004
+ g.add(clean.slice(pos, start), pos, start);
1005
+ emitComponent(g, clean, node, start, opts);
1006
+ pos = body.end;
536
1007
  }
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}`;
1008
+ g.add(clean.slice(pos), pos, source.length);
1009
+ let code = g.code;
1010
+ if (isModuleAst(ast) && containsIdentifier(code, 'Head') && !isIdentifierImported(code, 'Head')) {
1011
+ g.prepend('declare const Head: (props: { children?: unknown }) => unknown;\n');
1012
+ code = g.code;
544
1013
  }
545
- return out;
1014
+ return { code, mappings: g.mappings, styleRegions: g.styleRegions, errors: [] };
1015
+ }
1016
+ export function vskToTsx(source) {
1017
+ return compileVskCodegen(source).code;
546
1018
  }
547
1019
  function isTypeDecl(stmt) {
548
1020
  return (stmt.type === 'TSInterfaceDeclaration' ||