partial-tsx 0.0.0 → 0.0.1

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 (3) hide show
  1. package/package.json +17 -9
  2. package/src/partial.ts +936 -0
  3. package/index.js +0 -1
package/package.json CHANGED
@@ -1,11 +1,19 @@
1
1
  {
2
2
  "name": "partial-tsx",
3
- "version": "0.0.0",
4
- "description": "Placeholder for partial-tsx",
5
- "main": "index.js",
6
- "scripts": {},
7
- "keywords": [],
8
- "author": "muspi-merol (https://www.npmjs.com/~muspi-merol)",
9
- "license": "UNLICENSED",
10
- "private": false
11
- }
3
+ "description": "Utilities for completing prefix-growing TSX.",
4
+ "version": "0.0.1",
5
+ "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/MindLab-Research/macaron-genui-demo",
9
+ "directory": "lib/partial-tsx"
10
+ },
11
+ "exports": {
12
+ ".": "./src/partial.ts"
13
+ },
14
+ "files": [
15
+ "src/**/*",
16
+ "!src/**/*.test.*"
17
+ ],
18
+ "sideEffects": false
19
+ }
package/src/partial.ts ADDED
@@ -0,0 +1,936 @@
1
+ const VOID_TAGS = new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"]);
2
+ const BLOCK_FOLLOWERS: Record<string, string> = { "{": "}", "(": ")", "[": "]" };
3
+ const BLOCK_CLOSERS = new Set(Object.values(BLOCK_FOLLOWERS));
4
+ type CompletionFrame = { type: "block"; closer: string; statement: boolean; open?: number } | { type: "jsx"; tag: string };
5
+
6
+ const stripMarkdownFence = (code: string) => {
7
+ let next = code.trim();
8
+ next = next.replace(/^```(?:[^\r\n]*)?(?:\r?\n|$)/i, "");
9
+ next = next.replace(/\s*```$/i, "");
10
+ return next;
11
+ };
12
+ const maskExportScanRange = (chars: string[], start: number, end: number) => {
13
+ for (let index = start; index < end; index += 1) if (chars[index] !== "\n") chars[index] = " ";
14
+ };
15
+ const findStringLiteralEnd = (code: string, index: number) => {
16
+ const quote = code[index];
17
+ index += 1;
18
+ while (index < code.length) {
19
+ if (code[index] === "\\") index += 2;
20
+ else {
21
+ if (code[index] === quote) return index + 1;
22
+ index += 1;
23
+ }
24
+ }
25
+ return code.length;
26
+ };
27
+ const findTemplateLiteralEnd = (code: string, index: number) => {
28
+ index += 1;
29
+ while (index < code.length) {
30
+ if (code[index] === "\\") index += 2;
31
+ else if (code[index] === "`") return index + 1;
32
+ // A `${ … }` substitution can hold nested template literals (`${`inner`}`); recurse into it so an inner backtick is not
33
+ // mistaken for the outer literal's terminator, which would leave the substitution body unmasked in the export scan. Skip
34
+ // strings/templates inside the substitution so their braces (`${"{"}`) do not throw off the brace-depth tracking.
35
+ else if (code[index] === "$" && code[index + 1] === "{") {
36
+ index += 2;
37
+ for (let depth = 1; index < code.length && depth > 0; index += 1) {
38
+ if (code[index] === "/" && code[index + 1] === "/") {
39
+ const next = code.indexOf("\n", index + 2);
40
+ index = next === -1 ? code.length : next;
41
+ } else if (code[index] === "/" && code[index + 1] === "*") {
42
+ const next = code.indexOf("*/", index + 2);
43
+ index = next === -1 ? code.length : next + 1;
44
+ } else if (isLikelyRegexLiteralStart(code, index)) index = findRegexLiteralEnd(code, index) - 1;
45
+ else if (code[index] === "`") index = findTemplateLiteralEnd(code, index) - 1;
46
+ else if (code[index] === '"' || code[index] === "'") index = findStringLiteralEnd(code, index) - 1;
47
+ else if (code[index] === "{") depth += 1;
48
+ else if (code[index] === "}") depth -= 1;
49
+ }
50
+ } else index += 1;
51
+ }
52
+ return code.length;
53
+ };
54
+ const updateJsxStackForTag = (stack: CompletionFrame[], tag: string) => {
55
+ const match = /^<\/?\s*([A-Za-z][\w$.-]*)/.exec(tag);
56
+ const tagName = match?.[1] ?? (tag === "<>" || tag === "</>" ? "" : null);
57
+ if (tagName === null) return;
58
+ if (tag.startsWith("</")) {
59
+ const frame = stack.at(-1);
60
+ if (frame?.type === "jsx" && frame.tag === tagName) stack.pop();
61
+ return;
62
+ }
63
+ if (!tag.endsWith("/>") && !VOID_TAGS.has(tagName)) stack.push({ type: "jsx", tag: tagName });
64
+ };
65
+ const maskIgnoredTextForExportScan = (code: string) => {
66
+ // split("") keeps code-unit indexing aligned with code[index]; spread would split astral chars (emoji) by code point and desync the two.
67
+ const chars = code.split("");
68
+ const stack: CompletionFrame[] = [];
69
+ for (let index = 0; index < code.length; index += 1) {
70
+ const char = code[index];
71
+ if (isInsideJsxText(stack) && char !== "<" && char !== "{") {
72
+ if (chars[index] !== "\n") chars[index] = " ";
73
+ continue;
74
+ }
75
+ if (char === "/" && code[index + 1] === "/") {
76
+ const end = code.indexOf("\n", index + 2);
77
+ const next = end === -1 ? code.length : end;
78
+ maskExportScanRange(chars, index, next);
79
+ index = next;
80
+ continue;
81
+ }
82
+ if (char === "/" && code[index + 1] === "*") {
83
+ const close = code.indexOf("*/", index + 2);
84
+ const next = close === -1 ? code.length : close + 2;
85
+ maskExportScanRange(chars, index, next);
86
+ index = next - 1;
87
+ continue;
88
+ }
89
+ if (isLikelyRegexLiteralStart(code, index)) {
90
+ const next = findRegexLiteralEnd(code, index);
91
+ maskExportScanRange(chars, index, next);
92
+ index = next - 1;
93
+ continue;
94
+ }
95
+ if (char === '"' || char === "'") {
96
+ const next = findStringLiteralEnd(code, index);
97
+ maskExportScanRange(chars, index, next);
98
+ index = next - 1;
99
+ continue;
100
+ }
101
+ if (char === "`") {
102
+ const next = findTemplateLiteralEnd(code, index);
103
+ maskExportScanRange(chars, index, next);
104
+ index = next - 1;
105
+ continue;
106
+ }
107
+ if (char === "<" && shouldStartJsxTag(code, index, stack)) {
108
+ const closeIndex = findJsxTagEnd(code, index);
109
+ if (closeIndex === -1) break;
110
+ const tag = code.slice(index, closeIndex + 1);
111
+ maskExportScanRange(chars, index, closeIndex + 1);
112
+ chars[index] = "<"; // keep the leading `<` so `export default <Card/>` is still detectable after masking
113
+ index = closeIndex;
114
+ updateJsxStackForTag(stack, tag);
115
+ continue;
116
+ }
117
+ if (char in BLOCK_FOLLOWERS) {
118
+ stack.push({ type: "block", closer: BLOCK_FOLLOWERS[char], statement: false });
119
+ continue;
120
+ }
121
+ if (BLOCK_CLOSERS.has(char)) {
122
+ const frame = stack.at(-1);
123
+ if (frame?.type === "block" && frame.closer === char) stack.pop();
124
+ }
125
+ }
126
+ return chars.join("");
127
+ };
128
+ const findBalancedTypeArgumentsEnd = (code: string, index: number) => {
129
+ if (code[index] !== "<") return index;
130
+ let depth = 0;
131
+ for (; index < code.length; index += 1) {
132
+ const char = code[index];
133
+ if (char === "<") depth += 1;
134
+ else if (char === ">" && code[index - 1] === "=") continue;
135
+ else if (char === ">") {
136
+ depth -= 1;
137
+ if (depth === 0) return index + 1;
138
+ }
139
+ }
140
+ return -1;
141
+ };
142
+ const findWrappedComponentCallOpen = (code: string, index: number) => {
143
+ index = findBalancedTypeArgumentsEnd(code, index);
144
+ if (index === -1) return -1;
145
+ while (/\s/.test(code[index] ?? "")) index += 1;
146
+ return code[index] === "(" ? index : -1;
147
+ };
148
+ const isWrappedImplementationFunction = (code: string, index: number) => /\b(?:memo|forwardRef)\s*(?:<[\s\S]*>)?\s*\($/.test(code.slice(0, index).trimEnd());
149
+
150
+ const findLastExportableComponent = (code: string) => {
151
+ const candidates: Array<{ name: string; index: number }> = [];
152
+ for (const match of code.matchAll(/\bfunction\s+([A-Z][\w$]*)\s*\(/g)) {
153
+ if (!isWrappedImplementationFunction(code, match.index ?? 0)) candidates.push({ name: match[1], index: match.index ?? 0 });
154
+ }
155
+ for (const match of code.matchAll(/\bclass\s+([A-Z][\w$]*)\s+extends\s+(?:(?:React\.)?(?:Pure)?Component|[A-Za-z_$][\w$]*\.Component)\b/g)) candidates.push({ name: match[1], index: match.index ?? 0 });
156
+ for (const match of code.matchAll(/\b(?:const|let|var)\s+([A-Z][\w$]*)\s*(?::[^=]+)?=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)?\s*=>/g)) candidates.push({ name: match[1], index: match.index ?? 0 });
157
+ for (const match of code.matchAll(/\b(?:const|let|var)\s+([A-Z][\w$]*)\s*(?::[^=]+)?=\s*(?:[A-Za-z_$][\w$]*\.)?(?:memo|forwardRef)\s*/g)) {
158
+ if (findWrappedComponentCallOpen(code, (match.index ?? 0) + match[0].length) !== -1) candidates.push({ name: match[1], index: match.index ?? 0 });
159
+ }
160
+ return [...candidates].toSorted((left, right) => left.index - right.index).at(-1)?.name ?? null;
161
+ };
162
+ // `export default` is also valid mid-line after a `;` or block-closing `}` (`;export default …`, `if(a){} export default …`); match those
163
+ // so the scan does not miss an existing default and append a second one (`Duplicate export of 'default'`).
164
+ const hasDefaultExportDeclaration = (code: string) => /(?:^|[\n;}])\s*export\s+default\s+(?:async\s+)?(?:function|class|[A-Za-z_$]|\(|<)/m.test(code) || /(?:^|[\n;}])\s*export\s*\{[^}]*\b(?:as\s+default|default\s*(?:,|}))[^}]*\}/m.test(code);
165
+
166
+ // React hooks the model commonly forgets to import. Stay conservative: anything outside this set (custom hooks, react-dom hooks like useFormStatus) is left alone.
167
+ const REACT_HOOK_NAMES = new Set([
168
+ "useState",
169
+ "useEffect",
170
+ "useRef",
171
+ "useMemo",
172
+ "useCallback",
173
+ "useReducer",
174
+ "useContext",
175
+ "useLayoutEffect",
176
+ "useInsertionEffect",
177
+ "useTransition",
178
+ "useDeferredValue",
179
+ "useId",
180
+ "useImperativeHandle",
181
+ "useDebugValue",
182
+ "useSyncExternalStore",
183
+ "useActionState",
184
+ "useOptimistic",
185
+ "useEffectEvent",
186
+ ]);
187
+
188
+ const isUnmaskedImportMatch = (code: string, maskedCode: string, match: RegExpExecArray) => {
189
+ const importIndex = (match.index ?? 0) + match[0].indexOf("import");
190
+ return code.slice(importIndex, importIndex + "import".length) === maskedCode.slice(importIndex, importIndex + "import".length);
191
+ };
192
+ const collectImportedBindings = (code: string, maskedCode: string) => {
193
+ const bindings = new Set<string>();
194
+ for (const match of code.matchAll(/(?:^|[;\n])\s*import\s+([\s\S]*?)\s+from\s+["'][^"']*["']/g)) {
195
+ if (!isUnmaskedImportMatch(code, maskedCode, match)) continue;
196
+ const clause = match[1].trim();
197
+ if (!clause.startsWith("{") && !clause.startsWith("*")) {
198
+ const defaultMatch = /^([A-Za-z_$][\w$]*)/.exec(clause);
199
+ if (defaultMatch) bindings.add(defaultMatch[1]);
200
+ }
201
+ const namespaceMatch = /\*\s+as\s+([A-Za-z_$][\w$]*)/.exec(clause);
202
+ if (namespaceMatch) bindings.add(namespaceMatch[1]);
203
+ const namedMatch = /\{([\s\S]*?)\}/.exec(clause);
204
+ if (!namedMatch) continue;
205
+ for (const raw of namedMatch[1].split(",")) {
206
+ const item = raw.trim().replace(/^type\s+/, "");
207
+ if (!item) continue;
208
+ const aliased = /^([A-Za-z_$][\w$]*)\s+as\s+([A-Za-z_$][\w$]*)\s*$/.exec(item);
209
+ if (aliased) {
210
+ // Track both the original name and the alias so `useState as useS` blocks us from re-importing `useState`.
211
+ bindings.add(aliased[1]);
212
+ bindings.add(aliased[2]);
213
+ continue;
214
+ }
215
+ const name = /^([A-Za-z_$][\w$]*)/.exec(item)?.[1];
216
+ if (name) bindings.add(name);
217
+ }
218
+ }
219
+ return bindings;
220
+ };
221
+
222
+ const injectMissingReactHookImports = (code: string, maskedCode: string) => {
223
+ if (!/\buse[A-Z]/.test(maskedCode)) return code;
224
+ const bindings = collectImportedBindings(code, maskedCode);
225
+ const missing = new Set<string>();
226
+ for (const match of maskedCode.matchAll(/\b(use[A-Z][\w$]*)\b/g)) {
227
+ const name = match[1];
228
+ if (!REACT_HOOK_NAMES.has(name) || bindings.has(name)) continue;
229
+ if (maskedCode[(match.index ?? 0) - 1] === ".") continue;
230
+ missing.add(name);
231
+ }
232
+ if (missing.size === 0) return code;
233
+ const additions = [...missing].join(", ");
234
+ // Skips `import type { ... } from "react"` so we don't merge value hooks into a type-only import; supports mixed default+named imports like `import React, { useState } from "react"`.
235
+ const existing = /(?:^|[;\n])\s*import\s+(?:[A-Za-z_$][\w$]*\s*,\s*)?\{([^}]*)\}\s+from\s+["']react["']/.exec(code);
236
+ if (existing) {
237
+ if (!isUnmaskedImportMatch(code, maskedCode, existing)) return `import { ${additions} } from "react";\n${code}`;
238
+ const inner = existing[1].trim().replace(/,\s*$/, "");
239
+ const merged = inner ? `${inner}, ${additions}` : additions;
240
+ const rewritten = existing[0].replace(/\{[^}]*\}/, `{ ${merged} }`);
241
+ return `${code.slice(0, existing.index)}${rewritten}${code.slice(existing.index + existing[0].length)}`;
242
+ }
243
+ return `import { ${additions} } from "react";\n${code}`;
244
+ };
245
+
246
+ const isLikelyJsxTagStart = (code: string, index: number) => {
247
+ const next = code[index + 1] ?? "";
248
+ if (!/[A-Za-z/>]/.test(next)) return false;
249
+ let previous = index - 1;
250
+ while (previous >= 0 && /\s/.test(code[previous])) previous -= 1;
251
+ if (previous < 0) return true;
252
+ const previousChar = code[previous];
253
+ if ("([{=:?!,&|^~*/%+-;".includes(previousChar)) return true;
254
+ if (previousChar === ">" && code[previous - 1] === "=") return true;
255
+ if (/[)\]}"'`]/.test(previousChar)) return false;
256
+ if (/[A-Za-z0-9_$]/.test(previousChar)) {
257
+ let word = "";
258
+ for (let cursor = previous; cursor >= 0 && /[A-Za-z0-9_$]/.test(code[cursor]); cursor -= 1) word = code[cursor] + word;
259
+ return new Set(["return", "throw", "await", "yield", "case", "default", "else", "typeof", "void", "delete", "new"]).has(word);
260
+ }
261
+ return previousChar !== ">";
262
+ };
263
+
264
+ const isLikelyStatementBlock = (code: string, index: number) => {
265
+ if (code[index] !== "{") return false;
266
+ const before = code.slice(0, index).trimEnd();
267
+ return /\bfunction(?:\s+[\w$]+)?\s*\([^)]*\)$/.test(before) || /\b(?:if|for|while|switch|catch|with)\s*\([^)]*\)$/.test(before) || /(?:^|[^\w$])(?:else|try|finally|do)$/.test(before) || before.endsWith("=>");
268
+ };
269
+
270
+ const findJsxTagEnd = (code: string, start: number) => {
271
+ const stack: string[] = [];
272
+ let quote: string | null = null;
273
+ for (let index = start + 1; index < code.length; index += 1) {
274
+ const char = code[index];
275
+ if (quote) {
276
+ if (char === "\\") index += 1;
277
+ else if (char === quote) quote = null;
278
+ continue;
279
+ }
280
+ if (char === '"' || char === "'" || char === "`") {
281
+ quote = char;
282
+ continue;
283
+ }
284
+ if (char in BLOCK_FOLLOWERS) {
285
+ stack.push(BLOCK_FOLLOWERS[char]);
286
+ continue;
287
+ }
288
+ if (BLOCK_CLOSERS.has(char)) {
289
+ if (stack.at(-1) === char) stack.pop();
290
+ continue;
291
+ }
292
+ if (char === ">" && stack.length === 0) return index;
293
+ }
294
+ return -1;
295
+ };
296
+ const findJsxAttributeExpressionEnd = (code: string, index: number) => {
297
+ let depth = 1;
298
+ while (index < code.length) {
299
+ if (code[index] === "/" && code[index + 1] === "/") {
300
+ const end = code.indexOf("\n", index + 2);
301
+ index = end === -1 ? code.length : end;
302
+ continue;
303
+ }
304
+ if (code[index] === "/" && code[index + 1] === "*") {
305
+ const close = code.indexOf("*/", index + 2);
306
+ index = close === -1 ? code.length : close + 2;
307
+ continue;
308
+ }
309
+ if (isLikelyRegexLiteralStart(code, index)) {
310
+ index = findRegexLiteralEnd(code, index);
311
+ continue;
312
+ }
313
+ if (code[index] === '"' || code[index] === "'") {
314
+ index = findStringLiteralEnd(code, index);
315
+ continue;
316
+ }
317
+ if (code[index] === "`") {
318
+ index = findTemplateLiteralEnd(code, index);
319
+ continue;
320
+ }
321
+ if (code[index] === "{") depth += 1;
322
+ else if (code[index] === "}") {
323
+ depth -= 1;
324
+ if (depth === 0) return index;
325
+ }
326
+ index += 1;
327
+ }
328
+ return -1;
329
+ };
330
+ const completeIncompleteJsxTag = (code: string, start: number) => {
331
+ if (code[start + 1] === "/") return null;
332
+ const tagMatch = /^<\s*([A-Za-z][\w$.-]*)/.exec(code.slice(start));
333
+ if (!tagMatch) return null;
334
+ const [matched, tagName] = tagMatch;
335
+ // Tag name may still be streaming (`inp` may become `input`, `Sur` may become `Surface`); completing now would render the wrong element or throw `Sur is not defined` and remount. Wait for a separator (space/`>`/attr).
336
+ if (start + matched.length >= code.length) return null;
337
+ let index = start;
338
+ let tag = "";
339
+ while (index < code.length) {
340
+ const char = code[index];
341
+ if (char === "`") {
342
+ // A backtick is illegal as a JSX attribute value (`title=`x`` → "JSX value should be either an expression or a quoted JSX text"); template literals are only valid inside `{...}`. Drop the attribute.
343
+ tag = tag.replace(/\s*[\w$:-]+\s*=\s*$/, "");
344
+ index = findTemplateLiteralEnd(code, index);
345
+ continue;
346
+ }
347
+ if (char === '"' || char === "'") {
348
+ const end = findStringLiteralEnd(code, index);
349
+ let literal = code.slice(index, end);
350
+ const closed = literal.length > 1 && literal.endsWith(char);
351
+ // An unterminated string value is safe to close by synthesizing the missing quote — worst case the frame's text is briefly short, never a throw or a wrong binding. Host and component alike.
352
+ if (!closed) {
353
+ // A trailing odd backslash is an incomplete escape (`title="foo\`); synthesizing the quote would make `\"` an escaped quote, leaving the string unterminated. Drop the dangling backslash before closing.
354
+ if (/\\*$/.exec(literal)![0].length % 2) literal = literal.slice(0, -1);
355
+ literal += char;
356
+ }
357
+ tag += literal;
358
+ index = end;
359
+ continue;
360
+ }
361
+ if (char === "{") {
362
+ const expressionEnd = findJsxAttributeExpressionEnd(code, index + 1);
363
+ // An unterminated expression value must not be completed: mid-stream `{x` may still become `x.y`, `xyz`, or `x + 1`, so synthesizing `}` binds the frame to the wrong (or undefined) value and can throw. Drop the attribute, keep the tag. Host and component alike.
364
+ if (expressionEnd === -1) {
365
+ tag = tag.replace(/\s*[\w$:-]+\s*=\s*$/, "");
366
+ index = code.length;
367
+ continue;
368
+ }
369
+ const expression = code.slice(index + 1, expressionEnd);
370
+ const completedExpression = completeIncompleteExpressionPrefix(completePartialTsx(expression), true);
371
+ if (!expression.trim() || !completedExpression.trim() || /^(?:const|let|var)\b/.test(completedExpression.trim())) {
372
+ tag = tag.replace(/\s*[\w$:-]+\s*=\s*$/, "");
373
+ index = expressionEnd + 1;
374
+ continue;
375
+ }
376
+ tag += `{${completedExpression}}`;
377
+ index = expressionEnd + 1;
378
+ continue;
379
+ }
380
+ tag += char;
381
+ index += 1;
382
+ if (char === ">") return { tag: VOID_TAGS.has(tagName) ? tag.replace(/\s*\/?\s*>$/, " />") : tag, tagName };
383
+ }
384
+ // A dangling `prop=` would close as `prop=>` (SWC reads `=>` as an arrow function), breaking the whole module.
385
+ if (/=\s*$/.test(tag)) tag = tag.replace(/\s*[\w$:-]+\s*=\s*$/, "");
386
+ // Otherwise a trailing name with no following space is still streaming (`valu` may become `value`); a trailing space commits it as a boolean attr.
387
+ else if (!/\s$/.test(tag)) tag = tag.replace(/\s+[A-Za-z_$][\w$:-]*$/, "");
388
+ const completedTag = VOID_TAGS.has(tagName) ? `${tag.replace(/\s*\/\s*$/, "")} />` : `${tag}>`;
389
+ return { tag: completedTag, tagName };
390
+ };
391
+
392
+ type PushOutput = (value: string) => void;
393
+
394
+ const consumeStringLiteral = (code: string, index: number, push: PushOutput) => {
395
+ const quote = code[index];
396
+ push(quote);
397
+ index += 1;
398
+ let closed = false;
399
+ while (index < code.length) {
400
+ const next = code[index];
401
+ if (next === "\\") {
402
+ push(code.slice(index, index + 2));
403
+ index += 2;
404
+ continue;
405
+ }
406
+ push(next);
407
+ index += 1;
408
+ if (next === quote) {
409
+ closed = true;
410
+ break;
411
+ }
412
+ }
413
+ if (!closed) push(quote);
414
+ return index;
415
+ };
416
+
417
+ const closeFrame = (frame: CompletionFrame) => {
418
+ if (frame.type === "jsx") return frame.tag ? `</${frame.tag}>` : "</>";
419
+ return frame.closer;
420
+ };
421
+
422
+ const isExpressionEnd = (value: string) => /(?:<\/[\w$.-]+>|<\/>|\/>|[>`'")\]\w])$/.test(value.trimEnd());
423
+ const collectCompletionStackBefore = (code: string, end: number) => {
424
+ const stack: CompletionFrame[] = [];
425
+ for (let index = 0; index < end; index += 1) {
426
+ const char = code[index];
427
+ if (!isInsideJsxText(stack) && char === "/" && code[index + 1] === "/") {
428
+ const newlineIndex = code.indexOf("\n", index + 2);
429
+ index = (newlineIndex === -1 ? end : Math.min(newlineIndex, end)) - 1;
430
+ continue;
431
+ }
432
+ if (!isInsideJsxText(stack) && char === "/" && code[index + 1] === "*") {
433
+ const closeIndex = code.indexOf("*/", index + 2);
434
+ index = (closeIndex === -1 ? end : Math.min(closeIndex + 2, end)) - 1;
435
+ continue;
436
+ }
437
+ if (!isInsideJsxText(stack) && isLikelyRegexLiteralStart(code, index)) {
438
+ index = Math.min(findRegexLiteralEnd(code, index), end) - 1;
439
+ continue;
440
+ }
441
+ if (!isInsideJsxText(stack) && (char === '"' || char === "'")) {
442
+ index = Math.min(findStringLiteralEnd(code, index), end) - 1;
443
+ continue;
444
+ }
445
+ if (!isInsideJsxText(stack) && char === "`") {
446
+ index = Math.min(findTemplateLiteralEnd(code, index), end) - 1;
447
+ continue;
448
+ }
449
+ if (char === "<" && shouldStartJsxTag(code, index, stack)) {
450
+ const closeIndex = findJsxTagEnd(code, index);
451
+ if (closeIndex === -1 || closeIndex >= end) break;
452
+ updateJsxStackForTag(stack, code.slice(index, closeIndex + 1));
453
+ index = closeIndex;
454
+ continue;
455
+ }
456
+ if (char in BLOCK_FOLLOWERS) stack.push({ type: "block", closer: BLOCK_FOLLOWERS[char], statement: isLikelyStatementBlock(code, index) });
457
+ else if (BLOCK_CLOSERS.has(char)) {
458
+ const frame = stack.at(-1);
459
+ if (frame?.type === "block" && frame.closer === char) stack.pop();
460
+ }
461
+ }
462
+ return stack;
463
+ };
464
+ const endsWithJsxTagHead = (value: string) => {
465
+ const lastOpen = value.lastIndexOf("<");
466
+ if (lastOpen === -1 || !shouldStartJsxTag(value, lastOpen, collectCompletionStackBefore(value, lastOpen))) return false;
467
+ const tail = value.slice(lastOpen);
468
+ return /^<\/?(?:[A-Za-z][\w$.-]*|$)/.test(tail) && findJsxTagEnd(value, lastOpen) === -1;
469
+ };
470
+ const hasCompleteRegexLiteralEndingAt = (value: string, endIndex: number) => {
471
+ for (let index = 0; index <= endIndex; index += 1) {
472
+ if (value[index] !== "/" || !isLikelyRegexLiteralStart(value, index)) continue;
473
+ const end = findRegexLiteralEnd(value, index);
474
+ if (end === endIndex + 1) return true;
475
+ if (end > index) index = end - 1;
476
+ }
477
+ return false;
478
+ };
479
+ const dropTrailingOperator = (value: string, pattern: RegExp) => {
480
+ const match = pattern.exec(value);
481
+ if (!match) return value;
482
+ const operator = match[1];
483
+ const operatorIndex = value.indexOf(operator, match.index);
484
+ const before = value.slice(0, match.index).trimEnd();
485
+ if (operator === "=>") return `${before} => null`;
486
+ if (operator === "/" && hasCompleteRegexLiteralEndingAt(value, operatorIndex)) return value;
487
+ if ((operator === ">" || operator === ">=") && endsWithJsxTagHead(before)) return value;
488
+ return value.slice(0, match.index);
489
+ };
490
+ const dropTrailingIncompleteExpressionOperator = (value: string) => dropTrailingOperator(value, /\s*(>>>=?|>>=?|<<=?|===|!==|=>|\*\*=|\?\?=|&&=|\|\|=|[+\-*/%&|^]=|\?\?|\?\.|\*\*|[=!<>]=?|&&|\|\||[+\-*/%&|^~]|(?<![.\d])\.)\s*$/s);
491
+ // Blanks out string/template/regex/comment spans (but not JSX) so angle-bracket counting in `dropTrailingIncompleteStatement`
492
+ // only sees real `<`/`>` operators and type arguments, never a literal `<` (`"a<b"`) or a `</div>` closer.
493
+ const maskLiteralSpans = (body: string) => {
494
+ let out = "";
495
+ let index = 0;
496
+ while (index < body.length) {
497
+ const char = body[index];
498
+ if (char === "/" && body[index + 1] === "/") {
499
+ const end = body.indexOf("\n", index + 2);
500
+ const stop = end === -1 ? body.length : end;
501
+ out += " ".repeat(stop - index);
502
+ index = stop;
503
+ } else if (char === "/" && body[index + 1] === "*") {
504
+ const end = body.indexOf("*/", index + 2);
505
+ const stop = end === -1 ? body.length : end + 2;
506
+ out += " ".repeat(stop - index);
507
+ index = stop;
508
+ } else if (char === '"' || char === "'") {
509
+ const stop = findStringLiteralEnd(body, index);
510
+ out += " ".repeat(stop - index);
511
+ index = stop;
512
+ } else if (char === "`") {
513
+ const stop = findTemplateLiteralEnd(body, index);
514
+ out += " ".repeat(stop - index);
515
+ index = stop;
516
+ } else if (isLikelyRegexLiteralStart(body, index)) {
517
+ const stop = findRegexLiteralEnd(body, index);
518
+ out += " ".repeat(stop - index);
519
+ index = stop;
520
+ } else {
521
+ out += char;
522
+ index += 1;
523
+ }
524
+ }
525
+ return out;
526
+ };
527
+ // Blank out operators that share the `<` glyph so the angle-bracket counter never mistakes them for type brackets: `=>` (arrow,
528
+ // whose `>` would otherwise close a generic early in `useCallback<(x) => void`), `<<`/`<<=` (left-shift, whose `<`s would read as
529
+ // phantom openers in `1 << 2`), and `<=` (a `<` that can't open a generic, sparing `a <= b`). `>>`/`>>>`/`>=` are left alone so
530
+ // nested generic closers (`Map<K, Array<V>>`) still count.
531
+ const neutralizeAngleOperators = (masked: string) => masked.replace(/=>|<<=?|<=/g, (op) => " ".repeat(op.length));
532
+ // True when the masked body has a `<` that never closes AND sits in value position (after an identifier / `)` / `]`) with no
533
+ // bracket past it that would close out the surrounding expression — i.e. a type-argument list whose `>` hasn't streamed yet
534
+ // (`useState<number | null`). Excludes a `<` right after `=` (a generic-arrow `<T,>(x)=>x`, completed elsewhere) and any `<`
535
+ // followed by a `)`/`]`/`}` that ends the initializer (`items.filter((i) => i < 3)` — a real comparison, already terminated).
536
+ const hasUnclosedTypeArgument = (body: string) => {
537
+ const masked = neutralizeAngleOperators(body);
538
+ let depth = 0;
539
+ let firstUnclosed = -1;
540
+ for (let index = 0; index < masked.length; index += 1) {
541
+ const char = masked[index];
542
+ if (char === "<") {
543
+ if (depth === 0) firstUnclosed = index;
544
+ depth += 1;
545
+ } else if (char === ">") {
546
+ if (depth > 0) depth -= 1;
547
+ if (depth === 0) firstUnclosed = -1;
548
+ }
549
+ }
550
+ if (firstUnclosed < 0) return false;
551
+ if (!/[\w$)\]]$/.test(masked.slice(0, firstUnclosed).trimEnd())) return false;
552
+ let bracketDepth = 0;
553
+ for (let index = firstUnclosed + 1; index < masked.length; index += 1) {
554
+ const char = masked[index];
555
+ if (char === "(" || char === "[" || char === "{") bracketDepth += 1;
556
+ else if (char === ")" || char === "]" || char === "}") {
557
+ bracketDepth -= 1;
558
+ if (bracketDepth < 0) return false;
559
+ }
560
+ }
561
+ return true;
562
+ };
563
+ // Locate the last `const`/`let`/`var` whose declaration runs to EOF without a depth-0 `;` terminator, scanning the
564
+ // literal-masked text so a `;`/`{`/`<` sitting inside a string or comment (`const s = "a; const x = useState<T"`) is never
565
+ // mistaken for a statement boundary. A declaration can begin at the buffer start or right after `{`, `}`, `;`, or `=>`; we keep
566
+ // the latest one that isn't closed by a depth-0 `;` or by a `)`/`]`/`}` that pops out of its enclosing block.
567
+ const locateTrailingDeclaration = (value: string) => {
568
+ const masked = maskLiteralSpans(value);
569
+ const re = /(?:^|[{};\n]|=>)\s*(const|let|var)\b/g;
570
+ let match: RegExpExecArray | null;
571
+ let start = -1;
572
+ let kind = "";
573
+ while ((match = re.exec(masked))) {
574
+ const declarationStart = match.index + match[0].length - match[1].length;
575
+ let depth = 0;
576
+ let terminated = false;
577
+ for (let index = declarationStart + match[1].length; index < masked.length; index += 1) {
578
+ const char = masked[index];
579
+ if (char === "(" || char === "[" || char === "{") depth += 1;
580
+ else if (char === ")" || char === "]" || char === "}") {
581
+ if (depth === 0) {
582
+ terminated = true;
583
+ break;
584
+ }
585
+ depth -= 1;
586
+ } else if (char === ";" && depth === 0) {
587
+ terminated = true;
588
+ break;
589
+ }
590
+ }
591
+ if (!terminated) {
592
+ start = declarationStart;
593
+ kind = match[1];
594
+ }
595
+ }
596
+ return start === -1 ? null : { start, kind, body: value.slice(start + kind.length) };
597
+ };
598
+ const dropTrailingIncompleteStatement = (value: string) => {
599
+ const declaration = locateTrailingDeclaration(value);
600
+ if (!declaration) return value;
601
+ const { start, kind, body } = declaration;
602
+ // Net bracket depth of the initializer: a trailing `=` only truncates the declaration's own initializer when we're
603
+ // back at top level. Inside an already-open arrow/function body (`() => { ...; x =`) the `=` belongs to a nested
604
+ // statement the block-frame completer will close, so rolling back the whole declaration would over-strip and over-close.
605
+ let initializerDepth = 0;
606
+ for (const char of maskLiteralSpans(body)) {
607
+ if (char === "(" || char === "[" || char === "{") initializerDepth += 1;
608
+ else if (char === ")" || char === "]" || char === "}") initializerDepth -= 1;
609
+ }
610
+ const incompleteInitializer = initializerDepth <= 0 && /=\s*$/.test(body);
611
+ // An initializer truncated mid-generic (`useState<number | null`) is valid-looking JS where SWC reads `<` as a comparison
612
+ // and keeps `number` as a bare value reference, crashing at runtime; drop the statement until its `>` closer streams in.
613
+ const unclosedGenericInitializer = body.includes("=") && hasUnclosedTypeArgument(maskLiteralSpans(body));
614
+ const missingDeclaration = !body.trim() || (!body.includes("=") && (kind === "const" || /^[\s,]*(?:\[|\{)/.test(body)));
615
+ if (!incompleteInitializer && !missingDeclaration && !unclosedGenericInitializer) return value;
616
+ return value.slice(0, start);
617
+ };
618
+ const completeTrailingIncompleteTernary = (value: string) => {
619
+ const masked = maskIgnoredTextForExportScan(value);
620
+ if (/(?<!\?)\?(?![.?])[^?;{}]*:\s*$/s.test(masked)) return `${value} null`;
621
+ const match = /(?<!\?)\?(?![.?])(?<tail>[^?:;{}]*)$/s.exec(masked);
622
+ if (!match?.groups) return value;
623
+ return value.slice(match.index + 1).trim() ? `${value} : null` : `${value} null : null`;
624
+ };
625
+ // A second `function` declaration that has only streamed up to its keyword/name (no parameter list yet) leaves a dangling
626
+ // `function`/`function Name` that SWC rejects with `Expected ident`/`Expected '('`, blanking the already-rendered prior
627
+ // declaration. Drop it back to the preceding complete statement until its `(` arrives.
628
+ const dropTrailingIncompleteFunctionDeclaration = (value: string) => {
629
+ const match = /(?:^|[}\n;])\s*((?:export\s+(?:default\s+)?)?(?:async\s+)?function\b\s*\*?\s*[A-Za-z_$]?[\w$]*\s*)$/.exec(value);
630
+ return match ? value.slice(0, match.index + (match[0].length - match[1].length)) : value;
631
+ };
632
+ const completeIncompleteExpressionPrefix = (value: string, dropStatement = false) => completeTrailingIncompleteTernary(dropTrailingIncompleteExpressionOperator(dropStatement ? dropTrailingIncompleteFunctionDeclaration(dropTrailingIncompleteStatement(value)) : value));
633
+ const fillEmptyReturnExpression = (value: string) => {
634
+ if (/\breturn\s*$/.test(value)) return `${value} null`;
635
+ return value.replace(/(\breturn\s*\(\s*)\)$/s, "$1null)");
636
+ };
637
+ // A non-statement bracket frame gets its expression prefix completed (and possibly dropped). Completing can shorten the output
638
+ // two different ways: a trailing operator/dot trim (the frame's own `{`/`[` survives, so we still close it) or a whole-declaration
639
+ // rollback that removes the frame's own opening bracket plus every nested opener — e.g. `useState<{ nested: { count` or an array
640
+ // initializer `[{ foo: useState<number`. We tell them apart with `frame.open` (the opener's offset): if the completed text no
641
+ // longer reaches that offset, the opener is gone, so we emit no closer and report `dropped` for the caller to skip orphaned
642
+ // inner frames whose openers were inside the rolled-back span.
643
+ const completeBlockFrame = (output: string, frame: Extract<CompletionFrame, { type: "block" }>) => {
644
+ if (frame.statement) return { text: `${output}${frame.closer}`, dropped: false };
645
+ const completed = completeIncompleteExpressionPrefix(output, true);
646
+ const openerGone = frame.open !== undefined ? completed.length <= frame.open : completed.length < output.length;
647
+ if (openerGone) return { text: completed, dropped: true };
648
+ return { text: `${completed}${frame.closer}`, dropped: false };
649
+ };
650
+ // An empty `(` is a legal empty argument list only when it directly follows a callee; as a grouping or
651
+ // arrow body it must hold an expression, so fill `null` rather than closing into an illegal empty `()`.
652
+ const isCallArgumentParen = (value: string) => /(?:[\w$)\]]|\?\.)$/.test(value.slice(0, -1).trimEnd());
653
+ const completeStatementPrefixFrame = (output: string, frame: CompletionFrame): { text: string; dropped: boolean } => {
654
+ if (frame.type !== "block") return { text: `${output}${closeFrame(frame)}`, dropped: false };
655
+ if (frame.closer === ")") {
656
+ const completed = completeIncompleteExpressionPrefix(output);
657
+ return { text: `${completed.endsWith("(") && !isCallArgumentParen(completed) ? `${completed}null` : completed}${frame.closer}`, dropped: false };
658
+ }
659
+ return completeBlockFrame(output, frame);
660
+ };
661
+ // Close frames from the stack top down. A frame whose own opening bracket was rolled back by a declaration drop must not emit a
662
+ // closer — `completeBlockFrame` detects that and reports `open: number` for the bracket position it would have closed, so once
663
+ // a drop happens we know which later frames lost their opener (those whose recorded bracket offset is past the drop point).
664
+ const closeStackFrames = (output: string, frames: CompletionFrame[]) => {
665
+ let text = output;
666
+ let dropPoint = Infinity;
667
+ for (const frame of frames) {
668
+ if (frame.type === "block" && frame.open !== undefined && frame.open >= dropPoint) continue;
669
+ const result = completeStatementPrefixFrame(text, frame);
670
+ if (result.dropped) dropPoint = Math.min(dropPoint, result.text.length);
671
+ text = result.text;
672
+ }
673
+ return text;
674
+ };
675
+ const REGEX_PREFIX_WORDS = new Set(["return", "throw", "case", "delete", "typeof", "void", "new", "in", "of", "await", "yield"]);
676
+ const JSX_CLOSING_TAG_START = /^<\/\s*(?:[A-Za-z][\w$.-]*\s*)?>/;
677
+
678
+ function isLikelyRegexLiteralStart(code: string, index: number) {
679
+ if (code[index] !== "/" || code[index + 1] === "/" || code[index + 1] === "*") return false;
680
+ if (code[index - 1] === "<" && JSX_CLOSING_TAG_START.test(code.slice(index - 1))) return false;
681
+ let previous = index - 1;
682
+ while (previous >= 0 && /\s/.test(code[previous])) previous -= 1;
683
+ if (previous < 0) return true;
684
+ const previousChar = code[previous];
685
+ if ("([{=,:;!&|?+-*~^<>".includes(previousChar)) return true;
686
+ if (!/[A-Za-z_$]/.test(previousChar)) return false;
687
+ let word = "";
688
+ for (let cursor = previous; cursor >= 0 && /[A-Za-z0-9_$]/.test(code[cursor]); cursor -= 1) word = code[cursor] + word;
689
+ return REGEX_PREFIX_WORDS.has(word);
690
+ }
691
+
692
+ function findRegexLiteralEnd(code: string, index: number) {
693
+ index += 1;
694
+ let inClass = false;
695
+ while (index < code.length) {
696
+ const next = code[index];
697
+ if (next === "\\") {
698
+ index += 2;
699
+ continue;
700
+ }
701
+ index += 1;
702
+ if (next === "[" && !inClass) inClass = true;
703
+ else if (next === "]" && inClass) inClass = false;
704
+ else if (next === "/" && !inClass) {
705
+ while (index < code.length && /[A-Za-z]/.test(code[index])) index += 1;
706
+ return index;
707
+ }
708
+ }
709
+ return code.length;
710
+ }
711
+
712
+ const consumeRegexLiteral = (code: string, index: number, push: PushOutput) => {
713
+ push("/");
714
+ index += 1;
715
+ let inClass = false;
716
+ let closed = false;
717
+ while (index < code.length) {
718
+ const next = code[index];
719
+ if (next === "\\") {
720
+ push(code.slice(index, index + 2));
721
+ index += 2;
722
+ continue;
723
+ }
724
+ push(next);
725
+ index += 1;
726
+ if (next === "[" && !inClass) inClass = true;
727
+ else if (next === "]" && inClass) inClass = false;
728
+ else if (next === "/" && !inClass) {
729
+ closed = true;
730
+ break;
731
+ }
732
+ }
733
+ while (closed && index < code.length && /[A-Za-z]/.test(code[index])) {
734
+ push(code[index]);
735
+ index += 1;
736
+ }
737
+ if (!closed) push("/");
738
+ return index;
739
+ };
740
+
741
+ const isJsxTagHead = (code: string, index: number) => {
742
+ const tail = code.slice(index);
743
+ if (tail.startsWith("<>") || tail.startsWith("</>") || tail === "</") return true;
744
+ if (tail.startsWith("</")) return /^<\/\s*[A-Za-z][\w$.-]*(?:\s*>|>|$)/.test(tail);
745
+ const match = /^<[A-Za-z][\w$.-]*/.exec(tail);
746
+ return match !== null && /^(?:$|[\s/>])/.test(tail.slice(match[0].length));
747
+ };
748
+
749
+ // A comma right after the tag name (`<T,`) is a TSX generic type-param list, not JSX — a JSX tag name is only ever
750
+ // followed by whitespace, `/`, `>`, or end, never a bare comma. Without this, the generic arrow `<T,>(x:T)=>x` is
751
+ // misread as an open `<T>` element and a synthetic `</T>` is appended, breaking the module.
752
+ const isGenericTypeParamList = (code: string, index: number) => /^<\s*[A-Za-z_$][\w$]*\s*,/.test(code.slice(index));
753
+ const shouldStartJsxTag = (code: string, index: number, stack: CompletionFrame[]) => {
754
+ if (isGenericTypeParamList(code, index)) return false;
755
+ if (stack.at(-1)?.type === "jsx") return isJsxTagHead(code, index);
756
+ return isLikelyJsxTagStart(code, index);
757
+ };
758
+ const isInsideJsxText = (stack: CompletionFrame[]) => stack.at(-1)?.type === "jsx";
759
+
760
+ const consumeTemplateExpression = (code: string, index: number, push: PushOutput): number => {
761
+ let depth = 1;
762
+ while (index < code.length && depth > 0) {
763
+ const expressionChar = code[index];
764
+ if (expressionChar === "/" && code[index + 1] === "/") {
765
+ const newlineIndex = code.indexOf("\n", index + 2);
766
+ const end = newlineIndex === -1 ? code.length : newlineIndex;
767
+ push(code.slice(index, end));
768
+ index = end;
769
+ continue;
770
+ }
771
+ if (expressionChar === "/" && code[index + 1] === "*") {
772
+ const closeIndex = code.indexOf("*/", index + 2);
773
+ if (closeIndex === -1) {
774
+ push(`${code.slice(index)}*/`);
775
+ return code.length;
776
+ }
777
+ push(code.slice(index, closeIndex + 2));
778
+ index = closeIndex + 2;
779
+ continue;
780
+ }
781
+ if (isLikelyRegexLiteralStart(code, index)) {
782
+ index = consumeRegexLiteral(code, index, push);
783
+ continue;
784
+ }
785
+ if (expressionChar === '"' || expressionChar === "'") {
786
+ index = consumeStringLiteral(code, index, push);
787
+ continue;
788
+ }
789
+ if (expressionChar === "`") {
790
+ index = consumeTemplateLiteral(code, index, push);
791
+ continue;
792
+ }
793
+ push(expressionChar);
794
+ index += 1;
795
+ if (expressionChar === "{") depth += 1;
796
+ else if (expressionChar === "}") depth -= 1;
797
+ }
798
+ if (depth > 0) push("}".repeat(depth));
799
+ return index;
800
+ };
801
+
802
+ const consumeTemplateLiteral = (code: string, index: number, push: PushOutput): number => {
803
+ push("`");
804
+ index += 1;
805
+ let closed = false;
806
+ while (index < code.length) {
807
+ const next = code[index];
808
+ if (next === "\\") {
809
+ push(code.slice(index, index + 2));
810
+ index += 2;
811
+ continue;
812
+ }
813
+ if (next === "$" && code[index + 1] === "{") {
814
+ push("${");
815
+ index = consumeTemplateExpression(code, index + 2, push);
816
+ continue;
817
+ }
818
+ push(next);
819
+ index += 1;
820
+ if (next === "`") {
821
+ closed = true;
822
+ break;
823
+ }
824
+ }
825
+ if (!closed) push("`");
826
+ return index;
827
+ };
828
+
829
+ export function completePartialTsx(code: string) {
830
+ let output = "";
831
+ const stack: CompletionFrame[] = [];
832
+ let index = 0;
833
+ let validEnd = code.length;
834
+ const push = (value: string) => {
835
+ output += value;
836
+ };
837
+ while (index < code.length) {
838
+ const char = code[index];
839
+ if (!isInsideJsxText(stack) && char === "/" && code[index + 1] === "/") {
840
+ const newlineIndex = code.indexOf("\n", index + 2);
841
+ const end = newlineIndex === -1 ? code.length : newlineIndex;
842
+ push(code.slice(index, end));
843
+ index = end;
844
+ continue;
845
+ }
846
+ if (!isInsideJsxText(stack) && char === "/" && code[index + 1] === "*") {
847
+ const closeIndex = code.indexOf("*/", index + 2);
848
+ if (closeIndex === -1) {
849
+ push(`${code.slice(index)}*/`);
850
+ index = code.length;
851
+ continue;
852
+ }
853
+ push(code.slice(index, closeIndex + 2));
854
+ index = closeIndex + 2;
855
+ continue;
856
+ }
857
+ if (!isInsideJsxText(stack) && isLikelyRegexLiteralStart(code, index)) {
858
+ index = consumeRegexLiteral(code, index, push);
859
+ continue;
860
+ }
861
+ if (!isInsideJsxText(stack) && (char === '"' || char === "'")) {
862
+ index = consumeStringLiteral(code, index, push);
863
+ continue;
864
+ }
865
+ if (!isInsideJsxText(stack) && char === "`") {
866
+ index = consumeTemplateLiteral(code, index, push);
867
+ continue;
868
+ }
869
+ if (char === "<" && isInsideJsxText(stack) && !shouldStartJsxTag(code, index, stack) && index < code.length - 1) {
870
+ push("&lt;");
871
+ index += 1;
872
+ continue;
873
+ }
874
+ if (char === "<" && !shouldStartJsxTag(code, index, stack) && index === code.length - 1) {
875
+ const frame = stack.at(-1);
876
+ if (frame?.type === "block" && frame.closer === "}" && !frame.statement) output = output.trimEnd();
877
+ break;
878
+ }
879
+ if (char === "<" && shouldStartJsxTag(code, index, stack)) {
880
+ const closeIndex = findJsxTagEnd(code, index);
881
+ if (closeIndex === -1) {
882
+ const completedTag = completeIncompleteJsxTag(code, index);
883
+ if (completedTag === null) {
884
+ validEnd = index;
885
+ output = output.slice(0, validEnd);
886
+ break;
887
+ }
888
+ push(completedTag.tag);
889
+ index = code.length;
890
+ if (!completedTag.tag.trimEnd().endsWith("/>") && !VOID_TAGS.has(completedTag.tagName)) stack.push({ type: "jsx", tag: completedTag.tagName });
891
+ break;
892
+ }
893
+ const tag = code.slice(index, closeIndex + 1);
894
+ const match = /^<\/?\s*([A-Za-z][\w$.-]*)/.exec(tag);
895
+ const tagName = match?.[1] ?? (tag === "<>" || tag === "</>" ? "" : null);
896
+ if (!(tagName && VOID_TAGS.has(tagName) && tag.startsWith("</"))) push(tagName && VOID_TAGS.has(tagName) && !tag.endsWith("/>") ? tag.replace(/\s*\/?\s*>$/, " />") : tag);
897
+ index = closeIndex + 1;
898
+ updateJsxStackForTag(stack, tag);
899
+ continue;
900
+ }
901
+ if (char in BLOCK_FOLLOWERS) {
902
+ stack.push({ type: "block", closer: BLOCK_FOLLOWERS[char], statement: isLikelyStatementBlock(code, index), open: output.length });
903
+ push(char);
904
+ index += 1;
905
+ continue;
906
+ }
907
+ if (BLOCK_CLOSERS.has(char)) {
908
+ const frame = stack.at(-1);
909
+ if (frame?.type === "block" && frame.closer === char) stack.pop();
910
+ push(char);
911
+ index += 1;
912
+ continue;
913
+ }
914
+ push(char);
915
+ index += 1;
916
+ }
917
+ const closingFrames = [...stack].toReversed();
918
+ const closers = closingFrames.map(closeFrame).join("");
919
+ if (!closers) return completeIncompleteExpressionPrefix(output, true);
920
+ const statementIndex = closingFrames.findIndex((frame) => frame.type === "block" && frame.statement);
921
+ if (statementIndex === -1) return closeStackFrames(output, closingFrames);
922
+ const beforeStatement = closeStackFrames(output, closingFrames.slice(0, statementIndex));
923
+ const completedExpression = fillEmptyReturnExpression(completeIncompleteExpressionPrefix(beforeStatement, true));
924
+ return `${completedExpression}${isExpressionEnd(completedExpression) ? ";" : ""}${closingFrames.slice(statementIndex).map(closeFrame).join("")}`;
925
+ }
926
+
927
+ export function normalizeGeneratedTsx(code: string) {
928
+ let next = completePartialTsx(stripMarkdownFence(code)).trim();
929
+ const exportScanCode = maskIgnoredTextForExportScan(next);
930
+ if (!hasDefaultExportDeclaration(exportScanCode)) {
931
+ const componentName = findLastExportableComponent(exportScanCode);
932
+ if (componentName) next += `\nexport default ${componentName};`;
933
+ }
934
+ next = injectMissingReactHookImports(next, exportScanCode);
935
+ return next;
936
+ }
package/index.js DELETED
@@ -1 +0,0 @@
1
- // Placeholder