@tsrx/core 0.0.13 → 0.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/analyze/validation.js +47 -0
- package/src/index.js +11 -1
- package/src/plugin.js +280 -23
- package/src/transform/jsx/helpers.js +17 -0
- package/src/transform/jsx/index.js +540 -349
- package/src/transform/lazy.js +185 -13
- package/src/transform/segments.js +117 -7
- package/src/utils/ast.js +61 -0
- package/types/index.d.ts +17 -0
package/package.json
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
import { error } from '../errors.js';
|
|
7
7
|
|
|
8
|
+
export const COMPONENT_RETURN_VALUE_ERROR =
|
|
9
|
+
'Return statements inside components cannot have a return value.';
|
|
10
|
+
|
|
8
11
|
const invalid_nestings = {
|
|
9
12
|
// <p> cannot contain block-level elements
|
|
10
13
|
p: new Set([
|
|
@@ -125,6 +128,50 @@ function get_element_tag(element) {
|
|
|
125
128
|
return element.id.type === 'Identifier' ? element.id.name : null;
|
|
126
129
|
}
|
|
127
130
|
|
|
131
|
+
/**
|
|
132
|
+
* @param {AST.ReturnStatement} node
|
|
133
|
+
* @returns {AST.ReturnStatement}
|
|
134
|
+
*/
|
|
135
|
+
export function get_return_keyword_node(node) {
|
|
136
|
+
const return_keyword_length = 'return'.length;
|
|
137
|
+
const start = /** @type {AST.NodeWithLocation} */ (node).start ?? 0;
|
|
138
|
+
const loc = /** @type {AST.NodeWithLocation} */ (node).loc;
|
|
139
|
+
|
|
140
|
+
return /** @type {AST.ReturnStatement} */ ({
|
|
141
|
+
...node,
|
|
142
|
+
end: start + return_keyword_length,
|
|
143
|
+
loc: loc
|
|
144
|
+
? {
|
|
145
|
+
start: loc.start,
|
|
146
|
+
end: {
|
|
147
|
+
line: loc.start.line,
|
|
148
|
+
column: loc.start.column + return_keyword_length,
|
|
149
|
+
},
|
|
150
|
+
}
|
|
151
|
+
: undefined,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* @param {AST.ReturnStatement} node
|
|
157
|
+
* @param {string | null | undefined} filename
|
|
158
|
+
* @param {CompileError[]} [errors]
|
|
159
|
+
* @param {AST.CommentWithLocation[]} [comments]
|
|
160
|
+
*/
|
|
161
|
+
export function validate_component_return_statement(node, filename, errors, comments) {
|
|
162
|
+
if (node.argument === null) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
error(
|
|
167
|
+
COMPONENT_RETURN_VALUE_ERROR,
|
|
168
|
+
filename ?? null,
|
|
169
|
+
get_return_keyword_node(node),
|
|
170
|
+
errors,
|
|
171
|
+
comments,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
128
175
|
/**
|
|
129
176
|
* @param {AST.Element} element
|
|
130
177
|
* @param {AnalysisContext} context
|
package/src/index.js
CHANGED
|
@@ -78,12 +78,17 @@ export {
|
|
|
78
78
|
|
|
79
79
|
// AST utils
|
|
80
80
|
export {
|
|
81
|
+
get_component_from_path as getComponentFromPath,
|
|
81
82
|
object,
|
|
82
83
|
unwrap_pattern as unwrapPattern,
|
|
83
84
|
extract_identifiers as extractIdentifiers,
|
|
84
85
|
extract_paths as extractPaths,
|
|
85
86
|
build_fallback as buildFallback,
|
|
86
87
|
build_assignment_value as buildAssignmentValue,
|
|
88
|
+
is_class_node as isClassNode,
|
|
89
|
+
is_component_node as isComponentNode,
|
|
90
|
+
is_function_node as isFunctionNode,
|
|
91
|
+
is_inside_component as isInsideComponent,
|
|
87
92
|
} from './utils/ast.js';
|
|
88
93
|
|
|
89
94
|
// Builders (namespace re-export — members mirror AST node kinds)
|
|
@@ -197,4 +202,9 @@ export {
|
|
|
197
202
|
|
|
198
203
|
// Analyze
|
|
199
204
|
export { analyze_css as analyzeCss } from './analyze/css-analyze.js';
|
|
200
|
-
export {
|
|
205
|
+
export {
|
|
206
|
+
COMPONENT_RETURN_VALUE_ERROR,
|
|
207
|
+
get_return_keyword_node as getReturnKeywordNode,
|
|
208
|
+
validate_component_return_statement as validateComponentReturnStatement,
|
|
209
|
+
validate_nesting as validateNesting,
|
|
210
|
+
} from './analyze/validation.js';
|
package/src/plugin.js
CHANGED
|
@@ -16,6 +16,143 @@ import {
|
|
|
16
16
|
import { regex_newline_characters } from './utils/patterns.js';
|
|
17
17
|
import { error } from './errors.js';
|
|
18
18
|
|
|
19
|
+
/** @type {WeakMap<Record<string, boolean>, Map<string, number>>} */
|
|
20
|
+
const argument_clash_first_positions = new WeakMap();
|
|
21
|
+
/** @type {WeakMap<Record<string, boolean>, Set<string>>} */
|
|
22
|
+
const argument_clash_reported_names = new WeakMap();
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {Record<string, boolean>} check_clashes
|
|
26
|
+
* @returns {Map<string, number>}
|
|
27
|
+
*/
|
|
28
|
+
function get_argument_clash_first_positions(check_clashes) {
|
|
29
|
+
let first_positions = argument_clash_first_positions.get(check_clashes);
|
|
30
|
+
if (!first_positions) {
|
|
31
|
+
first_positions = new Map();
|
|
32
|
+
argument_clash_first_positions.set(check_clashes, first_positions);
|
|
33
|
+
}
|
|
34
|
+
return first_positions;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {Record<string, boolean>} check_clashes
|
|
39
|
+
* @returns {Set<string>}
|
|
40
|
+
*/
|
|
41
|
+
function get_argument_clash_reported_names(check_clashes) {
|
|
42
|
+
let reported_names = argument_clash_reported_names.get(check_clashes);
|
|
43
|
+
if (!reported_names) {
|
|
44
|
+
reported_names = new Set();
|
|
45
|
+
argument_clash_reported_names.set(check_clashes, reported_names);
|
|
46
|
+
}
|
|
47
|
+
return reported_names;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {string} input
|
|
52
|
+
* @param {number} i
|
|
53
|
+
*/
|
|
54
|
+
function skip_whitespace_from(input, i) {
|
|
55
|
+
while (i < input.length) {
|
|
56
|
+
const ch = input.charCodeAt(i);
|
|
57
|
+
if (ch !== 32 && ch !== 9 && ch !== 10 && ch !== 13) break;
|
|
58
|
+
i++;
|
|
59
|
+
}
|
|
60
|
+
return i;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Skip past a string literal opened at `i` with the given quote char code.
|
|
65
|
+
* @param {string} input
|
|
66
|
+
* @param {number} i
|
|
67
|
+
* @param {number} quote
|
|
68
|
+
*/
|
|
69
|
+
function skip_string_from(input, i, quote) {
|
|
70
|
+
i++;
|
|
71
|
+
while (i < input.length) {
|
|
72
|
+
const ch = input.charCodeAt(i);
|
|
73
|
+
i++;
|
|
74
|
+
if (ch === 92)
|
|
75
|
+
i++; // backslash escape
|
|
76
|
+
else if (ch === quote) return i;
|
|
77
|
+
}
|
|
78
|
+
return i;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Scan past a balanced pair starting at `i` (which must point at `open`).
|
|
83
|
+
* Returns the position after the matching close, or -1 if unbalanced.
|
|
84
|
+
* @param {string} input
|
|
85
|
+
* @param {number} i
|
|
86
|
+
* @param {number} open
|
|
87
|
+
* @param {number} close
|
|
88
|
+
*/
|
|
89
|
+
function scan_balanced_from(input, i, open, close) {
|
|
90
|
+
let depth = 1;
|
|
91
|
+
i++;
|
|
92
|
+
while (i < input.length) {
|
|
93
|
+
const ch = input.charCodeAt(i);
|
|
94
|
+
if (ch === 34 || ch === 39 || ch === 96) {
|
|
95
|
+
i = skip_string_from(input, i, ch);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (ch === open) depth++;
|
|
99
|
+
else if (ch === close && --depth === 0) return i + 1;
|
|
100
|
+
i++;
|
|
101
|
+
}
|
|
102
|
+
return -1;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Best-effort lookahead at a `<` to decide whether it starts a generic arrow
|
|
107
|
+
* expression — `<...>(...)[: T] => ...`. Conservative: returns false on any
|
|
108
|
+
* unexpected shape so JSX continues to parse as JSX.
|
|
109
|
+
* @param {string} input
|
|
110
|
+
* @param {number} pos
|
|
111
|
+
*/
|
|
112
|
+
function looks_like_generic_arrow(input, pos) {
|
|
113
|
+
if (input.charCodeAt(pos) !== 60) return false;
|
|
114
|
+
|
|
115
|
+
// Match the angle brackets, skipping over string literals.
|
|
116
|
+
let i = pos + 1;
|
|
117
|
+
let depth = 1;
|
|
118
|
+
while (i < input.length) {
|
|
119
|
+
const ch = input.charCodeAt(i);
|
|
120
|
+
if (ch === 34 || ch === 39 || ch === 96) {
|
|
121
|
+
i = skip_string_from(input, i, ch);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (ch === 60) depth++;
|
|
125
|
+
else if (ch === 62 && --depth === 0) break;
|
|
126
|
+
i++;
|
|
127
|
+
}
|
|
128
|
+
if (depth !== 0) return false;
|
|
129
|
+
|
|
130
|
+
// `>` must be followed by `(...)`.
|
|
131
|
+
i = skip_whitespace_from(input, i + 1);
|
|
132
|
+
if (input.charCodeAt(i) !== 40) return false;
|
|
133
|
+
i = scan_balanced_from(input, i, 40, 41);
|
|
134
|
+
if (i === -1) return false;
|
|
135
|
+
|
|
136
|
+
// Optional `: ReturnType` before `=>`.
|
|
137
|
+
i = skip_whitespace_from(input, i);
|
|
138
|
+
if (input.charCodeAt(i) === 58) {
|
|
139
|
+
i++;
|
|
140
|
+
while (i < input.length) {
|
|
141
|
+
const ch = input.charCodeAt(i);
|
|
142
|
+
if (ch === 34 || ch === 39 || ch === 96) {
|
|
143
|
+
i = skip_string_from(input, i, ch);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (ch === 61 && input.charCodeAt(i + 1) === 62) return true;
|
|
147
|
+
if (ch === 59 || ch === 123 || ch === 125) return false;
|
|
148
|
+
i++;
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return input.charCodeAt(i) === 61 && input.charCodeAt(i + 1) === 62;
|
|
154
|
+
}
|
|
155
|
+
|
|
19
156
|
/**
|
|
20
157
|
* Acorn parser plugin for Ripple syntax extensions.
|
|
21
158
|
* Adds support for: component declarations, &[]/&{} lazy destructuring,
|
|
@@ -44,6 +181,7 @@ export function TSRXPlugin(config) {
|
|
|
44
181
|
#errors = undefined;
|
|
45
182
|
/** @type {string | null} */
|
|
46
183
|
#filename = null;
|
|
184
|
+
#functionBodyDepth = 0;
|
|
47
185
|
|
|
48
186
|
/**
|
|
49
187
|
* @param {Parse.Options} options
|
|
@@ -59,20 +197,21 @@ export function TSRXPlugin(config) {
|
|
|
59
197
|
|
|
60
198
|
/**
|
|
61
199
|
* @param {number} position
|
|
200
|
+
* @param {number} end
|
|
62
201
|
* @param {string} message
|
|
63
202
|
*/
|
|
64
|
-
#
|
|
203
|
+
#report_recoverable_error_range(position, end, message) {
|
|
65
204
|
const start = Math.max(0, Math.min(position, this.input.length));
|
|
66
|
-
const
|
|
205
|
+
const range_end = Math.max(start, Math.min(end, this.input.length));
|
|
67
206
|
const start_loc = acorn.getLineInfo(this.input, start);
|
|
68
|
-
const end_loc = acorn.getLineInfo(this.input,
|
|
207
|
+
const end_loc = acorn.getLineInfo(this.input, range_end);
|
|
69
208
|
|
|
70
209
|
error(
|
|
71
210
|
message,
|
|
72
211
|
this.#filename,
|
|
73
212
|
/** @type {AST.NodeWithLocation} */ ({
|
|
74
213
|
start,
|
|
75
|
-
end,
|
|
214
|
+
end: range_end,
|
|
76
215
|
loc: {
|
|
77
216
|
start: start_loc,
|
|
78
217
|
end: end_loc,
|
|
@@ -82,6 +221,14 @@ export function TSRXPlugin(config) {
|
|
|
82
221
|
);
|
|
83
222
|
}
|
|
84
223
|
|
|
224
|
+
/**
|
|
225
|
+
* @param {number} position
|
|
226
|
+
* @param {string} message
|
|
227
|
+
*/
|
|
228
|
+
#report_recoverable_error(position, message) {
|
|
229
|
+
this.#report_recoverable_error_range(position, position + 1, message);
|
|
230
|
+
}
|
|
231
|
+
|
|
85
232
|
/**
|
|
86
233
|
* In loose mode, keep parsing after duplicate declaration diagnostics so
|
|
87
234
|
* editor tooling can continue producing AST and mappings.
|
|
@@ -96,7 +243,10 @@ export function TSRXPlugin(config) {
|
|
|
96
243
|
? message.message
|
|
97
244
|
: String(message);
|
|
98
245
|
|
|
99
|
-
if (
|
|
246
|
+
if (
|
|
247
|
+
error_message.includes('has already been declared') ||
|
|
248
|
+
error_message === 'Argument name clash'
|
|
249
|
+
) {
|
|
100
250
|
this.#report_recoverable_error(position, error_message);
|
|
101
251
|
return;
|
|
102
252
|
}
|
|
@@ -381,6 +531,28 @@ export function TSRXPlugin(config) {
|
|
|
381
531
|
return null;
|
|
382
532
|
}
|
|
383
533
|
|
|
534
|
+
/**
|
|
535
|
+
* Inside a component, `<T,>(x: T) => x` should parse as a generic arrow
|
|
536
|
+
* function, not a JSX element. acorn-typescript's `readToken` would
|
|
537
|
+
* otherwise tokenize `<` as `jsxTagStart` (when `exprAllowed` or the
|
|
538
|
+
* context is `tc_expr`), bypassing our `getTokenFromCode` override. We
|
|
539
|
+
* intercept here, but only when the source from `<` actually looks like
|
|
540
|
+
* a generic arrow expression — so JSX like `<div>` keeps parsing normally.
|
|
541
|
+
*
|
|
542
|
+
* @type {Parse.Parser['readToken']}
|
|
543
|
+
*/
|
|
544
|
+
readToken(code) {
|
|
545
|
+
if (
|
|
546
|
+
code === 60 &&
|
|
547
|
+
this.#path.findLast((n) => n.type === 'Component') &&
|
|
548
|
+
looks_like_generic_arrow(this.input, this.pos)
|
|
549
|
+
) {
|
|
550
|
+
++this.pos;
|
|
551
|
+
return this.finishToken(tt.relational, '<');
|
|
552
|
+
}
|
|
553
|
+
return super.readToken(code);
|
|
554
|
+
}
|
|
555
|
+
|
|
384
556
|
/**
|
|
385
557
|
* Get token from character code - handles Ripple-specific tokens
|
|
386
558
|
* @type {Parse.Parser['getTokenFromCode']}
|
|
@@ -469,22 +641,8 @@ export function TSRXPlugin(config) {
|
|
|
469
641
|
}
|
|
470
642
|
}
|
|
471
643
|
|
|
472
|
-
//
|
|
473
|
-
//
|
|
474
|
-
// (like for loops, if blocks, JSX elements, etc.)
|
|
475
|
-
const nestedFunctionContext = this.context.some((ctx) => ctx.token === 'function');
|
|
476
|
-
|
|
477
|
-
// Inside nested functions, treat < as relational/generic operator
|
|
478
|
-
// BUT: if the < is followed by /, it's a closing JSX tag, not a less-than operator
|
|
479
|
-
const nextChar =
|
|
480
|
-
this.pos + 1 < this.input.length ? this.input.charCodeAt(this.pos + 1) : -1;
|
|
481
|
-
const isClosingTag = nextChar === 47; // '/'
|
|
482
|
-
|
|
483
|
-
if (nestedFunctionContext && !isClosingTag) {
|
|
484
|
-
// Inside function - treat as TypeScript generic, not JSX
|
|
485
|
-
++this.pos;
|
|
486
|
-
return this.finishToken(tt.relational, '<');
|
|
487
|
-
}
|
|
644
|
+
// `<` inside a nested function body is intercepted earlier in
|
|
645
|
+
// `readToken` so it never reaches this path.
|
|
488
646
|
|
|
489
647
|
// Check if everything before this position on the current line is whitespace
|
|
490
648
|
let lineStart = this.pos - 1;
|
|
@@ -599,6 +757,67 @@ export function TSRXPlugin(config) {
|
|
|
599
757
|
return super.parseBindingAtom();
|
|
600
758
|
}
|
|
601
759
|
|
|
760
|
+
/**
|
|
761
|
+
* Acorn reports only the second duplicate function parameter. In loose
|
|
762
|
+
* mode, report the first one too so editor diagnostics can underline both
|
|
763
|
+
* binding sites. Keep strict mode on Acorn's normal fatal path.
|
|
764
|
+
*
|
|
765
|
+
* @type {Parse.Parser['checkLValSimple']}
|
|
766
|
+
*/
|
|
767
|
+
checkLValSimple(expr, bindingType = BINDING_TYPES.BIND_NONE, checkClashes) {
|
|
768
|
+
if (
|
|
769
|
+
this.#loose &&
|
|
770
|
+
expr.type === 'Identifier' &&
|
|
771
|
+
bindingType !== BINDING_TYPES.BIND_NONE &&
|
|
772
|
+
checkClashes
|
|
773
|
+
) {
|
|
774
|
+
const first_positions = get_argument_clash_first_positions(checkClashes);
|
|
775
|
+
const reported_names = get_argument_clash_reported_names(checkClashes);
|
|
776
|
+
const first_position = first_positions.get(expr.name);
|
|
777
|
+
|
|
778
|
+
if (Object.prototype.hasOwnProperty.call(checkClashes, expr.name)) {
|
|
779
|
+
if (first_position != null && !reported_names.has(expr.name)) {
|
|
780
|
+
this.#report_recoverable_error_range(
|
|
781
|
+
first_position,
|
|
782
|
+
first_position + expr.name.length,
|
|
783
|
+
'Argument name clash',
|
|
784
|
+
);
|
|
785
|
+
reported_names.add(expr.name);
|
|
786
|
+
}
|
|
787
|
+
const start = /** @type {number} */ (expr.start);
|
|
788
|
+
this.#report_recoverable_error_range(
|
|
789
|
+
start,
|
|
790
|
+
/** @type {number} */ (expr.end ?? start + expr.name.length),
|
|
791
|
+
'Argument name clash',
|
|
792
|
+
);
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const result = super.checkLValSimple(expr, bindingType, checkClashes);
|
|
797
|
+
first_positions.set(expr.name, /** @type {number} */ (expr.start));
|
|
798
|
+
return result;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
return super.checkLValSimple(expr, bindingType, checkClashes);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Components do not use Acorn's normal function-body parser, but they
|
|
806
|
+
* should still report duplicate parameter names like functions do. Keep
|
|
807
|
+
* this validation on `BIND_OUTSIDE` so params are checked without being
|
|
808
|
+
* declared in the component template scope, preserving existing shadowing
|
|
809
|
+
* behavior.
|
|
810
|
+
*
|
|
811
|
+
* @param {AST.Pattern[]} params
|
|
812
|
+
*/
|
|
813
|
+
checkComponentParams(params) {
|
|
814
|
+
/** @type {Record<string, boolean>} */
|
|
815
|
+
const name_hash = Object.create(null);
|
|
816
|
+
for (const param of params || []) {
|
|
817
|
+
this.checkLValInnerPattern(param, BINDING_TYPES.BIND_OUTSIDE, name_hash);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
602
821
|
/**
|
|
603
822
|
* Parse expression atom - handles RippleArray and RippleObject literals
|
|
604
823
|
* @type {Parse.Parser['parseExprAtom']}
|
|
@@ -741,11 +960,24 @@ export function TSRXPlugin(config) {
|
|
|
741
960
|
}
|
|
742
961
|
|
|
743
962
|
this.parseFunctionParams(node);
|
|
963
|
+
this.checkComponentParams(node.params);
|
|
964
|
+
|
|
965
|
+
// Reset before `eat(braceL)` so the lookahead `next()` it triggers reads
|
|
966
|
+
// the component body's first token as if we'd entered fresh — no
|
|
967
|
+
// surrounding function body should affect our parseStatement/parseBlock
|
|
968
|
+
// branching while inside the template.
|
|
969
|
+
const parent_function_body_depth = this.#functionBodyDepth;
|
|
970
|
+
this.#functionBodyDepth = 0;
|
|
971
|
+
|
|
744
972
|
this.eat(tt.braceL);
|
|
745
973
|
node.body = [];
|
|
746
974
|
this.#path.push(node);
|
|
747
975
|
|
|
748
|
-
|
|
976
|
+
try {
|
|
977
|
+
this.parseTemplateBody(node.body);
|
|
978
|
+
} finally {
|
|
979
|
+
this.#functionBodyDepth = parent_function_body_depth;
|
|
980
|
+
}
|
|
749
981
|
this.#path.pop();
|
|
750
982
|
this.exitScope();
|
|
751
983
|
|
|
@@ -962,6 +1194,19 @@ export function TSRXPlugin(config) {
|
|
|
962
1194
|
return this.finishNode(node, isForIn ? 'ForInStatement' : 'ForOfStatement');
|
|
963
1195
|
}
|
|
964
1196
|
|
|
1197
|
+
/**
|
|
1198
|
+
* @type {Parse.Parser['parseFunctionBody']}
|
|
1199
|
+
*/
|
|
1200
|
+
parseFunctionBody(node, isArrowFunction, isMethod, forInit, ...args) {
|
|
1201
|
+
this.#functionBodyDepth++;
|
|
1202
|
+
|
|
1203
|
+
try {
|
|
1204
|
+
return super.parseFunctionBody(node, isArrowFunction, isMethod, forInit, ...args);
|
|
1205
|
+
} finally {
|
|
1206
|
+
this.#functionBodyDepth--;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
965
1210
|
/**
|
|
966
1211
|
* @type {Parse.Parser['checkUnreserved']}
|
|
967
1212
|
*/
|
|
@@ -1916,12 +2161,16 @@ export function TSRXPlugin(config) {
|
|
|
1916
2161
|
|
|
1917
2162
|
if (!inside_tsx.openingElement.name) {
|
|
1918
2163
|
if (this.input.slice(this.pos, this.pos + 2) === '/>') {
|
|
2164
|
+
// Reset exprAllowed so the trailing `/` of `</>` is tokenized
|
|
2165
|
+
// as a slash rather than as the start of a regex literal.
|
|
2166
|
+
this.exprAllowed = false;
|
|
1919
2167
|
return;
|
|
1920
2168
|
}
|
|
1921
2169
|
} else if (this.input.slice(this.pos, this.pos + 4) === '/tsx') {
|
|
1922
2170
|
const after = this.input.charCodeAt(this.pos + 4);
|
|
1923
2171
|
// Make sure it's </tsx> and not </tsx:...>
|
|
1924
2172
|
if (after === 62 /* > */) {
|
|
2173
|
+
this.exprAllowed = false;
|
|
1925
2174
|
return;
|
|
1926
2175
|
}
|
|
1927
2176
|
}
|
|
@@ -1988,6 +2237,7 @@ export function TSRXPlugin(config) {
|
|
|
1988
2237
|
}
|
|
1989
2238
|
|
|
1990
2239
|
if (this.input.slice(this.pos, this.pos + 5) === '/tsx:') {
|
|
2240
|
+
this.exprAllowed = false;
|
|
1991
2241
|
return;
|
|
1992
2242
|
}
|
|
1993
2243
|
|
|
@@ -2188,6 +2438,7 @@ export function TSRXPlugin(config) {
|
|
|
2188
2438
|
if (
|
|
2189
2439
|
context !== 'for' &&
|
|
2190
2440
|
context !== 'if' &&
|
|
2441
|
+
this.#functionBodyDepth === 0 &&
|
|
2191
2442
|
this.context.at(-1) === b_stat &&
|
|
2192
2443
|
this.type === tt.braceL &&
|
|
2193
2444
|
this.context.some((c) => c === tstc.tc_expr)
|
|
@@ -2277,7 +2528,13 @@ export function TSRXPlugin(config) {
|
|
|
2277
2528
|
parseBlock(createNewLexicalScope, node, exitStrict) {
|
|
2278
2529
|
const parent = this.#path.at(-1);
|
|
2279
2530
|
|
|
2280
|
-
|
|
2531
|
+
// Inside a JS function body, parse `{...}` as a regular block statement,
|
|
2532
|
+
// even if the nearest `#path` entry is a Component/Element — we're in a
|
|
2533
|
+
// nested function callable, not in a template.
|
|
2534
|
+
if (
|
|
2535
|
+
this.#functionBodyDepth === 0 &&
|
|
2536
|
+
(parent?.type === 'Component' || parent?.type === 'Element')
|
|
2537
|
+
) {
|
|
2281
2538
|
if (createNewLexicalScope === void 0) createNewLexicalScope = true;
|
|
2282
2539
|
if (node === void 0) node = /** @type {AST.BlockStatement} */ (this.startNode());
|
|
2283
2540
|
|
|
@@ -106,6 +106,23 @@ export function tsx_with_ts_locations() {
|
|
|
106
106
|
context.visit(node.typeAnnotation);
|
|
107
107
|
}
|
|
108
108
|
},
|
|
109
|
+
Identifier: (node, context) => {
|
|
110
|
+
context.write(node.name, node);
|
|
111
|
+
if (node.optional) {
|
|
112
|
+
context.write('?');
|
|
113
|
+
}
|
|
114
|
+
if (node.typeAnnotation) {
|
|
115
|
+
context.visit(node.typeAnnotation);
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
TSNamedTupleMember: (node, context) => {
|
|
119
|
+
context.visit(node.label);
|
|
120
|
+
if (node.optional) {
|
|
121
|
+
context.write('?');
|
|
122
|
+
}
|
|
123
|
+
context.write(': ');
|
|
124
|
+
context.visit(node.elementType);
|
|
125
|
+
},
|
|
109
126
|
};
|
|
110
127
|
for (const type of [
|
|
111
128
|
// JS nodes whose esrap printer emits no location marker, causing
|