@tsrx/core 0.1.41 → 0.1.43
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/package.json +4 -4
- package/src/comment-utils.js +16 -1
- package/src/parse/index.js +30 -0
- package/src/transform/jsx/helpers.js +60 -52
- package/src/transform/jsx/index.js +293 -135
- package/src/transform/segments.js +63 -43
- package/src/utils/builders.js +16 -0
- package/types/index.d.ts +12 -0
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Core compiler infrastructure for TSRX syntax",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Dominic Gannaway",
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.43",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
@@ -63,11 +63,11 @@
|
|
|
63
63
|
"dependencies": {
|
|
64
64
|
"@jridgewell/sourcemap-codec": "^1.5.5",
|
|
65
65
|
"@noble/hashes": "^2.2.0",
|
|
66
|
-
"@sveltejs/acorn-typescript": "^1.0.
|
|
66
|
+
"@sveltejs/acorn-typescript": "^1.0.11",
|
|
67
67
|
"@types/estree-jsx": "^1.0.5",
|
|
68
68
|
"@types/estree": "^1.0.8",
|
|
69
|
-
"acorn": "^8.
|
|
70
|
-
"esrap": "^2.
|
|
69
|
+
"acorn": "^8.17.0",
|
|
70
|
+
"esrap": "^2.3.0",
|
|
71
71
|
"is-reference": "^3.0.3",
|
|
72
72
|
"magic-string": "^0.30.18",
|
|
73
73
|
"zimmerframe": "^1.1.2"
|
package/src/comment-utils.js
CHANGED
|
@@ -58,6 +58,18 @@ export function is_jsdoc_ts_annotation(comment) {
|
|
|
58
58
|
return tsAnnotations.some((annotation) => comment.value.includes(annotation));
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Check if a comment is a TypeScript JSX pragma (`@jsxImportSource`,
|
|
63
|
+
* `@jsxRuntime`, `@jsxFrag`, `@jsx`). TS reads these from a file's LEADING
|
|
64
|
+
* comments to pick that file's JSX type source/factory, overriding tsconfig —
|
|
65
|
+
* dropping one silently retypes every JSX expression in the file.
|
|
66
|
+
* @param {AST.CommentWithLocation} comment
|
|
67
|
+
* @returns {boolean}
|
|
68
|
+
*/
|
|
69
|
+
export function is_jsx_pragma(comment) {
|
|
70
|
+
return /@jsx(ImportSource|Runtime|Frag)?\b/.test(comment.value);
|
|
71
|
+
}
|
|
72
|
+
|
|
61
73
|
/**
|
|
62
74
|
* Check if a comment should be preserved in to_ts mode
|
|
63
75
|
* @param {AST.CommentWithLocation} comment
|
|
@@ -65,7 +77,10 @@ export function is_jsdoc_ts_annotation(comment) {
|
|
|
65
77
|
*/
|
|
66
78
|
export function should_preserve_comment(comment) {
|
|
67
79
|
return (
|
|
68
|
-
is_ts_pragma(comment) ||
|
|
80
|
+
is_ts_pragma(comment) ||
|
|
81
|
+
is_triple_slash_directive(comment) ||
|
|
82
|
+
is_jsdoc_ts_annotation(comment) ||
|
|
83
|
+
is_jsx_pragma(comment)
|
|
69
84
|
);
|
|
70
85
|
}
|
|
71
86
|
|
package/src/parse/index.js
CHANGED
|
@@ -219,12 +219,38 @@ export function createParser(...plugins) {
|
|
|
219
219
|
/** @type {AST.Program} */
|
|
220
220
|
let ast;
|
|
221
221
|
|
|
222
|
+
// Lexer-authoritative keyword positions (volar opt-in): the mapping
|
|
223
|
+
// collector needs the SOURCE spans of `async`/`function`, which no AST
|
|
224
|
+
// node records. The tokenizer is the only correct source — offset
|
|
225
|
+
// arithmetic breaks on extra whitespace, and text search breaks on
|
|
226
|
+
// comments (`async /* function */ function`).
|
|
227
|
+
/** @type {Array<{ value: string, start: number, end: number, loc: AST.SourceLocation }> | undefined} */
|
|
228
|
+
const keyword_tokens = options?.keywordTokens ? [] : undefined;
|
|
229
|
+
/** @type {Parse.Options['onToken'] | undefined} */
|
|
230
|
+
const onToken = keyword_tokens
|
|
231
|
+
? (token) => {
|
|
232
|
+
const t = /** @type {any} */ (token);
|
|
233
|
+
const is_function_keyword = t.type?.keyword === 'function';
|
|
234
|
+
const is_async_name = t.type?.label === 'name' && t.value === 'async';
|
|
235
|
+
if (is_function_keyword || is_async_name) {
|
|
236
|
+
keyword_tokens.push({
|
|
237
|
+
value: is_function_keyword ? 'function' : 'async',
|
|
238
|
+
start: t.start,
|
|
239
|
+
end: t.end,
|
|
240
|
+
loc: t.loc,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
: undefined;
|
|
245
|
+
|
|
222
246
|
try {
|
|
223
247
|
ast = parser.parse(source, {
|
|
224
248
|
sourceType: 'module',
|
|
225
249
|
ecmaVersion: 13,
|
|
226
250
|
allowReturnOutsideFunction: true,
|
|
227
251
|
locations: true,
|
|
252
|
+
onToken,
|
|
253
|
+
preserveParens: !!options?.preserveParens,
|
|
228
254
|
onComment,
|
|
229
255
|
tsrxOptions: {
|
|
230
256
|
filename,
|
|
@@ -245,6 +271,10 @@ export function createParser(...plugins) {
|
|
|
245
271
|
|
|
246
272
|
add_comments(ast);
|
|
247
273
|
|
|
274
|
+
if (keyword_tokens) {
|
|
275
|
+
/** @type {any} */ (ast).tsrx_keyword_tokens = keyword_tokens;
|
|
276
|
+
}
|
|
277
|
+
|
|
248
278
|
return ast;
|
|
249
279
|
};
|
|
250
280
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
/** @import { Visitors } from 'zimmerframe' */
|
|
3
3
|
|
|
4
4
|
import tsx from 'esrap/languages/tsx';
|
|
5
|
+
import { should_preserve_comment, format_comment } from '../../comment-utils.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Zimmerframe provides `path` as the ancestor chain. A native template node in
|
|
@@ -51,18 +52,48 @@ export function set_node_path_metadata(node, path) {
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
/**
|
|
54
|
-
* Wrap esrap's `tsx()` printer with location markers for
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
55
|
+
* Wrap esrap's `tsx()` printer with location markers for the remaining nodes
|
|
56
|
+
* whose spans are invisible to the source map (e.g. `class`, template-literal
|
|
57
|
+
* backticks, JSX angle brackets, generic-argument delimiters). Without these
|
|
58
|
+
* markers, Volar mapping collection in `segments.js` throws when looking up
|
|
59
|
+
* the node's start/end positions. esrap ≥2.3.0 (keyword writes +
|
|
60
|
+
* `boundaryTokens`) covers most starts, but not statement ends — see the
|
|
61
|
+
* list below for what each entry still compensates.
|
|
59
62
|
*
|
|
60
63
|
* Shared across all JSX-producing targets (React, Preact, Solid).
|
|
61
64
|
*
|
|
62
65
|
* @returns {any}
|
|
63
66
|
*/
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
/**
|
|
68
|
+
* @param {boolean} [boundary_tokens] Enable esrap's `boundaryTokens` anchors
|
|
69
|
+
* (structural tokens carry one-character source locations). typeOnly/volar
|
|
70
|
+
* prints opt in — their maps are consumed positionally by the language
|
|
71
|
+
* tooling and never shipped; build prints stay sparse.
|
|
72
|
+
* @param {AST.CommentWithLocation[]} [comments] Source comments; the ones
|
|
73
|
+
* `should_preserve_comment` classifies as semantic-to-TS (`@ts-nocheck`,
|
|
74
|
+
* `@jsxImportSource`, triple-slash references, …) and that LEAD the program
|
|
75
|
+
* are re-emitted at the top of the printed output. The generated TSX is real
|
|
76
|
+
* TS input — dropping a leading pragma changes how the whole file checks.
|
|
77
|
+
*/
|
|
78
|
+
export function tsx_with_ts_locations(boundary_tokens = false, comments = undefined) {
|
|
79
|
+
const base = /** @type {any} */ (tsx({ boundaryTokens: boundary_tokens }));
|
|
80
|
+
|
|
81
|
+
const leading_preserved = (/** @type {any} */ program) => {
|
|
82
|
+
if (!comments?.length) return [];
|
|
83
|
+
// Injected statements (dynamic-import/try-import prepends) carry no
|
|
84
|
+
// loc; anchor "leading" on the first statement that maps to source,
|
|
85
|
+
// else every preserved comment in the file would hoist to the top.
|
|
86
|
+
const first = program.body?.find((/** @type {any} */ node) => node.loc);
|
|
87
|
+
return comments.filter(
|
|
88
|
+
(/** @type {any} */ comment) =>
|
|
89
|
+
should_preserve_comment(comment) &&
|
|
90
|
+
(first?.loc == null ||
|
|
91
|
+
(comment.loc &&
|
|
92
|
+
(comment.loc.end.line < first.loc.start.line ||
|
|
93
|
+
(comment.loc.end.line === first.loc.start.line &&
|
|
94
|
+
comment.loc.end.column <= first.loc.start.column)))),
|
|
95
|
+
);
|
|
96
|
+
};
|
|
66
97
|
|
|
67
98
|
/**
|
|
68
99
|
* @param {any} node
|
|
@@ -81,17 +112,17 @@ export function tsx_with_ts_locations() {
|
|
|
81
112
|
|
|
82
113
|
/** @type {Record<string, (node: any, context: any) => void>} */
|
|
83
114
|
const wrappers = {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
context.
|
|
115
|
+
Program: (node, context) => {
|
|
116
|
+
for (const comment of leading_preserved(node)) {
|
|
117
|
+
if (comment.loc) context.location(comment.loc.start.line, comment.loc.start.column);
|
|
118
|
+
context.write(format_comment(comment));
|
|
119
|
+
if (comment.loc) context.location(comment.loc.end.line, comment.loc.end.column);
|
|
120
|
+
context.newline();
|
|
88
121
|
}
|
|
122
|
+
base.Program(node, context);
|
|
89
123
|
},
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (node.optional) {
|
|
93
|
-
context.write('?');
|
|
94
|
-
}
|
|
124
|
+
ArrayPattern: (node, context) => {
|
|
125
|
+
base.ArrayPattern(node, context);
|
|
95
126
|
if (node.typeAnnotation) {
|
|
96
127
|
context.visit(node.typeAnnotation);
|
|
97
128
|
}
|
|
@@ -134,39 +165,6 @@ export function tsx_with_ts_locations() {
|
|
|
134
165
|
context.write(' ');
|
|
135
166
|
context.visit(value.body);
|
|
136
167
|
},
|
|
137
|
-
// esrap's ArrowFunctionExpression printer ignores `typeParameters` and
|
|
138
|
-
// `returnType`, so an annotated arrow like `(): Record<...> => ...`
|
|
139
|
-
// prints as `() => ...` and segments.js can't resolve the return-type
|
|
140
|
-
// nodes' positions in the generated output.
|
|
141
|
-
ArrowFunctionExpression: (node, context) => {
|
|
142
|
-
if (node.async) context.write('async ');
|
|
143
|
-
if (node.typeParameters) {
|
|
144
|
-
context.visit(node.typeParameters);
|
|
145
|
-
}
|
|
146
|
-
context.write('(');
|
|
147
|
-
for (let i = 0; i < node.params.length; i++) {
|
|
148
|
-
if (i > 0) context.write(', ');
|
|
149
|
-
context.visit(node.params[i]);
|
|
150
|
-
}
|
|
151
|
-
context.write(')');
|
|
152
|
-
if (node.returnType) {
|
|
153
|
-
context.visit(node.returnType);
|
|
154
|
-
}
|
|
155
|
-
context.write(' => ');
|
|
156
|
-
const body = node.body;
|
|
157
|
-
const wrap_body =
|
|
158
|
-
body.type === 'ObjectExpression' ||
|
|
159
|
-
(body.type === 'AssignmentExpression' && body.left.type === 'ObjectPattern') ||
|
|
160
|
-
(body.type === 'LogicalExpression' && body.left.type === 'ObjectExpression') ||
|
|
161
|
-
(body.type === 'ConditionalExpression' && body.test.type === 'ObjectExpression');
|
|
162
|
-
if (wrap_body) {
|
|
163
|
-
context.write('(');
|
|
164
|
-
context.visit(body);
|
|
165
|
-
context.write(')');
|
|
166
|
-
} else {
|
|
167
|
-
context.visit(body);
|
|
168
|
-
}
|
|
169
|
-
},
|
|
170
168
|
|
|
171
169
|
// esrap's JSXOpeningElement printer doesn't emit `typeArguments`, so generic
|
|
172
170
|
// component tags like `<RenderProp<User>>` lose the `<User>` in the output.
|
|
@@ -196,6 +194,11 @@ export function tsx_with_ts_locations() {
|
|
|
196
194
|
}
|
|
197
195
|
},
|
|
198
196
|
TSModuleDeclaration: (node, context) => {
|
|
197
|
+
// Ambient `declare module '…' { … }` must keep its `declare` — the
|
|
198
|
+
// typeOnly/volar output is real TS and `module '…' { … }` alone is a
|
|
199
|
+
// syntax error (TS1035). Non-ambient `module name { }` blocks have no
|
|
200
|
+
// `declare` and print unchanged.
|
|
201
|
+
if (node.declare) context.write('declare ');
|
|
199
202
|
context.write(node.metadata?.module_keyword ?? 'module');
|
|
200
203
|
context.write(' ');
|
|
201
204
|
context.visit(node.id);
|
|
@@ -209,9 +212,14 @@ export function tsx_with_ts_locations() {
|
|
|
209
212
|
// on the whole node, only then duplicate it here
|
|
210
213
|
// e.g. JSXOpeningElement is such a case
|
|
211
214
|
for (const type of [
|
|
212
|
-
// JS nodes
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
+
// JS nodes with boundary positions esrap still cannot map. Keyword
|
|
216
|
+
// writes (if/new/return/for/switch/await) and `boundaryTokens`
|
|
217
|
+
// anchors (brackets, braces, parens, computed/call closers) cover
|
|
218
|
+
// many STARTS, but statement ENDS land on unanchored characters
|
|
219
|
+
// (`;`, a block's `}`), `class` is not a keyword-write, template
|
|
220
|
+
// literals' backticks carry no location, and an arrow's span can
|
|
221
|
+
// start at a bare `(` — so these node-level markers remain the
|
|
222
|
+
// source of both boundaries until esrap can anchor them.
|
|
215
223
|
'ClassDeclaration',
|
|
216
224
|
'ClassExpression',
|
|
217
225
|
'IfStatement',
|
|
@@ -214,32 +214,53 @@ function lower_code_block_child(block) {
|
|
|
214
214
|
* `lower_code_block_child`). This is the element-scoped equivalent of
|
|
215
215
|
* `transform_function`'s body lowering — function and arrow bodies are never
|
|
216
216
|
* element children, so they are untouched here.
|
|
217
|
+
*
|
|
218
|
+
* The input tree is never mutated: replacements land on a shallow copy of the
|
|
219
|
+
* owning node (or array), so the return value must be used in place of the
|
|
220
|
+
* argument. Untouched subtrees are shared by reference with the input.
|
|
217
221
|
* @param {any} node
|
|
218
222
|
* @param {Set<any>} [seen]
|
|
219
|
-
* @returns {
|
|
223
|
+
* @returns {any}
|
|
220
224
|
*/
|
|
221
225
|
function expand_child_code_blocks(node, seen = new Set()) {
|
|
222
|
-
if (!node || typeof node !== 'object' || seen.has(node)) return;
|
|
226
|
+
if (!node || typeof node !== 'object' || seen.has(node)) return node;
|
|
223
227
|
seen.add(node);
|
|
224
228
|
|
|
225
229
|
if (Array.isArray(node)) {
|
|
226
|
-
|
|
227
|
-
|
|
230
|
+
let changed = false;
|
|
231
|
+
const result = node.map((item) => {
|
|
232
|
+
const walked = expand_child_code_blocks(item, seen);
|
|
233
|
+
if (walked !== item) changed = true;
|
|
234
|
+
return walked;
|
|
235
|
+
});
|
|
236
|
+
return changed ? result : node;
|
|
228
237
|
}
|
|
229
238
|
|
|
239
|
+
let out = node;
|
|
240
|
+
const set = (/** @type {string} */ key, /** @type {any} */ value) => {
|
|
241
|
+
if (out[key] === value) return;
|
|
242
|
+
if (out === node) out = { ...node };
|
|
243
|
+
out[key] = value;
|
|
244
|
+
};
|
|
245
|
+
|
|
230
246
|
if (
|
|
231
247
|
Array.isArray(node.children) &&
|
|
232
248
|
node.children.some((/** @type {any} */ c) => c?.type === 'JSXCodeBlock')
|
|
233
249
|
) {
|
|
234
|
-
|
|
235
|
-
|
|
250
|
+
set(
|
|
251
|
+
'children',
|
|
252
|
+
node.children.flatMap((/** @type {any} */ child) =>
|
|
253
|
+
child?.type === 'JSXCodeBlock' ? lower_code_block_child(child) : [child],
|
|
254
|
+
),
|
|
236
255
|
);
|
|
237
256
|
}
|
|
238
257
|
|
|
239
|
-
for (const key of Object.keys(
|
|
258
|
+
for (const key of Object.keys(out)) {
|
|
240
259
|
if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') continue;
|
|
241
|
-
expand_child_code_blocks(
|
|
260
|
+
set(key, expand_child_code_blocks(out[key], seen));
|
|
242
261
|
}
|
|
262
|
+
|
|
263
|
+
return out;
|
|
243
264
|
}
|
|
244
265
|
|
|
245
266
|
/**
|
|
@@ -418,13 +439,17 @@ function wrap_lowered_value_in_fragment(expression, source) {
|
|
|
418
439
|
* its rendered value. Either way nothing leaks to the printer as a raw
|
|
419
440
|
* `JSX…Expression`.
|
|
420
441
|
*
|
|
442
|
+
* The input tree is never mutated: every replacement lands on a shallow copy of
|
|
443
|
+
* the owning node (or array), so the return value must be used in place of the
|
|
444
|
+
* argument. Untouched subtrees are shared by reference with the input.
|
|
445
|
+
*
|
|
421
446
|
* @param {any} node
|
|
422
447
|
* @param {TransformContext} transform_context
|
|
423
448
|
* @param {Set<any>} [seen]
|
|
424
|
-
* @returns {
|
|
449
|
+
* @returns {any}
|
|
425
450
|
*/
|
|
426
451
|
function wrap_control_flow_expression_values(node, transform_context, seen = new Set()) {
|
|
427
|
-
if (!node || typeof node !== 'object' || seen.has(node)) return;
|
|
452
|
+
if (!node || typeof node !== 'object' || seen.has(node)) return node;
|
|
428
453
|
seen.add(node);
|
|
429
454
|
|
|
430
455
|
// Dynamic tags on factory platforms must lower before control-flow
|
|
@@ -454,11 +479,17 @@ function wrap_control_flow_expression_values(node, transform_context, seen = new
|
|
|
454
479
|
: value;
|
|
455
480
|
|
|
456
481
|
if (Array.isArray(node)) {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
wrap_control_flow_expression_values(
|
|
460
|
-
|
|
461
|
-
|
|
482
|
+
let changed = false;
|
|
483
|
+
const result = node.map((entry) => {
|
|
484
|
+
const walked = wrap_control_flow_expression_values(
|
|
485
|
+
lower_child(entry),
|
|
486
|
+
transform_context,
|
|
487
|
+
seen,
|
|
488
|
+
);
|
|
489
|
+
if (walked !== entry) changed = true;
|
|
490
|
+
return walked;
|
|
491
|
+
});
|
|
492
|
+
return changed ? result : node;
|
|
462
493
|
}
|
|
463
494
|
|
|
464
495
|
// Wrap a bare control-flow directive that is the sole value of a render-output
|
|
@@ -469,50 +500,72 @@ function wrap_control_flow_expression_values(node, transform_context, seen = new
|
|
|
469
500
|
const wrap_value = (/** @type {any} */ value) =>
|
|
470
501
|
is_jsx_control_flow_expression(value) ? wrap_in_native_tsrx_fragment(value) : value;
|
|
471
502
|
|
|
503
|
+
// All replacements land on `out`, a shallow copy made on first write; the
|
|
504
|
+
// input node's fields are never reassigned.
|
|
505
|
+
let out = node;
|
|
506
|
+
const set = (/** @type {string} */ key, /** @type {any} */ value) => {
|
|
507
|
+
if (out[key] === value) return;
|
|
508
|
+
if (out === node) out = { ...node };
|
|
509
|
+
out[key] = value;
|
|
510
|
+
};
|
|
511
|
+
|
|
472
512
|
if (
|
|
473
513
|
node.type === 'ArrowFunctionExpression' &&
|
|
474
514
|
node.body?.type !== 'BlockStatement' &&
|
|
475
515
|
is_jsx_control_flow_expression(node.body)
|
|
476
516
|
) {
|
|
477
|
-
|
|
517
|
+
set('body', wrap_in_native_tsrx_fragment(node.body));
|
|
478
518
|
} else if (node.type === 'ReturnStatement' && is_jsx_control_flow_expression(node.argument)) {
|
|
479
|
-
|
|
519
|
+
set('argument', wrap_in_native_tsrx_fragment(node.argument));
|
|
480
520
|
} else if (
|
|
481
521
|
node.type === 'ExpressionStatement' &&
|
|
482
522
|
is_jsx_control_flow_expression(node.expression)
|
|
483
523
|
) {
|
|
484
|
-
|
|
524
|
+
set('expression', wrap_in_native_tsrx_fragment(node.expression));
|
|
485
525
|
} else if (node.type === 'VariableDeclarator' && is_jsx_control_flow_expression(node.init)) {
|
|
486
|
-
|
|
526
|
+
set('init', wrap_in_native_tsrx_fragment(node.init));
|
|
487
527
|
} else if (node.type === 'AssignmentExpression' && is_jsx_control_flow_expression(node.right)) {
|
|
488
|
-
|
|
528
|
+
set('right', wrap_in_native_tsrx_fragment(node.right));
|
|
489
529
|
} else if (
|
|
490
530
|
(node.type === 'CallExpression' || node.type === 'NewExpression') &&
|
|
491
531
|
Array.isArray(node.arguments)
|
|
492
532
|
) {
|
|
493
|
-
|
|
533
|
+
const wrapped = node.arguments.map(wrap_value);
|
|
534
|
+
if (
|
|
535
|
+
wrapped.some(
|
|
536
|
+
(/** @type {any} */ argument, /** @type {number} */ i) => argument !== node.arguments[i],
|
|
537
|
+
)
|
|
538
|
+
) {
|
|
539
|
+
set('arguments', wrapped);
|
|
540
|
+
}
|
|
494
541
|
}
|
|
495
542
|
|
|
496
|
-
for (const key of Object.keys(
|
|
543
|
+
for (const key of Object.keys(out)) {
|
|
497
544
|
if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') continue;
|
|
498
545
|
// A directive is allowed as a render child/statement, and as the sole value
|
|
499
546
|
// of a render-output slot (handled above for control flow; `@{ … }` blocks
|
|
500
547
|
// self-lower). Everywhere else it is combined into an expression — wrap it.
|
|
501
548
|
const allowed_slot =
|
|
502
549
|
is_statement_or_template_slot(node, key) || is_render_output_value_slot(node, key);
|
|
503
|
-
const value =
|
|
550
|
+
const value = out[key];
|
|
504
551
|
if (Array.isArray(value)) {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
552
|
+
let changed = false;
|
|
553
|
+
const result = value.map((entry) => {
|
|
554
|
+
let next = lower_child(entry);
|
|
555
|
+
if (!allowed_slot) next = wrap_directive_in_expression(next);
|
|
556
|
+
next = wrap_control_flow_expression_values(next, transform_context, seen);
|
|
557
|
+
if (next !== entry) changed = true;
|
|
558
|
+
return next;
|
|
559
|
+
});
|
|
560
|
+
if (changed) set(key, result);
|
|
510
561
|
} else {
|
|
511
|
-
|
|
512
|
-
if (!allowed_slot)
|
|
513
|
-
|
|
562
|
+
let next = lower_child(value);
|
|
563
|
+
if (!allowed_slot) next = wrap_directive_in_expression(next);
|
|
564
|
+
set(key, wrap_control_flow_expression_values(next, transform_context, seen));
|
|
514
565
|
}
|
|
515
566
|
}
|
|
567
|
+
|
|
568
|
+
return out;
|
|
516
569
|
}
|
|
517
570
|
|
|
518
571
|
/**
|
|
@@ -578,8 +631,8 @@ export function createJsxTransform(platform) {
|
|
|
578
631
|
...(platform.hooks?.initialState?.() ?? {}),
|
|
579
632
|
};
|
|
580
633
|
|
|
581
|
-
expand_child_code_blocks(/** @type {any} */ (ast));
|
|
582
|
-
wrap_control_flow_expression_values(/** @type {any} */ (ast), transform_context);
|
|
634
|
+
ast = expand_child_code_blocks(/** @type {any} */ (ast));
|
|
635
|
+
ast = wrap_control_flow_expression_values(/** @type {any} */ (ast), transform_context);
|
|
583
636
|
|
|
584
637
|
if (!transform_context.typeOnly) {
|
|
585
638
|
preallocate_lazy_ids(/** @type {any} */ (ast), transform_context);
|
|
@@ -735,7 +788,15 @@ export function createJsxTransform(platform) {
|
|
|
735
788
|
},
|
|
736
789
|
});
|
|
737
790
|
|
|
738
|
-
|
|
791
|
+
let transformed_program = /** @type {AST.Program} */ (transformed);
|
|
792
|
+
// The walk returns the input program unchanged when no visitor replaced
|
|
793
|
+
// anything beneath it. The post-passes below (style anchors, helper
|
|
794
|
+
// expansion, import injection) write into the program's `body`, so
|
|
795
|
+
// detach from the caller's AST first; a changed program is already a
|
|
796
|
+
// fresh walk-owned node with a fresh `body` array.
|
|
797
|
+
if (/** @type {any} */ (transformed_program) === /** @type {any} */ (ast)) {
|
|
798
|
+
transformed_program = { ...transformed_program, body: [...transformed_program.body] };
|
|
799
|
+
}
|
|
739
800
|
if (type_only_style_anchors.length > 0) {
|
|
740
801
|
transformed_program.body.unshift(...type_only_style_anchors);
|
|
741
802
|
}
|
|
@@ -751,7 +812,7 @@ export function createJsxTransform(platform) {
|
|
|
751
812
|
// lazy transform runs, so every `@{ … }` block / `@`-directive has already
|
|
752
813
|
// been lowered to its final closure / block shape. The lazy transform can
|
|
753
814
|
// then walk the complete function structure in one pass.
|
|
754
|
-
lower_remaining_jsx_code_blocks(expanded, transform_context);
|
|
815
|
+
const lowered_program = lower_remaining_jsx_code_blocks(expanded, transform_context);
|
|
755
816
|
|
|
756
817
|
// Apply lazy destructuring transforms to module-level code (top-level function
|
|
757
818
|
// declarations, arrow functions, etc.).
|
|
@@ -772,18 +833,28 @@ export function createJsxTransform(platform) {
|
|
|
772
833
|
// so lazy bindings declared inside a nested block or directive body are
|
|
773
834
|
// rewritten just like a flat function body.
|
|
774
835
|
if (!transform_context.typeOnly) {
|
|
775
|
-
preallocate_lazy_ids(/** @type {any} */ (
|
|
836
|
+
preallocate_lazy_ids(/** @type {any} */ (lowered_program), transform_context);
|
|
776
837
|
}
|
|
777
838
|
const final_program = /** @type {any} */ (
|
|
778
839
|
transform_context.typeOnly
|
|
779
|
-
?
|
|
780
|
-
: apply_lazy_transforms(/** @type {any} */ (
|
|
840
|
+
? lowered_program
|
|
841
|
+
: apply_lazy_transforms(/** @type {any} */ (lowered_program), new Map())
|
|
781
842
|
);
|
|
782
843
|
|
|
783
|
-
const result = print(
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
844
|
+
const result = print(
|
|
845
|
+
final_program,
|
|
846
|
+
// typeOnly output is real TS input: re-emit preserved leading comments
|
|
847
|
+
// (@jsxImportSource / @ts-nocheck / triple-slash references) so TS
|
|
848
|
+
// semantics survive the comment-stripping print.
|
|
849
|
+
tsx_with_ts_locations(
|
|
850
|
+
transform_context.typeOnly,
|
|
851
|
+
transform_context.typeOnly ? transform_context.comments : undefined,
|
|
852
|
+
),
|
|
853
|
+
{
|
|
854
|
+
sourceMapSource: filename,
|
|
855
|
+
sourceMapContent: source,
|
|
856
|
+
},
|
|
857
|
+
);
|
|
787
858
|
|
|
788
859
|
const { css, cssHash } = render_css_result(/** @type {any} */ (stylesheets));
|
|
789
860
|
|
|
@@ -1707,7 +1778,7 @@ function get_active_native_tsrx_function(path) {
|
|
|
1707
1778
|
|
|
1708
1779
|
/**
|
|
1709
1780
|
* @param {any} node
|
|
1710
|
-
* @param {{ next: () => any, state: TransformContext, path: AST.Node[] }} context
|
|
1781
|
+
* @param {{ next: () => any, visit: (node: any, state?: TransformContext) => any, state: TransformContext, path: AST.Node[] }} context
|
|
1711
1782
|
* @returns {any}
|
|
1712
1783
|
*/
|
|
1713
1784
|
function transform_function(node, context) {
|
|
@@ -1717,15 +1788,26 @@ function transform_function(node, context) {
|
|
|
1717
1788
|
// from here it flows through the existing native-component machinery exactly
|
|
1718
1789
|
// like the older fenced `{ return <> … </> }` shape.
|
|
1719
1790
|
const has_jsx_code_block_body = node.body?.type === 'JSXCodeBlock';
|
|
1720
|
-
lower_jsx_code_block_function_body(node);
|
|
1791
|
+
const lowered = lower_jsx_code_block_function_body(node);
|
|
1792
|
+
if (lowered !== node) {
|
|
1793
|
+
// The lowering produced a COPY; carry the native-body fact through the
|
|
1794
|
+
// sanctioned metadata channel and re-dispatch so the walker transforms
|
|
1795
|
+
// the lowered tree (terminates: the copy's body is a BlockStatement).
|
|
1796
|
+
lowered.metadata = { ...(lowered.metadata || {}), native_tsrx_body: true };
|
|
1797
|
+
return context.visit(lowered);
|
|
1798
|
+
}
|
|
1721
1799
|
|
|
1722
1800
|
if (
|
|
1723
1801
|
has_jsx_code_block_body ||
|
|
1724
1802
|
node.metadata?.native_tsrx_function ||
|
|
1803
|
+
node.metadata?.native_tsrx_body ||
|
|
1725
1804
|
function_has_native_tsrx_return(node)
|
|
1726
1805
|
) {
|
|
1727
1806
|
return transform_native_tsrx_function(node, context, {
|
|
1728
|
-
nativeBody:
|
|
1807
|
+
nativeBody:
|
|
1808
|
+
has_jsx_code_block_body ||
|
|
1809
|
+
!!node.metadata?.native_tsrx_function ||
|
|
1810
|
+
!!node.metadata?.native_tsrx_body,
|
|
1729
1811
|
});
|
|
1730
1812
|
}
|
|
1731
1813
|
|
|
@@ -1733,11 +1815,14 @@ function transform_function(node, context) {
|
|
|
1733
1815
|
}
|
|
1734
1816
|
|
|
1735
1817
|
/**
|
|
1818
|
+
* Lower a `@{ … }` body (JSXCodeBlock) to an ordinary block on a COPY built
|
|
1819
|
+
* with the AST builders — the source function node is never mutated. Returns
|
|
1820
|
+
* the input node unchanged when there is nothing to lower.
|
|
1736
1821
|
* @param {any} node
|
|
1737
|
-
* @returns {
|
|
1822
|
+
* @returns {any}
|
|
1738
1823
|
*/
|
|
1739
1824
|
function lower_jsx_code_block_function_body(node) {
|
|
1740
|
-
if (node.body?.type !== 'JSXCodeBlock') return;
|
|
1825
|
+
if (node.body?.type !== 'JSXCodeBlock') return node;
|
|
1741
1826
|
|
|
1742
1827
|
const code_block = node.body;
|
|
1743
1828
|
const statements = [...code_block.body];
|
|
@@ -1758,10 +1843,11 @@ function lower_jsx_code_block_function_body(node) {
|
|
|
1758
1843
|
}
|
|
1759
1844
|
statements.push(b.return(render, code_block.render));
|
|
1760
1845
|
}
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1846
|
+
return {
|
|
1847
|
+
...node,
|
|
1848
|
+
body: b.block(statements, code_block),
|
|
1849
|
+
...(node.type === 'ArrowFunctionExpression' ? { expression: false } : null),
|
|
1850
|
+
};
|
|
1765
1851
|
}
|
|
1766
1852
|
|
|
1767
1853
|
/**
|
|
@@ -3039,46 +3125,62 @@ function expand_component_helpers(program) {
|
|
|
3039
3125
|
* If one of those helpers contains a statement-container body, lower it before
|
|
3040
3126
|
* the printer sees the helper subtree.
|
|
3041
3127
|
*
|
|
3128
|
+
* The tree is never mutated: replacements land on a shallow copy of the owning
|
|
3129
|
+
* node (or array), so the return value must be used in place of the argument.
|
|
3130
|
+
* Untouched subtrees are shared by reference with the input.
|
|
3131
|
+
*
|
|
3042
3132
|
* @param {any} node
|
|
3043
3133
|
* @param {TransformContext} transform_context
|
|
3044
3134
|
* @param {Set<any>} [seen]
|
|
3045
|
-
* @returns {
|
|
3135
|
+
* @returns {any}
|
|
3046
3136
|
*/
|
|
3047
3137
|
function lower_remaining_jsx_code_blocks(node, transform_context, seen = new Set()) {
|
|
3048
|
-
if (!node || typeof node !== 'object' || seen.has(node)) return;
|
|
3138
|
+
if (!node || typeof node !== 'object' || seen.has(node)) return node;
|
|
3049
3139
|
seen.add(node);
|
|
3050
3140
|
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3141
|
+
// A code-block function body lowers to a fresh copy of the function node;
|
|
3142
|
+
// its children are then walked below like any other node's.
|
|
3143
|
+
let out = is_function_or_class_boundary(node) ? lower_jsx_code_block_function_body(node) : node;
|
|
3144
|
+
const set = (/** @type {string} */ key, /** @type {any} */ value) => {
|
|
3145
|
+
if (out[key] === value) return;
|
|
3146
|
+
if (out === node) out = { ...node };
|
|
3147
|
+
out[key] = value;
|
|
3148
|
+
};
|
|
3054
3149
|
|
|
3055
|
-
for (const key of Object.keys(
|
|
3150
|
+
for (const key of Object.keys(out)) {
|
|
3056
3151
|
if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') continue;
|
|
3057
|
-
|
|
3152
|
+
const value = out[key];
|
|
3058
3153
|
if (!value || typeof value !== 'object') continue;
|
|
3059
3154
|
|
|
3060
3155
|
if (Array.isArray(value)) {
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3156
|
+
const expanded =
|
|
3157
|
+
key === 'body' && value.some((child) => child?.type === 'JSXCodeBlock')
|
|
3158
|
+
? value.flatMap((child) => {
|
|
3159
|
+
if (child?.type !== 'JSXCodeBlock') return [child];
|
|
3160
|
+
const body_nodes = get_jsx_code_block_body_nodes(child, transform_context);
|
|
3161
|
+
return mark_native_pretransformed_jsx(
|
|
3162
|
+
build_render_statements(
|
|
3163
|
+
body_nodes,
|
|
3164
|
+
true,
|
|
3165
|
+
transform_context,
|
|
3166
|
+
is_authored_native_fragment(child.render) ? child.render : null,
|
|
3167
|
+
),
|
|
3168
|
+
);
|
|
3169
|
+
})
|
|
3170
|
+
: value;
|
|
3171
|
+
let changed = expanded !== value;
|
|
3172
|
+
const result = expanded.map((child) => {
|
|
3173
|
+
const walked = lower_remaining_jsx_code_blocks(child, transform_context, seen);
|
|
3174
|
+
if (walked !== child) changed = true;
|
|
3175
|
+
return walked;
|
|
3176
|
+
});
|
|
3177
|
+
if (changed) set(key, result);
|
|
3078
3178
|
} else {
|
|
3079
|
-
lower_remaining_jsx_code_blocks(value, transform_context, seen);
|
|
3179
|
+
set(key, lower_remaining_jsx_code_blocks(value, transform_context, seen));
|
|
3080
3180
|
}
|
|
3081
3181
|
}
|
|
3182
|
+
|
|
3183
|
+
return out;
|
|
3082
3184
|
}
|
|
3083
3185
|
|
|
3084
3186
|
/**
|
|
@@ -3202,9 +3304,8 @@ function get_loop_skip_if_consequent_body(node) {
|
|
|
3202
3304
|
*/
|
|
3203
3305
|
function create_component_loop_skip_if_statement(node, render_nodes, transform_context) {
|
|
3204
3306
|
const consequent_body = /** @type {any[]} */ (get_loop_skip_if_consequent_body(node));
|
|
3205
|
-
const branch_statements =
|
|
3206
|
-
|
|
3207
|
-
branch_statements,
|
|
3307
|
+
const branch_statements = prepend_render_nodes_to_return_statements(
|
|
3308
|
+
build_render_statements(consequent_body, true, transform_context),
|
|
3208
3309
|
render_nodes,
|
|
3209
3310
|
transform_context.typeOnly,
|
|
3210
3311
|
);
|
|
@@ -3221,19 +3322,23 @@ function create_component_loop_skip_if_statement(node, render_nodes, transform_c
|
|
|
3221
3322
|
}
|
|
3222
3323
|
|
|
3223
3324
|
/**
|
|
3325
|
+
* Statements can be passed through `build_render_statements` by reference, so
|
|
3326
|
+
* rewritten returns land on shallow copies; the returned array must be used in
|
|
3327
|
+
* place of the argument.
|
|
3328
|
+
*
|
|
3224
3329
|
* @param {any[]} statements
|
|
3225
3330
|
* @param {any[]} render_nodes
|
|
3226
3331
|
* @param {boolean} [type_only]
|
|
3227
|
-
* @returns {
|
|
3332
|
+
* @returns {any[]}
|
|
3228
3333
|
*/
|
|
3229
3334
|
function prepend_render_nodes_to_return_statements(statements, render_nodes, type_only = false) {
|
|
3230
3335
|
if (render_nodes.length === 0) {
|
|
3231
|
-
return;
|
|
3336
|
+
return statements;
|
|
3232
3337
|
}
|
|
3233
3338
|
|
|
3234
|
-
|
|
3235
|
-
prepend_render_nodes_to_return_statement(
|
|
3236
|
-
|
|
3339
|
+
return /** @type {any[]} */ (
|
|
3340
|
+
prepend_render_nodes_to_return_statement(statements, render_nodes, false, type_only)
|
|
3341
|
+
);
|
|
3237
3342
|
}
|
|
3238
3343
|
|
|
3239
3344
|
/**
|
|
@@ -3241,7 +3346,7 @@ function prepend_render_nodes_to_return_statements(statements, render_nodes, typ
|
|
|
3241
3346
|
* @param {any[]} render_nodes
|
|
3242
3347
|
* @param {boolean} inside_nested_function
|
|
3243
3348
|
* @param {boolean} [type_only]
|
|
3244
|
-
* @returns {
|
|
3349
|
+
* @returns {any}
|
|
3245
3350
|
*/
|
|
3246
3351
|
function prepend_render_nodes_to_return_statement(
|
|
3247
3352
|
node,
|
|
@@ -3250,7 +3355,7 @@ function prepend_render_nodes_to_return_statement(
|
|
|
3250
3355
|
type_only = false,
|
|
3251
3356
|
) {
|
|
3252
3357
|
if (!node || typeof node !== 'object') {
|
|
3253
|
-
return;
|
|
3358
|
+
return node;
|
|
3254
3359
|
}
|
|
3255
3360
|
|
|
3256
3361
|
if (
|
|
@@ -3262,33 +3367,44 @@ function prepend_render_nodes_to_return_statement(
|
|
|
3262
3367
|
}
|
|
3263
3368
|
|
|
3264
3369
|
if (!inside_nested_function && node.type === 'ReturnStatement') {
|
|
3265
|
-
|
|
3266
|
-
|
|
3370
|
+
return {
|
|
3371
|
+
...node,
|
|
3372
|
+
argument: combine_render_return_argument(render_nodes, node.argument, type_only),
|
|
3373
|
+
};
|
|
3267
3374
|
}
|
|
3268
3375
|
|
|
3269
3376
|
if (Array.isArray(node)) {
|
|
3270
|
-
|
|
3271
|
-
|
|
3377
|
+
let changed = false;
|
|
3378
|
+
const result = node.map((child) => {
|
|
3379
|
+
const walked = prepend_render_nodes_to_return_statement(
|
|
3272
3380
|
child,
|
|
3273
3381
|
render_nodes,
|
|
3274
3382
|
inside_nested_function,
|
|
3275
3383
|
type_only,
|
|
3276
3384
|
);
|
|
3277
|
-
|
|
3278
|
-
|
|
3385
|
+
if (walked !== child) changed = true;
|
|
3386
|
+
return walked;
|
|
3387
|
+
});
|
|
3388
|
+
return changed ? result : node;
|
|
3279
3389
|
}
|
|
3280
3390
|
|
|
3391
|
+
let out = node;
|
|
3281
3392
|
for (const key of Object.keys(node)) {
|
|
3282
3393
|
if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
|
|
3283
3394
|
continue;
|
|
3284
3395
|
}
|
|
3285
|
-
prepend_render_nodes_to_return_statement(
|
|
3396
|
+
const walked = prepend_render_nodes_to_return_statement(
|
|
3286
3397
|
node[key],
|
|
3287
3398
|
render_nodes,
|
|
3288
3399
|
inside_nested_function,
|
|
3289
3400
|
type_only,
|
|
3290
3401
|
);
|
|
3402
|
+
if (walked !== node[key]) {
|
|
3403
|
+
if (out === node) out = { ...node };
|
|
3404
|
+
out[key] = walked;
|
|
3405
|
+
}
|
|
3291
3406
|
}
|
|
3407
|
+
return out;
|
|
3292
3408
|
}
|
|
3293
3409
|
|
|
3294
3410
|
/**
|
|
@@ -4733,9 +4849,16 @@ function continue_to_bare_return(source_node) {
|
|
|
4733
4849
|
*/
|
|
4734
4850
|
export function rewrite_loop_continues_to_bare_returns(node, is_root = true) {
|
|
4735
4851
|
if (Array.isArray(node)) {
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4852
|
+
let changed = false;
|
|
4853
|
+
const result = node.map((child) => {
|
|
4854
|
+
const walked = rewrite_loop_continues_to_bare_returns(
|
|
4855
|
+
child,
|
|
4856
|
+
is_root && !is_loop_statement(child),
|
|
4857
|
+
);
|
|
4858
|
+
if (walked !== child) changed = true;
|
|
4859
|
+
return walked;
|
|
4860
|
+
});
|
|
4861
|
+
return changed ? result : node;
|
|
4739
4862
|
}
|
|
4740
4863
|
|
|
4741
4864
|
if (!node || typeof node !== 'object') {
|
|
@@ -4750,14 +4873,19 @@ export function rewrite_loop_continues_to_bare_returns(node, is_root = true) {
|
|
|
4750
4873
|
return node;
|
|
4751
4874
|
}
|
|
4752
4875
|
|
|
4876
|
+
let out = node;
|
|
4753
4877
|
for (const key of Object.keys(node)) {
|
|
4754
4878
|
if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
|
|
4755
4879
|
continue;
|
|
4756
4880
|
}
|
|
4757
|
-
|
|
4881
|
+
const walked = rewrite_loop_continues_to_bare_returns(node[key], false);
|
|
4882
|
+
if (walked !== node[key]) {
|
|
4883
|
+
if (out === node) out = { ...node };
|
|
4884
|
+
out[key] = walked;
|
|
4885
|
+
}
|
|
4758
4886
|
}
|
|
4759
4887
|
|
|
4760
|
-
return
|
|
4888
|
+
return out;
|
|
4761
4889
|
}
|
|
4762
4890
|
|
|
4763
4891
|
/**
|
|
@@ -4921,7 +5049,7 @@ function for_of_statement_to_jsx_child(node, transform_context) {
|
|
|
4921
5049
|
}
|
|
4922
5050
|
|
|
4923
5051
|
const loop_params = get_for_of_iteration_params(node.left, node.index);
|
|
4924
|
-
|
|
5052
|
+
let loop_body = /** @type {any[]} */ (
|
|
4925
5053
|
node.body.type === 'BlockStatement' ? node.body.body : [node.body]
|
|
4926
5054
|
);
|
|
4927
5055
|
validate_for_body_control_flow(loop_body, transform_context);
|
|
@@ -4952,10 +5080,10 @@ function for_of_statement_to_jsx_child(node, transform_context) {
|
|
|
4952
5080
|
}
|
|
4953
5081
|
|
|
4954
5082
|
if (implicit_non_hook_key_expression && should_apply_key_to_loop_body(loop_body)) {
|
|
4955
|
-
apply_key_to_loop_body(loop_body, implicit_non_hook_key_expression);
|
|
5083
|
+
loop_body = apply_key_to_loop_body(loop_body, implicit_non_hook_key_expression);
|
|
4956
5084
|
}
|
|
4957
5085
|
|
|
4958
|
-
|
|
5086
|
+
let body_statements = has_hooks
|
|
4959
5087
|
? hook_safe_render_statements(loop_body, key_expression, transform_context)
|
|
4960
5088
|
: build_render_statements(loop_body, true, transform_context);
|
|
4961
5089
|
|
|
@@ -4972,7 +5100,11 @@ function for_of_statement_to_jsx_child(node, transform_context) {
|
|
|
4972
5100
|
|
|
4973
5101
|
const non_hook_key_expression = key_expression ?? implicit_non_hook_key_expression;
|
|
4974
5102
|
if (!has_hooks && non_hook_key_expression) {
|
|
4975
|
-
|
|
5103
|
+
body_statements = apply_key_to_render_statements(
|
|
5104
|
+
body_statements,
|
|
5105
|
+
non_hook_key_expression,
|
|
5106
|
+
transform_context,
|
|
5107
|
+
);
|
|
4976
5108
|
}
|
|
4977
5109
|
|
|
4978
5110
|
// Restore bindings
|
|
@@ -5025,32 +5157,40 @@ function for_of_statement_to_jsx_child(node, transform_context) {
|
|
|
5025
5157
|
}
|
|
5026
5158
|
|
|
5027
5159
|
/**
|
|
5160
|
+
* Returns a copy of `body_nodes` where the first keyable element carries the
|
|
5161
|
+
* key attribute on a rebuilt opening element — the source nodes are never
|
|
5162
|
+
* mutated (they may belong to the caller's parsed AST).
|
|
5028
5163
|
* @param {any[]} body_nodes
|
|
5029
5164
|
* @param {any} key_expression
|
|
5030
|
-
* @returns {
|
|
5165
|
+
* @returns {any[]}
|
|
5031
5166
|
*/
|
|
5032
5167
|
function apply_key_to_loop_body(body_nodes, key_expression) {
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5168
|
+
let applied = false;
|
|
5169
|
+
return body_nodes.map((node) => {
|
|
5170
|
+
if (applied || node.type !== 'JSXElement') return node;
|
|
5171
|
+
applied = true;
|
|
5172
|
+
const attributes = node.openingElement?.attributes || [];
|
|
5173
|
+
const has_key = attributes.some(
|
|
5174
|
+
(/** @type {any} */ attr) =>
|
|
5175
|
+
attr.type === 'JSXAttribute' &&
|
|
5176
|
+
attr.name?.type === 'JSXIdentifier' &&
|
|
5177
|
+
attr.name.name === 'key',
|
|
5178
|
+
);
|
|
5179
|
+
if (has_key) return node;
|
|
5180
|
+
return {
|
|
5181
|
+
...node,
|
|
5182
|
+
openingElement: {
|
|
5183
|
+
...node.openingElement,
|
|
5184
|
+
attributes: [
|
|
5185
|
+
...attributes,
|
|
5045
5186
|
b.jsx_attribute(
|
|
5046
5187
|
b.jsx_id('key'),
|
|
5047
5188
|
to_jsx_expression_container(clone_expression_node(key_expression), key_expression),
|
|
5048
5189
|
),
|
|
5049
|
-
|
|
5050
|
-
}
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
}
|
|
5190
|
+
],
|
|
5191
|
+
},
|
|
5192
|
+
};
|
|
5193
|
+
});
|
|
5054
5194
|
}
|
|
5055
5195
|
|
|
5056
5196
|
/**
|
|
@@ -5068,10 +5208,14 @@ function should_apply_key_to_loop_body(body_nodes) {
|
|
|
5068
5208
|
}
|
|
5069
5209
|
|
|
5070
5210
|
/**
|
|
5211
|
+
* Statement entries can be shared with the source tree, so the keyed return
|
|
5212
|
+
* lands on shallow copies; the returned array must be used in place of the
|
|
5213
|
+
* argument.
|
|
5214
|
+
*
|
|
5071
5215
|
* @param {any[]} statements
|
|
5072
5216
|
* @param {any} key_expression
|
|
5073
5217
|
* @param {TransformContext} transform_context
|
|
5074
|
-
* @returns {
|
|
5218
|
+
* @returns {any[]}
|
|
5075
5219
|
*/
|
|
5076
5220
|
function apply_key_to_render_statements(statements, key_expression, transform_context) {
|
|
5077
5221
|
for (let i = statements.length - 1; i >= 0; i -= 1) {
|
|
@@ -5080,21 +5224,29 @@ function apply_key_to_render_statements(statements, key_expression, transform_co
|
|
|
5080
5224
|
continue;
|
|
5081
5225
|
}
|
|
5082
5226
|
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5227
|
+
let argument = statement.argument;
|
|
5228
|
+
if (argument.type === 'JSXElement') {
|
|
5229
|
+
argument = apply_key_to_jsx_element(argument, key_expression);
|
|
5230
|
+
} else if (argument.type === 'JSXFragment') {
|
|
5086
5231
|
transform_context.needs_fragment = true;
|
|
5087
|
-
|
|
5232
|
+
argument = keyed_fragment_to_jsx_element(argument, key_expression);
|
|
5088
5233
|
}
|
|
5089
5234
|
|
|
5090
|
-
|
|
5235
|
+
if (argument === statement.argument) {
|
|
5236
|
+
return statements;
|
|
5237
|
+
}
|
|
5238
|
+
const result = [...statements];
|
|
5239
|
+
result[i] = { ...statement, argument };
|
|
5240
|
+
return result;
|
|
5091
5241
|
}
|
|
5242
|
+
return statements;
|
|
5092
5243
|
}
|
|
5093
5244
|
|
|
5094
5245
|
/**
|
|
5095
5246
|
* @param {any} element
|
|
5096
5247
|
* @param {any} key_expression
|
|
5097
|
-
* @returns {
|
|
5248
|
+
* @returns {any} the element itself when it already has a `key`, otherwise a
|
|
5249
|
+
* shallow copy with the key attribute appended.
|
|
5098
5250
|
*/
|
|
5099
5251
|
function apply_key_to_jsx_element(element, key_expression) {
|
|
5100
5252
|
const attributes = element.openingElement?.attributes || [];
|
|
@@ -5104,15 +5256,21 @@ function apply_key_to_jsx_element(element, key_expression) {
|
|
|
5104
5256
|
attr.name?.type === 'JSXIdentifier' &&
|
|
5105
5257
|
attr.name.name === 'key',
|
|
5106
5258
|
);
|
|
5259
|
+
if (has_key) return element;
|
|
5107
5260
|
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5261
|
+
return {
|
|
5262
|
+
...element,
|
|
5263
|
+
openingElement: {
|
|
5264
|
+
...element.openingElement,
|
|
5265
|
+
attributes: [
|
|
5266
|
+
...attributes,
|
|
5267
|
+
b.jsx_attribute(
|
|
5268
|
+
b.jsx_id('key'),
|
|
5269
|
+
to_jsx_expression_container(clone_expression_node(key_expression), key_expression),
|
|
5270
|
+
),
|
|
5271
|
+
],
|
|
5272
|
+
},
|
|
5273
|
+
};
|
|
5116
5274
|
}
|
|
5117
5275
|
|
|
5118
5276
|
/**
|
|
@@ -962,19 +962,12 @@ export function convert_source_map_to_mappings(
|
|
|
962
962
|
const is_method = node.metadata?.is_method;
|
|
963
963
|
|
|
964
964
|
if (node.type === 'ArrowFunctionExpression' && node.loc) {
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
node,
|
|
972
|
-
src_to_gen_map,
|
|
973
|
-
gen_line_offsets,
|
|
974
|
-
mapping_data_verify_only,
|
|
975
|
-
),
|
|
976
|
-
);
|
|
977
|
-
}
|
|
965
|
+
// The printer emits node-level boundary markers for arrows (their
|
|
966
|
+
// span can start at a bare `(`), so the strict lookup always
|
|
967
|
+
// resolves — no defensive has() guard.
|
|
968
|
+
mappings.push(
|
|
969
|
+
get_mapping_from_node(node, src_to_gen_map, gen_line_offsets, mapping_data_verify_only),
|
|
970
|
+
);
|
|
978
971
|
}
|
|
979
972
|
|
|
980
973
|
// Add the function keyword token.
|
|
@@ -987,41 +980,68 @@ export function convert_source_map_to_mappings(
|
|
|
987
980
|
const function_hover = create_function_hover_replacement(
|
|
988
981
|
/** @type {AST.Parameter[]} */ (node.params),
|
|
989
982
|
);
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
983
|
+
// Keyword SOURCE spans come from the LEXER (parse-time
|
|
984
|
+
// `tsrx_keyword_tokens`, opt-in via ParseOptions.keywordTokens):
|
|
985
|
+
// no AST node records them, offset arithmetic breaks on extra
|
|
986
|
+
// whitespace, and text search breaks on comments. Fall back to
|
|
987
|
+
// node-start-anchored arithmetic when tokens were not collected.
|
|
988
|
+
const keyword_bound =
|
|
989
|
+
node_fn.id?.start ?? node_fn.params?.[0]?.start ?? node_fn.body?.start ?? node_fn.end;
|
|
990
|
+
/** @type {Array<{ value: string, start: number, end: number, loc: AST.SourceLocation }>} */
|
|
991
|
+
const lexer_tokens = /** @type {any} */ (ast_from_source).tsrx_keyword_tokens ?? [];
|
|
992
|
+
/**
|
|
993
|
+
* @param {'async' | 'function'} keyword
|
|
994
|
+
* @param {number} from
|
|
995
|
+
* @returns {AST.SourceLocation | null}
|
|
996
|
+
*/
|
|
997
|
+
const keyword_loc = (keyword, from) => {
|
|
998
|
+
const token = lexer_tokens.find(
|
|
999
|
+
(candidate) =>
|
|
1000
|
+
candidate.value === keyword &&
|
|
1001
|
+
candidate.start >= from &&
|
|
1002
|
+
candidate.start < keyword_bound,
|
|
1003
|
+
);
|
|
1004
|
+
if (token) return token.loc;
|
|
1005
|
+
if (lexer_tokens.length > 0) return null;
|
|
1006
|
+
// Arithmetic fallback (callers that do not collect tokens):
|
|
1007
|
+
// assumes the historical `async` + one-space + `function`
|
|
1008
|
+
// single-line layout.
|
|
1009
|
+
const offset =
|
|
1010
|
+
keyword === 'function' && node_fn.async
|
|
1011
|
+
? node_fn.start + 'async '.length
|
|
1012
|
+
: node_fn.start;
|
|
1013
|
+
const start_pos = offset_to_line_col(offset, src_line_offsets);
|
|
1014
|
+
const end_pos = offset_to_line_col(offset + keyword.length, src_line_offsets);
|
|
1015
|
+
return { start: start_pos, end: end_pos };
|
|
1016
|
+
};
|
|
993
1017
|
|
|
1018
|
+
let function_from = node_fn.start;
|
|
994
1019
|
if (node_fn.async) {
|
|
995
|
-
|
|
1020
|
+
const async_loc = keyword_loc('async', node_fn.start);
|
|
1021
|
+
if (async_loc) {
|
|
1022
|
+
tokens.push({
|
|
1023
|
+
source: 'async',
|
|
1024
|
+
generated: 'async',
|
|
1025
|
+
loc: async_loc,
|
|
1026
|
+
metadata: {},
|
|
1027
|
+
});
|
|
1028
|
+
function_from = loc_to_offset(
|
|
1029
|
+
async_loc.end.line,
|
|
1030
|
+
async_loc.end.column,
|
|
1031
|
+
src_line_offsets,
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
const function_loc = keyword_loc('function', function_from);
|
|
1037
|
+
if (function_loc) {
|
|
996
1038
|
tokens.push({
|
|
997
|
-
source:
|
|
998
|
-
generated:
|
|
999
|
-
loc:
|
|
1000
|
-
|
|
1001
|
-
end: {
|
|
1002
|
-
line: node_fn.loc.start.line,
|
|
1003
|
-
column: start_col + async_keyword.length,
|
|
1004
|
-
},
|
|
1005
|
-
},
|
|
1006
|
-
metadata: {},
|
|
1039
|
+
source: 'function',
|
|
1040
|
+
generated: 'function',
|
|
1041
|
+
loc: function_loc,
|
|
1042
|
+
metadata: function_hover ? { hover: function_hover } : {},
|
|
1007
1043
|
});
|
|
1008
|
-
|
|
1009
|
-
start_col += async_keyword.length + 1; // +1 for space
|
|
1010
|
-
start += async_keyword.length + 1;
|
|
1011
1044
|
}
|
|
1012
|
-
|
|
1013
|
-
tokens.push({
|
|
1014
|
-
source: 'function',
|
|
1015
|
-
generated: 'function',
|
|
1016
|
-
loc: {
|
|
1017
|
-
start: { line: node_fn.loc.start.line, column: start_col },
|
|
1018
|
-
end: {
|
|
1019
|
-
line: node_fn.loc.start.line,
|
|
1020
|
-
column: start_col + 'function'.length,
|
|
1021
|
-
},
|
|
1022
|
-
},
|
|
1023
|
-
metadata: function_hover ? { hover: function_hover } : {},
|
|
1024
|
-
});
|
|
1025
1045
|
}
|
|
1026
1046
|
|
|
1027
1047
|
// Visit in source order: id, params, body
|
package/src/utils/builders.js
CHANGED
|
@@ -251,6 +251,22 @@ export function export_default(declaration) {
|
|
|
251
251
|
return { type: 'ExportDefaultDeclaration', declaration, metadata: { path: [] } };
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
+
/**
|
|
255
|
+
* @param {string | AST.Identifier} local
|
|
256
|
+
* @param {string | AST.Identifier} [exported]
|
|
257
|
+
* @param {AST.ExportSpecifier['exportKind']} [exportKind]
|
|
258
|
+
* @returns {AST.ExportSpecifier}
|
|
259
|
+
*/
|
|
260
|
+
export function export_specifier(local, exported = local, exportKind = 'value') {
|
|
261
|
+
return {
|
|
262
|
+
type: 'ExportSpecifier',
|
|
263
|
+
local: typeof local === 'string' ? id(local) : local,
|
|
264
|
+
exported: typeof exported === 'string' ? id(exported) : exported,
|
|
265
|
+
exportKind,
|
|
266
|
+
metadata: { path: [] },
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
254
270
|
/**
|
|
255
271
|
* @param {AST.Declaration | null} declaration
|
|
256
272
|
* @param {AST.ExportSpecifier[]} [specifiers]
|
package/types/index.d.ts
CHANGED
|
@@ -230,12 +230,14 @@ declare module 'estree' {
|
|
|
230
230
|
interface MethodDefinition {
|
|
231
231
|
typeParameters?: TSTypeParameterDeclaration;
|
|
232
232
|
accessibility?: Accessibility;
|
|
233
|
+
optional?: boolean;
|
|
233
234
|
}
|
|
234
235
|
|
|
235
236
|
interface PropertyDefinition {
|
|
236
237
|
accessibility?: Accessibility;
|
|
237
238
|
readonly?: boolean;
|
|
238
239
|
optional?: boolean;
|
|
240
|
+
definite?: boolean;
|
|
239
241
|
}
|
|
240
242
|
|
|
241
243
|
interface ClassDeclaration {
|
|
@@ -484,6 +486,9 @@ declare module 'estree' {
|
|
|
484
486
|
interface ExportNamedDeclaration {
|
|
485
487
|
exportKind: TSESTree.ExportNamedDeclaration['exportKind'];
|
|
486
488
|
}
|
|
489
|
+
interface ExportSpecifier {
|
|
490
|
+
exportKind: TSESTree.ExportSpecifier['exportKind'];
|
|
491
|
+
}
|
|
487
492
|
|
|
488
493
|
interface BaseNodeWithoutComments {
|
|
489
494
|
// Adding start, end for now as always there
|
|
@@ -1231,6 +1236,13 @@ export interface ParseError {
|
|
|
1231
1236
|
export interface ParseOptions {
|
|
1232
1237
|
collect?: boolean;
|
|
1233
1238
|
loose?: boolean;
|
|
1239
|
+
preserveParens?: boolean;
|
|
1240
|
+
/**
|
|
1241
|
+
* Collect `async`/`function` keyword tokens from the lexer onto the
|
|
1242
|
+
* returned program (`tsrx_keyword_tokens`) so mapping collection can span
|
|
1243
|
+
* keywords exactly. Volar/typeOnly parses opt in.
|
|
1244
|
+
*/
|
|
1245
|
+
keywordTokens?: boolean;
|
|
1234
1246
|
errors?: CompileError[];
|
|
1235
1247
|
comments?: AST.CommentWithLocation[];
|
|
1236
1248
|
}
|