@tsrx/core 0.0.13 → 0.0.15
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/README.md +13 -8
- package/package.json +1 -1
- package/src/analyze/validation.js +47 -0
- package/src/index.js +11 -1
- package/src/plugin.js +319 -24
- 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 +131 -20
- package/src/utils/ast.js +61 -0
- package/types/index.d.ts +17 -0
- package/types/parse.d.ts +2 -0
package/README.md
CHANGED
|
@@ -61,15 +61,20 @@ component Button(props: Props) {
|
|
|
61
61
|
### 2. JSX-as-statements
|
|
62
62
|
|
|
63
63
|
Inside a `component` body, JSX elements are valid _statement_ forms. They describe
|
|
64
|
-
rendered output and are not expressions — they have no value.
|
|
64
|
+
rendered output and are not expressions — they have no value. Static text may be
|
|
65
|
+
written as a direct double-quoted child; dynamic values and other JavaScript
|
|
66
|
+
expressions stay inside `{}`.
|
|
65
67
|
|
|
66
68
|
```tsx
|
|
67
69
|
component Greeting() {
|
|
68
|
-
<h1>
|
|
69
|
-
<p>
|
|
70
|
+
<h1>"Hello"</h1>
|
|
71
|
+
<p>"Welcome"</p>
|
|
70
72
|
}
|
|
71
73
|
```
|
|
72
74
|
|
|
75
|
+
Only double quotes have direct-child text meaning. Single-quoted strings and
|
|
76
|
+
template literals remain JavaScript expressions and must be written inside `{}`.
|
|
77
|
+
|
|
73
78
|
Elsewhere (outside a `component` body), JSX remains an expression, as in standard
|
|
74
79
|
JSX.
|
|
75
80
|
|
|
@@ -84,9 +89,9 @@ introduced — but framework compilers treat them as _reactive_ boundaries.
|
|
|
84
89
|
```tsx
|
|
85
90
|
component List(props: { items: Item[]; showHeader: boolean }) {
|
|
86
91
|
if (props.showHeader) {
|
|
87
|
-
<h1>
|
|
92
|
+
<h1>"Items"</h1>
|
|
88
93
|
} else {
|
|
89
|
-
<h2>
|
|
94
|
+
<h2>"(no header)"</h2>
|
|
90
95
|
}
|
|
91
96
|
|
|
92
97
|
for (const item of props.items) {
|
|
@@ -95,10 +100,10 @@ component List(props: { items: Item[]; showHeader: boolean }) {
|
|
|
95
100
|
|
|
96
101
|
switch (props.items.length) {
|
|
97
102
|
case 0:
|
|
98
|
-
<p>
|
|
103
|
+
<p>"empty"</p>
|
|
99
104
|
break;
|
|
100
105
|
default:
|
|
101
|
-
<p>
|
|
106
|
+
<p>"has items"</p>
|
|
102
107
|
}
|
|
103
108
|
|
|
104
109
|
try {
|
|
@@ -169,7 +174,7 @@ children).
|
|
|
169
174
|
|
|
170
175
|
```tsx
|
|
171
176
|
component Page() {
|
|
172
|
-
const header = <tsx><h1>
|
|
177
|
+
const header = <tsx><h1>Hello</h1></tsx>;
|
|
173
178
|
renderSomewhereElse(header);
|
|
174
179
|
}
|
|
175
180
|
```
|
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,
|
|
@@ -38,12 +175,14 @@ export function TSRXPlugin(config) {
|
|
|
38
175
|
class TSRXParser extends Parser {
|
|
39
176
|
/** @type {AST.Node[]} */
|
|
40
177
|
#path = [];
|
|
178
|
+
#allowTagStartAfterDoubleQuotedText = false;
|
|
41
179
|
#commentContextId = 0;
|
|
42
180
|
#loose = false;
|
|
43
181
|
/** @type {import('../types/index').CompileError[] | undefined} */
|
|
44
182
|
#errors = undefined;
|
|
45
183
|
/** @type {string | null} */
|
|
46
184
|
#filename = null;
|
|
185
|
+
#functionBodyDepth = 0;
|
|
47
186
|
|
|
48
187
|
/**
|
|
49
188
|
* @param {Parse.Options} options
|
|
@@ -59,20 +198,21 @@ export function TSRXPlugin(config) {
|
|
|
59
198
|
|
|
60
199
|
/**
|
|
61
200
|
* @param {number} position
|
|
201
|
+
* @param {number} end
|
|
62
202
|
* @param {string} message
|
|
63
203
|
*/
|
|
64
|
-
#
|
|
204
|
+
#report_recoverable_error_range(position, end, message) {
|
|
65
205
|
const start = Math.max(0, Math.min(position, this.input.length));
|
|
66
|
-
const
|
|
206
|
+
const range_end = Math.max(start, Math.min(end, this.input.length));
|
|
67
207
|
const start_loc = acorn.getLineInfo(this.input, start);
|
|
68
|
-
const end_loc = acorn.getLineInfo(this.input,
|
|
208
|
+
const end_loc = acorn.getLineInfo(this.input, range_end);
|
|
69
209
|
|
|
70
210
|
error(
|
|
71
211
|
message,
|
|
72
212
|
this.#filename,
|
|
73
213
|
/** @type {AST.NodeWithLocation} */ ({
|
|
74
214
|
start,
|
|
75
|
-
end,
|
|
215
|
+
end: range_end,
|
|
76
216
|
loc: {
|
|
77
217
|
start: start_loc,
|
|
78
218
|
end: end_loc,
|
|
@@ -82,6 +222,14 @@ export function TSRXPlugin(config) {
|
|
|
82
222
|
);
|
|
83
223
|
}
|
|
84
224
|
|
|
225
|
+
/**
|
|
226
|
+
* @param {number} position
|
|
227
|
+
* @param {string} message
|
|
228
|
+
*/
|
|
229
|
+
#report_recoverable_error(position, message) {
|
|
230
|
+
this.#report_recoverable_error_range(position, position + 1, message);
|
|
231
|
+
}
|
|
232
|
+
|
|
85
233
|
/**
|
|
86
234
|
* In loose mode, keep parsing after duplicate declaration diagnostics so
|
|
87
235
|
* editor tooling can continue producing AST and mappings.
|
|
@@ -96,7 +244,10 @@ export function TSRXPlugin(config) {
|
|
|
96
244
|
? message.message
|
|
97
245
|
: String(message);
|
|
98
246
|
|
|
99
|
-
if (
|
|
247
|
+
if (
|
|
248
|
+
error_message.includes('has already been declared') ||
|
|
249
|
+
error_message === 'Argument name clash'
|
|
250
|
+
) {
|
|
100
251
|
this.#report_recoverable_error(position, error_message);
|
|
101
252
|
return;
|
|
102
253
|
}
|
|
@@ -381,11 +532,37 @@ export function TSRXPlugin(config) {
|
|
|
381
532
|
return null;
|
|
382
533
|
}
|
|
383
534
|
|
|
535
|
+
/**
|
|
536
|
+
* Inside a component, `<T,>(x: T) => x` should parse as a generic arrow
|
|
537
|
+
* function, not a JSX element. acorn-typescript's `readToken` would
|
|
538
|
+
* otherwise tokenize `<` as `jsxTagStart` (when `exprAllowed` or the
|
|
539
|
+
* context is `tc_expr`), bypassing our `getTokenFromCode` override. We
|
|
540
|
+
* intercept here, but only when the source from `<` actually looks like
|
|
541
|
+
* a generic arrow expression — so JSX like `<div>` keeps parsing normally.
|
|
542
|
+
*
|
|
543
|
+
* @type {Parse.Parser['readToken']}
|
|
544
|
+
*/
|
|
545
|
+
readToken(code) {
|
|
546
|
+
if (
|
|
547
|
+
code === 60 &&
|
|
548
|
+
this.#path.findLast((n) => n.type === 'Component') &&
|
|
549
|
+
looks_like_generic_arrow(this.input, this.pos)
|
|
550
|
+
) {
|
|
551
|
+
++this.pos;
|
|
552
|
+
return this.finishToken(tt.relational, '<');
|
|
553
|
+
}
|
|
554
|
+
return super.readToken(code);
|
|
555
|
+
}
|
|
556
|
+
|
|
384
557
|
/**
|
|
385
558
|
* Get token from character code - handles Ripple-specific tokens
|
|
386
559
|
* @type {Parse.Parser['getTokenFromCode']}
|
|
387
560
|
*/
|
|
388
561
|
getTokenFromCode(code) {
|
|
562
|
+
if (code !== 60) {
|
|
563
|
+
this.#allowTagStartAfterDoubleQuotedText = false;
|
|
564
|
+
}
|
|
565
|
+
|
|
389
566
|
if (code === 60) {
|
|
390
567
|
// < character
|
|
391
568
|
const inComponent = this.#path.findLast((n) => n.type === 'Component');
|
|
@@ -462,29 +639,21 @@ export function TSRXPlugin(config) {
|
|
|
462
639
|
// Inside component template bodies, allow adjacent tags without requiring
|
|
463
640
|
// a newline/indentation before the next '<'. This is important for inputs
|
|
464
641
|
// like `<div />` and `</div><style>...</style>` which Prettier formats.
|
|
465
|
-
if (
|
|
642
|
+
if (
|
|
643
|
+
(prevNonWhitespaceChar === 34 /* '"' */ &&
|
|
644
|
+
this.#allowTagStartAfterDoubleQuotedText) ||
|
|
645
|
+
prevNonWhitespaceChar === 123 /* '{' */ ||
|
|
646
|
+
prevNonWhitespaceChar === 62 /* '>' */
|
|
647
|
+
) {
|
|
466
648
|
if (!isWhitespaceAfterLt) {
|
|
649
|
+
this.#allowTagStartAfterDoubleQuotedText = false;
|
|
467
650
|
++this.pos;
|
|
468
651
|
return this.finishToken(tstt.jsxTagStart);
|
|
469
652
|
}
|
|
470
653
|
}
|
|
471
654
|
|
|
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
|
-
}
|
|
655
|
+
// `<` inside a nested function body is intercepted earlier in
|
|
656
|
+
// `readToken` so it never reaches this path.
|
|
488
657
|
|
|
489
658
|
// Check if everything before this position on the current line is whitespace
|
|
490
659
|
let lineStart = this.pos - 1;
|
|
@@ -554,6 +723,7 @@ export function TSRXPlugin(config) {
|
|
|
554
723
|
}
|
|
555
724
|
}
|
|
556
725
|
}
|
|
726
|
+
this.#allowTagStartAfterDoubleQuotedText = false;
|
|
557
727
|
return super.getTokenFromCode(code);
|
|
558
728
|
}
|
|
559
729
|
|
|
@@ -599,6 +769,67 @@ export function TSRXPlugin(config) {
|
|
|
599
769
|
return super.parseBindingAtom();
|
|
600
770
|
}
|
|
601
771
|
|
|
772
|
+
/**
|
|
773
|
+
* Acorn reports only the second duplicate function parameter. In loose
|
|
774
|
+
* mode, report the first one too so editor diagnostics can underline both
|
|
775
|
+
* binding sites. Keep strict mode on Acorn's normal fatal path.
|
|
776
|
+
*
|
|
777
|
+
* @type {Parse.Parser['checkLValSimple']}
|
|
778
|
+
*/
|
|
779
|
+
checkLValSimple(expr, bindingType = BINDING_TYPES.BIND_NONE, checkClashes) {
|
|
780
|
+
if (
|
|
781
|
+
this.#loose &&
|
|
782
|
+
expr.type === 'Identifier' &&
|
|
783
|
+
bindingType !== BINDING_TYPES.BIND_NONE &&
|
|
784
|
+
checkClashes
|
|
785
|
+
) {
|
|
786
|
+
const first_positions = get_argument_clash_first_positions(checkClashes);
|
|
787
|
+
const reported_names = get_argument_clash_reported_names(checkClashes);
|
|
788
|
+
const first_position = first_positions.get(expr.name);
|
|
789
|
+
|
|
790
|
+
if (Object.prototype.hasOwnProperty.call(checkClashes, expr.name)) {
|
|
791
|
+
if (first_position != null && !reported_names.has(expr.name)) {
|
|
792
|
+
this.#report_recoverable_error_range(
|
|
793
|
+
first_position,
|
|
794
|
+
first_position + expr.name.length,
|
|
795
|
+
'Argument name clash',
|
|
796
|
+
);
|
|
797
|
+
reported_names.add(expr.name);
|
|
798
|
+
}
|
|
799
|
+
const start = /** @type {number} */ (expr.start);
|
|
800
|
+
this.#report_recoverable_error_range(
|
|
801
|
+
start,
|
|
802
|
+
/** @type {number} */ (expr.end ?? start + expr.name.length),
|
|
803
|
+
'Argument name clash',
|
|
804
|
+
);
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const result = super.checkLValSimple(expr, bindingType, checkClashes);
|
|
809
|
+
first_positions.set(expr.name, /** @type {number} */ (expr.start));
|
|
810
|
+
return result;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
return super.checkLValSimple(expr, bindingType, checkClashes);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Components do not use Acorn's normal function-body parser, but they
|
|
818
|
+
* should still report duplicate parameter names like functions do. Keep
|
|
819
|
+
* this validation on `BIND_OUTSIDE` so params are checked without being
|
|
820
|
+
* declared in the component template scope, preserving existing shadowing
|
|
821
|
+
* behavior.
|
|
822
|
+
*
|
|
823
|
+
* @param {AST.Pattern[]} params
|
|
824
|
+
*/
|
|
825
|
+
checkComponentParams(params) {
|
|
826
|
+
/** @type {Record<string, boolean>} */
|
|
827
|
+
const name_hash = Object.create(null);
|
|
828
|
+
for (const param of params || []) {
|
|
829
|
+
this.checkLValInnerPattern(param, BINDING_TYPES.BIND_OUTSIDE, name_hash);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
602
833
|
/**
|
|
603
834
|
* Parse expression atom - handles RippleArray and RippleObject literals
|
|
604
835
|
* @type {Parse.Parser['parseExprAtom']}
|
|
@@ -741,11 +972,24 @@ export function TSRXPlugin(config) {
|
|
|
741
972
|
}
|
|
742
973
|
|
|
743
974
|
this.parseFunctionParams(node);
|
|
975
|
+
this.checkComponentParams(node.params);
|
|
976
|
+
|
|
977
|
+
// Reset before `eat(braceL)` so the lookahead `next()` it triggers reads
|
|
978
|
+
// the component body's first token as if we'd entered fresh — no
|
|
979
|
+
// surrounding function body should affect our parseStatement/parseBlock
|
|
980
|
+
// branching while inside the template.
|
|
981
|
+
const parent_function_body_depth = this.#functionBodyDepth;
|
|
982
|
+
this.#functionBodyDepth = 0;
|
|
983
|
+
|
|
744
984
|
this.eat(tt.braceL);
|
|
745
985
|
node.body = [];
|
|
746
986
|
this.#path.push(node);
|
|
747
987
|
|
|
748
|
-
|
|
988
|
+
try {
|
|
989
|
+
this.parseTemplateBody(node.body);
|
|
990
|
+
} finally {
|
|
991
|
+
this.#functionBodyDepth = parent_function_body_depth;
|
|
992
|
+
}
|
|
749
993
|
this.#path.pop();
|
|
750
994
|
this.exitScope();
|
|
751
995
|
|
|
@@ -962,6 +1206,19 @@ export function TSRXPlugin(config) {
|
|
|
962
1206
|
return this.finishNode(node, isForIn ? 'ForInStatement' : 'ForOfStatement');
|
|
963
1207
|
}
|
|
964
1208
|
|
|
1209
|
+
/**
|
|
1210
|
+
* @type {Parse.Parser['parseFunctionBody']}
|
|
1211
|
+
*/
|
|
1212
|
+
parseFunctionBody(node, isArrowFunction, isMethod, forInit, ...args) {
|
|
1213
|
+
this.#functionBodyDepth++;
|
|
1214
|
+
|
|
1215
|
+
try {
|
|
1216
|
+
return super.parseFunctionBody(node, isArrowFunction, isMethod, forInit, ...args);
|
|
1217
|
+
} finally {
|
|
1218
|
+
this.#functionBodyDepth--;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
965
1222
|
/**
|
|
966
1223
|
* @type {Parse.Parser['checkUnreserved']}
|
|
967
1224
|
*/
|
|
@@ -1055,6 +1312,30 @@ export function TSRXPlugin(config) {
|
|
|
1055
1312
|
);
|
|
1056
1313
|
}
|
|
1057
1314
|
|
|
1315
|
+
/**
|
|
1316
|
+
* @returns {AST.TextNode}
|
|
1317
|
+
*/
|
|
1318
|
+
parseDoubleQuotedTextChild() {
|
|
1319
|
+
const node = /** @type {AST.TextNode} */ (this.startNode());
|
|
1320
|
+
const expression = /** @type {AST.Literal} */ (this.startNode());
|
|
1321
|
+
const raw = this.input.slice(this.start, this.end);
|
|
1322
|
+
const end = this.end;
|
|
1323
|
+
const endLoc = this.endLoc;
|
|
1324
|
+
|
|
1325
|
+
expression.value = this.value;
|
|
1326
|
+
expression.raw = raw;
|
|
1327
|
+
node.expression = this.finishNodeAt(expression, 'Literal', end, endLoc);
|
|
1328
|
+
|
|
1329
|
+
this.#allowTagStartAfterDoubleQuotedText = true;
|
|
1330
|
+
try {
|
|
1331
|
+
this.next();
|
|
1332
|
+
} finally {
|
|
1333
|
+
this.#allowTagStartAfterDoubleQuotedText = false;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
return this.finishNodeAt(node, 'Text', end, endLoc);
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1058
1339
|
/**
|
|
1059
1340
|
* @type {Parse.Parser['jsx_parseAttribute']}
|
|
1060
1341
|
*/
|
|
@@ -1916,12 +2197,16 @@ export function TSRXPlugin(config) {
|
|
|
1916
2197
|
|
|
1917
2198
|
if (!inside_tsx.openingElement.name) {
|
|
1918
2199
|
if (this.input.slice(this.pos, this.pos + 2) === '/>') {
|
|
2200
|
+
// Reset exprAllowed so the trailing `/` of `</>` is tokenized
|
|
2201
|
+
// as a slash rather than as the start of a regex literal.
|
|
2202
|
+
this.exprAllowed = false;
|
|
1919
2203
|
return;
|
|
1920
2204
|
}
|
|
1921
2205
|
} else if (this.input.slice(this.pos, this.pos + 4) === '/tsx') {
|
|
1922
2206
|
const after = this.input.charCodeAt(this.pos + 4);
|
|
1923
2207
|
// Make sure it's </tsx> and not </tsx:...>
|
|
1924
2208
|
if (after === 62 /* > */) {
|
|
2209
|
+
this.exprAllowed = false;
|
|
1925
2210
|
return;
|
|
1926
2211
|
}
|
|
1927
2212
|
}
|
|
@@ -1988,6 +2273,7 @@ export function TSRXPlugin(config) {
|
|
|
1988
2273
|
}
|
|
1989
2274
|
|
|
1990
2275
|
if (this.input.slice(this.pos, this.pos + 5) === '/tsx:') {
|
|
2276
|
+
this.exprAllowed = false;
|
|
1991
2277
|
return;
|
|
1992
2278
|
}
|
|
1993
2279
|
|
|
@@ -2043,6 +2329,8 @@ export function TSRXPlugin(config) {
|
|
|
2043
2329
|
delete node.text;
|
|
2044
2330
|
}
|
|
2045
2331
|
body.push(node);
|
|
2332
|
+
} else if (this.type === tt.string && this.input.charCodeAt(this.start) === 34) {
|
|
2333
|
+
body.push(this.parseDoubleQuotedTextChild());
|
|
2046
2334
|
} else if (this.type === tt.braceR) {
|
|
2047
2335
|
// Leaving a component/template body. We may still be in TSX/JSX tokenization
|
|
2048
2336
|
// context (e.g. after parsing markup), but the closing `}` is a JS token.
|
|
@@ -2188,6 +2476,7 @@ export function TSRXPlugin(config) {
|
|
|
2188
2476
|
if (
|
|
2189
2477
|
context !== 'for' &&
|
|
2190
2478
|
context !== 'if' &&
|
|
2479
|
+
this.#functionBodyDepth === 0 &&
|
|
2191
2480
|
this.context.at(-1) === b_stat &&
|
|
2192
2481
|
this.type === tt.braceL &&
|
|
2193
2482
|
this.context.some((c) => c === tstc.tc_expr)
|
|
@@ -2277,7 +2566,13 @@ export function TSRXPlugin(config) {
|
|
|
2277
2566
|
parseBlock(createNewLexicalScope, node, exitStrict) {
|
|
2278
2567
|
const parent = this.#path.at(-1);
|
|
2279
2568
|
|
|
2280
|
-
|
|
2569
|
+
// Inside a JS function body, parse `{...}` as a regular block statement,
|
|
2570
|
+
// even if the nearest `#path` entry is a Component/Element — we're in a
|
|
2571
|
+
// nested function callable, not in a template.
|
|
2572
|
+
if (
|
|
2573
|
+
this.#functionBodyDepth === 0 &&
|
|
2574
|
+
(parent?.type === 'Component' || parent?.type === 'Element')
|
|
2575
|
+
) {
|
|
2281
2576
|
if (createNewLexicalScope === void 0) createNewLexicalScope = true;
|
|
2282
2577
|
if (node === void 0) node = /** @type {AST.BlockStatement} */ (this.startNode());
|
|
2283
2578
|
|
|
@@ -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
|