@tsrx/core 0.0.18 → 0.0.20
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 +10 -185
- package/package.json +2 -1
- package/src/diagnostics.js +7 -0
- package/src/errors.js +3 -1
- package/src/index.js +1 -0
- package/src/parse/index.js +4 -2
- package/src/plugin.js +157 -112
- package/src/scope.js +2 -2
- package/src/transform/jsx/index.js +146 -72
- package/types/index.d.ts +19 -4
- package/types/jsx-platform.d.ts +53 -0
- package/types/parse.d.ts +4 -2
- package/types/runtime/merge-refs.d.ts +12 -0
package/README.md
CHANGED
|
@@ -35,194 +35,19 @@ const ast = parseModule(source, 'App.tsrx');
|
|
|
35
35
|
The parser produces an ESTree-compatible AST, augmented with the TSRX node types
|
|
36
36
|
listed below. Framework compilers walk this AST to emit their own output.
|
|
37
37
|
|
|
38
|
-
##
|
|
38
|
+
## Language docs
|
|
39
39
|
|
|
40
|
-
TSRX is
|
|
41
|
-
the following productions.
|
|
40
|
+
The TSRX website is the canonical source for language documentation:
|
|
42
41
|
|
|
43
|
-
|
|
42
|
+
- [Getting Started](https://tsrx.dev/getting-started) — install TSRX for React,
|
|
43
|
+
Preact, Solid, Vue, or Ripple and configure editor/AI tooling.
|
|
44
|
+
- [Features](https://tsrx.dev/features) — examples of components, statement
|
|
45
|
+
templates, control flow, scoped styles, server blocks, and lazy destructuring.
|
|
46
|
+
- [Specification](https://tsrx.dev/specification) — the current grammar and
|
|
47
|
+
parser-level semantics.
|
|
44
48
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
that framework compilers can treat it specially.
|
|
48
|
-
|
|
49
|
-
```tsx
|
|
50
|
-
component Button(props: Props) {
|
|
51
|
-
<button>{props.label}</button>
|
|
52
|
-
}
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
- `component` may be used wherever `function` may be used (declaration,
|
|
56
|
-
expression, default export).
|
|
57
|
-
- The body of a `component` may contain JSX-like elements as statements — see §3.
|
|
58
|
-
- `component` is a contextual keyword. Use as an identifier is preserved in
|
|
59
|
-
non-declaration positions.
|
|
60
|
-
|
|
61
|
-
### 2. JSX-as-statements
|
|
62
|
-
|
|
63
|
-
Inside a `component` body, JSX elements are valid _statement_ forms. They describe
|
|
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 `{}`.
|
|
67
|
-
|
|
68
|
-
```tsx
|
|
69
|
-
component Greeting() {
|
|
70
|
-
<h1>"Hello"</h1>
|
|
71
|
-
<p>"Welcome"</p>
|
|
72
|
-
}
|
|
73
|
-
```
|
|
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
|
-
|
|
78
|
-
Elsewhere (outside a `component` body), JSX remains an expression, as in standard
|
|
79
|
-
JSX.
|
|
80
|
-
|
|
81
|
-
### 4. Control-flow statements in `component` bodies
|
|
82
|
-
|
|
83
|
-
Inside a `component` body, the standard JavaScript control-flow keywords `if`,
|
|
84
|
-
`else`, `for`, `switch`, and `try` gain an additional role: their branches may
|
|
85
|
-
contain JSX-as-statements (§2) describing conditionally- or repeatedly-rendered
|
|
86
|
-
output. The keywords retain their usual JavaScript syntax — no new grammar is
|
|
87
|
-
introduced — but framework compilers treat them as _reactive_ boundaries.
|
|
88
|
-
|
|
89
|
-
```tsx
|
|
90
|
-
component List(props: { items: Item[]; showHeader: boolean }) {
|
|
91
|
-
if (props.showHeader) {
|
|
92
|
-
<h1>"Items"</h1>
|
|
93
|
-
} else {
|
|
94
|
-
<h2>"(no header)"</h2>
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
for (const item of props.items) {
|
|
98
|
-
<li>{item.name}</li>
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
switch (props.items.length) {
|
|
102
|
-
case 0:
|
|
103
|
-
<p>"empty"</p>
|
|
104
|
-
break;
|
|
105
|
-
default:
|
|
106
|
-
<p>"has items"</p>
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
try {
|
|
110
|
-
<AsyncThing />
|
|
111
|
-
} catch (e) {
|
|
112
|
-
<pre>{String(e)}</pre>
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
**Early returns.** A bare `return;` (or `return` at the end of a branch) is a
|
|
118
|
-
valid statement inside a `component` body and short-circuits any remaining
|
|
119
|
-
rendering in the current branch. This composes naturally with the control-flow
|
|
120
|
-
forms above:
|
|
121
|
-
|
|
122
|
-
```tsx
|
|
123
|
-
component Page(props: { user: User | null }) {
|
|
124
|
-
if (props.user == null) {
|
|
125
|
-
<LoginPrompt />
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
<Dashboard user={props.user} />
|
|
130
|
-
}
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
Because a `component` body does not produce a value, `return` never carries an
|
|
134
|
-
expression — it only marks a rendering short-circuit.
|
|
135
|
-
|
|
136
|
-
**Nesting inside elements.** Control-flow statements may appear directly as
|
|
137
|
-
children of a JSX element, not only at the top level of the component body. Their
|
|
138
|
-
branches contribute children to the enclosing element in source order:
|
|
139
|
-
|
|
140
|
-
```tsx
|
|
141
|
-
component Menu(props: { items: Item[]; loading: boolean }) {
|
|
142
|
-
<ul>
|
|
143
|
-
if (props.loading) {
|
|
144
|
-
<li>{'loading…'}</li>
|
|
145
|
-
} else {
|
|
146
|
-
for (const item of props.items) {
|
|
147
|
-
<li>
|
|
148
|
-
<a href={item.href}>{item.label}</a>
|
|
149
|
-
if (item.badge) {
|
|
150
|
-
<span class="badge">{item.badge}</span>
|
|
151
|
-
}
|
|
152
|
-
</li>
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
</ul>
|
|
156
|
-
}
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
Any control-flow form that is legal at the component-body level is also legal as a
|
|
160
|
-
child of a JSX element, and may be nested to arbitrary depth.
|
|
161
|
-
|
|
162
|
-
TSRX only describes what is syntactically permitted. The reactive semantics
|
|
163
|
-
(dependency tracking, list reconciliation, error boundaries, suspense) are the
|
|
164
|
-
responsibility of the framework compiler.
|
|
165
|
-
|
|
166
|
-
### 5. JSX escape hatch: `<tsx>...</tsx>`
|
|
167
|
-
|
|
168
|
-
Because JSX inside a `component` body is a _statement_ (§2), the element itself
|
|
169
|
-
has no value. To embed regular _expression_-form JSX — e.g. when a third-party
|
|
170
|
-
library accepts a JSX tree as a value — wrap it in the reserved `<tsx>` element.
|
|
171
|
-
Its children are parsed as standard JSX expressions and the whole form evaluates
|
|
172
|
-
to the JSX expression value (or an array of values if there are multiple
|
|
173
|
-
children).
|
|
174
|
-
|
|
175
|
-
```tsx
|
|
176
|
-
component Page() {
|
|
177
|
-
const header = <tsx><h1>Hello</h1></tsx>;
|
|
178
|
-
renderSomewhereElse(header);
|
|
179
|
-
}
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
`<tsx>` is a reserved tag name in TSRX. It has no runtime representation of its
|
|
183
|
-
own — the framework compiler unwraps it into the underlying JSX expression.
|
|
184
|
-
|
|
185
|
-
### 6. Lazy destructuring: `&[]` and `&{}`
|
|
186
|
-
|
|
187
|
-
Two new destructuring forms prefixed with `&` bind by _reference_ rather than by
|
|
188
|
-
value. Each bound name compiles to a lazy property lookup on the source, so reads
|
|
189
|
-
and writes are deferred to the use-site.
|
|
190
|
-
|
|
191
|
-
```tsx
|
|
192
|
-
let &[count] = source; // array-style lazy destructure
|
|
193
|
-
let &{ name, age } = props; // object-style lazy destructure
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
Semantics are provided by the framework compiler. TSRX only defines the syntax and
|
|
197
|
-
the AST shape (`kind: 'lazy'` binding patterns).
|
|
198
|
-
|
|
199
|
-
### 7. `#server` blocks
|
|
200
|
-
|
|
201
|
-
A `#server { ... }` block marks a lexical region whose contents are intended for
|
|
202
|
-
the server compile target. TSRX parses the block and records its exports;
|
|
203
|
-
framework compilers decide how to emit or strip it per target.
|
|
204
|
-
|
|
205
|
-
```ts
|
|
206
|
-
#server {
|
|
207
|
-
export async function load() { /* ... */ }
|
|
208
|
-
}
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
### 8. `#style` identifier
|
|
212
|
-
|
|
213
|
-
`#style` is a reserved identifier that refers, at compile time, to the set of
|
|
214
|
-
scoped CSS classes declared in the current module. It is legal only in positions
|
|
215
|
-
where the framework compiler expects a class-name value.
|
|
216
|
-
|
|
217
|
-
```tsx
|
|
218
|
-
<div class={#style.card} />
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
### 9. Scoped CSS blocks
|
|
222
|
-
|
|
223
|
-
A `component` may contain a trailing CSS block (delimited by the framework
|
|
224
|
-
compiler's chosen grammar). The block is parsed into a `CSS.StyleSheet` AST node
|
|
225
|
-
and hashed for scoping.
|
|
49
|
+
Keeping the language reference on the website avoids duplicating the specification
|
|
50
|
+
here and keeps package docs focused on the core parser API.
|
|
226
51
|
|
|
227
52
|
## What `@tsrx/core` provides
|
|
228
53
|
|
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.0.
|
|
6
|
+
"version": "0.0.20",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"types": "./types/acorn.d.ts"
|
|
29
29
|
},
|
|
30
30
|
"./runtime/merge-refs": {
|
|
31
|
+
"types": "./types/runtime/merge-refs.d.ts",
|
|
31
32
|
"default": "./src/runtime/merge-refs.js"
|
|
32
33
|
},
|
|
33
34
|
"./test-harness/source-mappings": "./tests/shared/source-mappings.js",
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const DIAGNOSTIC_CODES = {
|
|
2
|
+
JSX_EXPRESSION_VALUE: 'tsrx-jsx-expression-value',
|
|
3
|
+
JSX_RETURN_IN_COMPONENT: 'tsrx-jsx-return-in-component',
|
|
4
|
+
FUNCTION_COMPONENT_SYNTAX: 'tsrx-function-component-syntax',
|
|
5
|
+
UNCLOSED_TAG: 'tsrx-unclosed-tag',
|
|
6
|
+
MISMATCHED_CLOSING_TAG: 'tsrx-mismatched-closing-tag',
|
|
7
|
+
};
|
package/src/errors.js
CHANGED
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
* @param {AST.Node | AST.NodeWithLocation} node
|
|
11
11
|
* @param {CompileError[]} [errors]
|
|
12
12
|
* @param {AST.CommentWithLocation[]} [comments]
|
|
13
|
+
* @param {string} [code]
|
|
13
14
|
* @returns {void}
|
|
14
15
|
*/
|
|
15
|
-
export function error(message, filename, node, errors, comments) {
|
|
16
|
+
export function error(message, filename, node, errors, comments, code) {
|
|
16
17
|
if (errors && comments && is_error_suppressed(node, comments)) {
|
|
17
18
|
return;
|
|
18
19
|
}
|
|
@@ -25,6 +26,7 @@ export function error(message, filename, node, errors, comments) {
|
|
|
25
26
|
|
|
26
27
|
// custom properties
|
|
27
28
|
error.fileName = filename;
|
|
29
|
+
error.code = code;
|
|
28
30
|
error.end = node.end ?? undefined;
|
|
29
31
|
error.loc = !node.loc
|
|
30
32
|
? undefined
|
package/src/index.js
CHANGED
package/src/parse/index.js
CHANGED
|
@@ -201,7 +201,8 @@ export function createParser(...plugins) {
|
|
|
201
201
|
return function parse(source, filename, options) {
|
|
202
202
|
/** @type {AST.CommentWithLocation[]} */
|
|
203
203
|
const comments = [];
|
|
204
|
-
const
|
|
204
|
+
const collect = !!(options?.collect || options?.loose);
|
|
205
|
+
const output_comments = collect ? options?.comments : undefined;
|
|
205
206
|
|
|
206
207
|
const { onComment, add_comments } = get_comment_handlers(source, comments);
|
|
207
208
|
/** @type {AST.Program} */
|
|
@@ -216,7 +217,8 @@ export function createParser(...plugins) {
|
|
|
216
217
|
onComment,
|
|
217
218
|
tsrxOptions: {
|
|
218
219
|
filename,
|
|
219
|
-
|
|
220
|
+
collect,
|
|
221
|
+
errors: collect ? (options?.errors ?? []) : undefined,
|
|
220
222
|
loose: options?.loose || false,
|
|
221
223
|
},
|
|
222
224
|
});
|
package/src/plugin.js
CHANGED
|
@@ -15,6 +15,10 @@ import {
|
|
|
15
15
|
} from './parse/index.js';
|
|
16
16
|
import { regex_newline_characters } from './utils/patterns.js';
|
|
17
17
|
import { error } from './errors.js';
|
|
18
|
+
import { DIAGNOSTIC_CODES } from './diagnostics.js';
|
|
19
|
+
|
|
20
|
+
const JSX_EXPRESSION_VALUE_ERROR =
|
|
21
|
+
'JSX elements cannot be used as expressions. Wrap with `<>...</>` or `<tsx>...</tsx>` or use elements as statements within a component.';
|
|
18
22
|
|
|
19
23
|
/** @type {WeakMap<Record<string, boolean>, Map<string, number>>} */
|
|
20
24
|
const argument_clash_first_positions = new WeakMap();
|
|
@@ -153,6 +157,35 @@ function looks_like_generic_arrow(input, pos) {
|
|
|
153
157
|
return input.charCodeAt(i) === 61 && input.charCodeAt(i + 1) === 62;
|
|
154
158
|
}
|
|
155
159
|
|
|
160
|
+
/**
|
|
161
|
+
* @param {AST.Node | null | undefined} node
|
|
162
|
+
* @returns {boolean}
|
|
163
|
+
*/
|
|
164
|
+
function is_pascal_case_function(node) {
|
|
165
|
+
if (node && 'id' in node && node.id && node.id.type === 'Identifier') {
|
|
166
|
+
return /^[A-Z]/.test(node.id.name);
|
|
167
|
+
}
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* @param {string} input
|
|
173
|
+
* @param {number} pos
|
|
174
|
+
*/
|
|
175
|
+
function previous_word_before(input, pos) {
|
|
176
|
+
let i = pos - 1;
|
|
177
|
+
while (i >= 0) {
|
|
178
|
+
const ch = input.charCodeAt(i);
|
|
179
|
+
if (ch !== 32 && ch !== 9 && ch !== 10 && ch !== 13) break;
|
|
180
|
+
i--;
|
|
181
|
+
}
|
|
182
|
+
const end = i + 1;
|
|
183
|
+
while (i >= 0 && /[$_\p{ID_Continue}]/u.test(input[i])) {
|
|
184
|
+
i--;
|
|
185
|
+
}
|
|
186
|
+
return input.slice(i + 1, end);
|
|
187
|
+
}
|
|
188
|
+
|
|
156
189
|
/**
|
|
157
190
|
* Acorn parser plugin for Ripple syntax extensions.
|
|
158
191
|
* Adds support for: component declarations, &[]/&{} lazy destructuring,
|
|
@@ -178,7 +211,10 @@ export function TSRXPlugin(config) {
|
|
|
178
211
|
#allowTagStartAfterDoubleQuotedText = false;
|
|
179
212
|
#allowDoubleQuotedTextChildAfterBrace = false;
|
|
180
213
|
#commentContextId = 0;
|
|
214
|
+
#collect = false;
|
|
181
215
|
#loose = false;
|
|
216
|
+
/** @type {AST.Node[]} */
|
|
217
|
+
#functionStack = [];
|
|
182
218
|
/** @type {import('../types/index').CompileError[] | undefined} */
|
|
183
219
|
#errors = undefined;
|
|
184
220
|
/** @type {string | null} */
|
|
@@ -192,6 +228,7 @@ export function TSRXPlugin(config) {
|
|
|
192
228
|
constructor(options, input) {
|
|
193
229
|
super(options, input);
|
|
194
230
|
const tsrx_options = options?.tsrxOptions ?? options?.rippleOptions;
|
|
231
|
+
this.#collect = tsrx_options?.collect === true || tsrx_options?.loose === true;
|
|
195
232
|
this.#loose = tsrx_options?.loose === true;
|
|
196
233
|
this.#errors = tsrx_options?.errors;
|
|
197
234
|
this.#filename = tsrx_options?.filename || null;
|
|
@@ -274,8 +311,9 @@ export function TSRXPlugin(config) {
|
|
|
274
311
|
* @param {number} position
|
|
275
312
|
* @param {number} end
|
|
276
313
|
* @param {string} message
|
|
314
|
+
* @param {string} [code]
|
|
277
315
|
*/
|
|
278
|
-
#report_recoverable_error_range(position, end, message) {
|
|
316
|
+
#report_recoverable_error_range(position, end, message, code) {
|
|
279
317
|
const start = Math.max(0, Math.min(position, this.input.length));
|
|
280
318
|
const range_end = Math.max(start, Math.min(end, this.input.length));
|
|
281
319
|
const start_loc = acorn.getLineInfo(this.input, start);
|
|
@@ -292,20 +330,37 @@ export function TSRXPlugin(config) {
|
|
|
292
330
|
end: end_loc,
|
|
293
331
|
},
|
|
294
332
|
}),
|
|
295
|
-
this.#
|
|
333
|
+
this.#collect ? this.#errors : undefined,
|
|
334
|
+
undefined,
|
|
335
|
+
code,
|
|
296
336
|
);
|
|
297
337
|
}
|
|
298
338
|
|
|
299
339
|
/**
|
|
300
340
|
* @param {number} position
|
|
301
341
|
* @param {string} message
|
|
342
|
+
* @param {string} [code]
|
|
343
|
+
*/
|
|
344
|
+
#report_recoverable_error(position, message, code) {
|
|
345
|
+
this.#report_recoverable_error_range(position, position + 1, message, code);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* @param {number} position
|
|
350
|
+
* @param {string} message
|
|
351
|
+
* @param {string} [code]
|
|
302
352
|
*/
|
|
303
|
-
#
|
|
304
|
-
this.#
|
|
353
|
+
#report_broken_markup_error(position, message, code = DIAGNOSTIC_CODES.UNCLOSED_TAG) {
|
|
354
|
+
if (this.#loose) return;
|
|
355
|
+
if (this.#collect) {
|
|
356
|
+
this.#report_recoverable_error(position, message, code);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
this.raise(position, message);
|
|
305
360
|
}
|
|
306
361
|
|
|
307
362
|
/**
|
|
308
|
-
*
|
|
363
|
+
* When collecting, keep parsing after duplicate declaration diagnostics so
|
|
309
364
|
* editor tooling can continue producing AST and mappings.
|
|
310
365
|
* @param {number} position
|
|
311
366
|
* @param {string | { message?: string }} message
|
|
@@ -339,7 +394,7 @@ export function TSRXPlugin(config) {
|
|
|
339
394
|
*/
|
|
340
395
|
reportReservedArrowTypeParam(node) {
|
|
341
396
|
// Allow <T>() => {} syntax without requiring trailing comma
|
|
342
|
-
if (this.#
|
|
397
|
+
if (this.#collect && node.params.length === 1 && node.extra?.trailingComma === undefined) {
|
|
343
398
|
error(
|
|
344
399
|
'This syntax is reserved in files with the .mts or .cts extension. Add a trailing comma, as in `<T,>() => ...`.',
|
|
345
400
|
this.#filename,
|
|
@@ -350,7 +405,7 @@ export function TSRXPlugin(config) {
|
|
|
350
405
|
}
|
|
351
406
|
|
|
352
407
|
/**
|
|
353
|
-
* Override to allow `readonly` type modifier on any type
|
|
408
|
+
* Override to allow `readonly` type modifier on any type when collecting.
|
|
354
409
|
* By default, @sveltejs/acorn-typescript throws an error for `readonly { ... }`
|
|
355
410
|
* because TypeScript only permits `readonly` on array and tuple types.
|
|
356
411
|
* Suppress the error in the strict mode as ts is compiled away.
|
|
@@ -363,7 +418,7 @@ export function TSRXPlugin(config) {
|
|
|
363
418
|
return;
|
|
364
419
|
}
|
|
365
420
|
|
|
366
|
-
if (this.#
|
|
421
|
+
if (this.#collect) {
|
|
367
422
|
error(
|
|
368
423
|
"'readonly' type modifier is only permitted on array and tuple literal types.",
|
|
369
424
|
this.#filename,
|
|
@@ -607,21 +662,17 @@ export function TSRXPlugin(config) {
|
|
|
607
662
|
}
|
|
608
663
|
|
|
609
664
|
/**
|
|
610
|
-
*
|
|
611
|
-
*
|
|
612
|
-
* otherwise tokenize `<` as `jsxTagStart`
|
|
613
|
-
*
|
|
614
|
-
*
|
|
615
|
-
*
|
|
665
|
+
* `<T,>(x: T) => x` and `<T>(x: T): T => x` should parse as generic
|
|
666
|
+
* arrow functions, not JSX elements. acorn-typescript's `readToken`
|
|
667
|
+
* can otherwise tokenize `<` as `jsxTagStart` when expression parsing
|
|
668
|
+
* allows JSX, bypassing our `getTokenFromCode` override. We intercept
|
|
669
|
+
* only when the source from `<` actually looks like a generic arrow
|
|
670
|
+
* expression, so JSX like `<div>` keeps parsing normally.
|
|
616
671
|
*
|
|
617
672
|
* @type {Parse.Parser['readToken']}
|
|
618
673
|
*/
|
|
619
674
|
readToken(code) {
|
|
620
|
-
if (
|
|
621
|
-
code === 60 &&
|
|
622
|
-
this.#path.findLast((n) => n.type === 'Component') &&
|
|
623
|
-
looks_like_generic_arrow(this.input, this.pos)
|
|
624
|
-
) {
|
|
675
|
+
if (code === 60 && looks_like_generic_arrow(this.input, this.pos)) {
|
|
625
676
|
++this.pos;
|
|
626
677
|
return this.finishToken(tt.relational, '<');
|
|
627
678
|
}
|
|
@@ -854,15 +905,15 @@ export function TSRXPlugin(config) {
|
|
|
854
905
|
}
|
|
855
906
|
|
|
856
907
|
/**
|
|
857
|
-
* Acorn reports only the second duplicate function parameter.
|
|
858
|
-
*
|
|
908
|
+
* Acorn reports only the second duplicate function parameter. When collecting,
|
|
909
|
+
* report the first one too so editor diagnostics can underline both
|
|
859
910
|
* binding sites. Keep strict mode on Acorn's normal fatal path.
|
|
860
911
|
*
|
|
861
912
|
* @type {Parse.Parser['checkLValSimple']}
|
|
862
913
|
*/
|
|
863
914
|
checkLValSimple(expr, bindingType = BINDING_TYPES.BIND_NONE, checkClashes) {
|
|
864
915
|
if (
|
|
865
|
-
this.#
|
|
916
|
+
this.#collect &&
|
|
866
917
|
expr.type === 'Identifier' &&
|
|
867
918
|
bindingType !== BINDING_TYPES.BIND_NONE &&
|
|
868
919
|
checkClashes
|
|
@@ -1298,10 +1349,12 @@ export function TSRXPlugin(config) {
|
|
|
1298
1349
|
*/
|
|
1299
1350
|
parseFunctionBody(node, isArrowFunction, isMethod, forInit, ...args) {
|
|
1300
1351
|
this.#functionBodyDepth++;
|
|
1352
|
+
this.#functionStack.push(node);
|
|
1301
1353
|
|
|
1302
1354
|
try {
|
|
1303
1355
|
return super.parseFunctionBody(node, isArrowFunction, isMethod, forInit, ...args);
|
|
1304
1356
|
} finally {
|
|
1357
|
+
this.#functionStack.pop();
|
|
1305
1358
|
this.#functionBodyDepth--;
|
|
1306
1359
|
}
|
|
1307
1360
|
}
|
|
@@ -1849,10 +1902,16 @@ export function TSRXPlugin(config) {
|
|
|
1849
1902
|
);
|
|
1850
1903
|
}
|
|
1851
1904
|
|
|
1852
|
-
this.
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1905
|
+
const code = this.#functionStack.findLast(is_pascal_case_function)
|
|
1906
|
+
? DIAGNOSTIC_CODES.FUNCTION_COMPONENT_SYNTAX
|
|
1907
|
+
: this.#path.findLast((node) => node.type === 'Component') &&
|
|
1908
|
+
this.#functionStack.length === 0 &&
|
|
1909
|
+
previous_word_before(this.input, this.start) === 'return'
|
|
1910
|
+
? DIAGNOSTIC_CODES.JSX_RETURN_IN_COMPONENT
|
|
1911
|
+
: DIAGNOSTIC_CODES.JSX_EXPRESSION_VALUE;
|
|
1912
|
+
|
|
1913
|
+
this.#report_recoverable_error(this.start, JSX_EXPRESSION_VALUE_ERROR, code);
|
|
1914
|
+
return super.jsx_parseElement();
|
|
1856
1915
|
}
|
|
1857
1916
|
|
|
1858
1917
|
/**
|
|
@@ -2051,12 +2110,10 @@ export function TSRXPlugin(config) {
|
|
|
2051
2110
|
this.#path.pop();
|
|
2052
2111
|
} else {
|
|
2053
2112
|
// No closing tag
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
);
|
|
2059
|
-
}
|
|
2113
|
+
this.#report_broken_markup_error(
|
|
2114
|
+
open.end,
|
|
2115
|
+
"Unclosed tag '<script>'. Expected '</script>' before end of component.",
|
|
2116
|
+
);
|
|
2060
2117
|
/** @type {AST.Element} */ (element).unclosed = true;
|
|
2061
2118
|
this.#path.pop();
|
|
2062
2119
|
}
|
|
@@ -2113,12 +2170,10 @@ export function TSRXPlugin(config) {
|
|
|
2113
2170
|
this.exprAllowed = false;
|
|
2114
2171
|
this.#path.pop();
|
|
2115
2172
|
} else {
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
);
|
|
2121
|
-
}
|
|
2173
|
+
this.#report_broken_markup_error(
|
|
2174
|
+
open.end,
|
|
2175
|
+
"Unclosed tag '<style>'. Expected '</style>' before end of component.",
|
|
2176
|
+
);
|
|
2122
2177
|
/** @type {AST.Element} */ (element).unclosed = true;
|
|
2123
2178
|
this.#path.pop();
|
|
2124
2179
|
}
|
|
@@ -2203,20 +2258,17 @@ export function TSRXPlugin(config) {
|
|
|
2203
2258
|
}
|
|
2204
2259
|
} else if (this.#path[this.#path.length - 1] === element) {
|
|
2205
2260
|
// Check if this element was properly closed
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
this.
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
element.end = element.openingElement.end;
|
|
2218
|
-
this.#path.pop();
|
|
2219
|
-
}
|
|
2261
|
+
const tagName = this.getElementName(element.id);
|
|
2262
|
+
this.#report_broken_markup_error(
|
|
2263
|
+
this.start,
|
|
2264
|
+
`Unclosed tag '<${tagName}>'. Expected '</${tagName}>' before end of component.`,
|
|
2265
|
+
);
|
|
2266
|
+
element.unclosed = true;
|
|
2267
|
+
element.loc.end = {
|
|
2268
|
+
.../** @type {AST.SourceLocation} */ (element.openingElement.loc).end,
|
|
2269
|
+
};
|
|
2270
|
+
element.end = element.openingElement.end;
|
|
2271
|
+
this.#path.pop();
|
|
2220
2272
|
}
|
|
2221
2273
|
}
|
|
2222
2274
|
|
|
@@ -2267,18 +2319,15 @@ export function TSRXPlugin(config) {
|
|
|
2267
2319
|
|
|
2268
2320
|
while (true) {
|
|
2269
2321
|
if (this.type === tt.eof || this.pos >= this.input.length || this.type === tt.braceR) {
|
|
2270
|
-
|
|
2271
|
-
this.
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
}
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
};
|
|
2280
|
-
inside_tsx.end = inside_tsx.openingElement.end;
|
|
2281
|
-
}
|
|
2322
|
+
this.#report_broken_markup_error(
|
|
2323
|
+
this.start,
|
|
2324
|
+
`Unclosed tag '<tsx>'. Expected '</tsx>' before end of component.`,
|
|
2325
|
+
);
|
|
2326
|
+
inside_tsx.unclosed = true;
|
|
2327
|
+
/** @type {AST.NodeWithLocation} */ (inside_tsx).loc.end = {
|
|
2328
|
+
.../** @type {AST.SourceLocation} */ (inside_tsx.openingElement.loc).end,
|
|
2329
|
+
};
|
|
2330
|
+
inside_tsx.end = inside_tsx.openingElement.end;
|
|
2282
2331
|
return;
|
|
2283
2332
|
}
|
|
2284
2333
|
|
|
@@ -2344,18 +2393,15 @@ export function TSRXPlugin(config) {
|
|
|
2344
2393
|
|
|
2345
2394
|
while (true) {
|
|
2346
2395
|
if (this.type === tt.eof || this.pos >= this.input.length || this.type === tt.braceR) {
|
|
2347
|
-
|
|
2348
|
-
this.
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
}
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
};
|
|
2357
|
-
inside_tsx_compat.end = inside_tsx_compat.openingElement.end;
|
|
2358
|
-
}
|
|
2396
|
+
this.#report_broken_markup_error(
|
|
2397
|
+
this.start,
|
|
2398
|
+
`Unclosed tag '<tsx:${inside_tsx_compat.kind}>'. Expected '</tsx:${inside_tsx_compat.kind}>' before end of component.`,
|
|
2399
|
+
);
|
|
2400
|
+
inside_tsx_compat.unclosed = true;
|
|
2401
|
+
/** @type {AST.NodeWithLocation} */ (inside_tsx_compat).loc.end = {
|
|
2402
|
+
.../** @type {AST.SourceLocation} */ (inside_tsx_compat.openingElement.loc).end,
|
|
2403
|
+
};
|
|
2404
|
+
inside_tsx_compat.end = inside_tsx_compat.openingElement.end;
|
|
2359
2405
|
return;
|
|
2360
2406
|
}
|
|
2361
2407
|
|
|
@@ -2480,46 +2526,45 @@ export function TSRXPlugin(config) {
|
|
|
2480
2526
|
}
|
|
2481
2527
|
|
|
2482
2528
|
if (openingTagName !== closingTagName) {
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2529
|
+
// this will throw if not collecting errors
|
|
2530
|
+
this.#report_broken_markup_error(
|
|
2531
|
+
closingElement.start,
|
|
2532
|
+
`Expected closing tag to match opening tag. Expected '</${openingTagName}>' but found '</${closingTagName}>'`,
|
|
2533
|
+
DIAGNOSTIC_CODES.MISMATCHED_CLOSING_TAG,
|
|
2534
|
+
);
|
|
2535
|
+
// Loop through all unclosed elements on the stack
|
|
2536
|
+
while (this.#path.length > 0) {
|
|
2537
|
+
const elem = this.#path[this.#path.length - 1];
|
|
2492
2538
|
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2539
|
+
// Stop at non-Element boundaries (Component, etc.)
|
|
2540
|
+
if (elem.type !== 'Element' && elem.type !== 'Tsx' && elem.type !== 'TsxCompat') {
|
|
2541
|
+
break;
|
|
2542
|
+
}
|
|
2497
2543
|
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2544
|
+
const elemName =
|
|
2545
|
+
elem.type === 'TsxCompat'
|
|
2546
|
+
? 'tsx:' + elem.kind
|
|
2547
|
+
: elem.type === 'Tsx'
|
|
2548
|
+
? elem.openingElement.name
|
|
2549
|
+
? 'tsx'
|
|
2550
|
+
: null
|
|
2551
|
+
: elem.id
|
|
2552
|
+
? this.getElementName(elem.id)
|
|
2553
|
+
: null;
|
|
2554
|
+
|
|
2555
|
+
// Found matching opening tag
|
|
2556
|
+
if (elemName === closingTagName) {
|
|
2557
|
+
break;
|
|
2558
|
+
}
|
|
2513
2559
|
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2560
|
+
// Mark as unclosed and adjust location
|
|
2561
|
+
elem.unclosed = true;
|
|
2562
|
+
/** @type {AST.NodeWithLocation} */ (elem).loc.end = {
|
|
2563
|
+
.../** @type {AST.SourceLocation} */ (elem.openingElement.loc).end,
|
|
2564
|
+
};
|
|
2565
|
+
elem.end = elem.openingElement.end;
|
|
2520
2566
|
|
|
2521
|
-
|
|
2522
|
-
}
|
|
2567
|
+
this.#path.pop(); // Remove from stack
|
|
2523
2568
|
}
|
|
2524
2569
|
}
|
|
2525
2570
|
|
package/src/scope.js
CHANGED
|
@@ -335,7 +335,7 @@ export class Scope {
|
|
|
335
335
|
`Cannot declare a variable named "${node.name}" as identifiers starting with "${IDENTIFIER_OBFUSCATION_PREFIX}" are reserved`,
|
|
336
336
|
this.#error_options.filename,
|
|
337
337
|
node,
|
|
338
|
-
this.#error_options.
|
|
338
|
+
this.#error_options.collect ? this.#error_options.errors : undefined,
|
|
339
339
|
this.#error_options.comments,
|
|
340
340
|
);
|
|
341
341
|
}
|
|
@@ -345,7 +345,7 @@ export class Scope {
|
|
|
345
345
|
`'${node.name}' has already been declared in the current scope`,
|
|
346
346
|
this.#error_options.filename,
|
|
347
347
|
node,
|
|
348
|
-
this.#error_options.
|
|
348
|
+
this.#error_options.collect ? this.#error_options.errors : undefined,
|
|
349
349
|
this.#error_options.comments,
|
|
350
350
|
);
|
|
351
351
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/** @import * as AST from 'estree' */
|
|
2
2
|
/** @import * as ESTreeJSX from 'estree-jsx' */
|
|
3
|
-
/** @import { JsxPlatform, JsxTransformOptions, JsxTransformResult } from '@tsrx/core/types' */
|
|
3
|
+
/** @import { JsxPlatform, JsxTransformContext, JsxTransformOptions, JsxTransformResult } from '@tsrx/core/types' */
|
|
4
4
|
|
|
5
5
|
import { walk } from 'zimmerframe';
|
|
6
6
|
import { print } from 'esrap';
|
|
7
|
+
import { error } from '../../errors.js';
|
|
7
8
|
import {
|
|
8
9
|
ensure_function_metadata,
|
|
9
10
|
in_jsx_child_context,
|
|
@@ -14,7 +15,6 @@ import {
|
|
|
14
15
|
clone_expression_node,
|
|
15
16
|
clone_identifier,
|
|
16
17
|
clone_jsx_name,
|
|
17
|
-
create_compile_error,
|
|
18
18
|
create_generated_identifier,
|
|
19
19
|
create_null_literal,
|
|
20
20
|
flatten_switch_consequent,
|
|
@@ -26,7 +26,11 @@ import {
|
|
|
26
26
|
to_text_expression,
|
|
27
27
|
} from './ast-builders.js';
|
|
28
28
|
import { render_stylesheets as renderStylesheets } from '../stylesheet.js';
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
set_location as setLocation,
|
|
31
|
+
jsx_attribute as build_jsx_attribute,
|
|
32
|
+
jsx_id as build_jsx_id,
|
|
33
|
+
} from '../../utils/builders.js';
|
|
30
34
|
import {
|
|
31
35
|
apply_lazy_transforms,
|
|
32
36
|
collect_lazy_bindings_from_component,
|
|
@@ -45,18 +49,11 @@ import {
|
|
|
45
49
|
import { is_hoist_safe_jsx_node } from '../jsx-hoist.js';
|
|
46
50
|
|
|
47
51
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* needs_merge_refs: boolean,
|
|
54
|
-
* helper_state: { base_name: string, next_id: number, helpers: any[], statics: any[] } | null,
|
|
55
|
-
* available_bindings: Map<string, AST.Identifier>,
|
|
56
|
-
* lazy_next_id: number,
|
|
57
|
-
* current_css_hash: string | null,
|
|
58
|
-
* inside_element_child?: boolean,
|
|
59
|
-
* }} TransformContext
|
|
52
|
+
* Local alias for the shared `JsxTransformContext`. Kept as a typedef so the
|
|
53
|
+
* rest of this file's `@param {TransformContext}` annotations don't all have
|
|
54
|
+
* to spell out the import.
|
|
55
|
+
*
|
|
56
|
+
* @typedef {JsxTransformContext} TransformContext
|
|
60
57
|
*/
|
|
61
58
|
|
|
62
59
|
/**
|
|
@@ -95,11 +92,12 @@ export function createJsxTransform(platform) {
|
|
|
95
92
|
const module_uses_server_directive = should_scan_use_server_directive
|
|
96
93
|
? has_use_server_directive(ast)
|
|
97
94
|
: true;
|
|
95
|
+
const collect = !!(options?.collect || options?.loose);
|
|
98
96
|
/** @type {any[]} */
|
|
99
97
|
const stylesheets = [];
|
|
100
98
|
|
|
101
99
|
/** @type {TransformContext} */
|
|
102
|
-
const transform_context =
|
|
100
|
+
const transform_context = {
|
|
103
101
|
platform,
|
|
104
102
|
local_statement_component_index: 0,
|
|
105
103
|
needs_error_boundary: false,
|
|
@@ -109,17 +107,26 @@ export function createJsxTransform(platform) {
|
|
|
109
107
|
available_bindings: new Map(),
|
|
110
108
|
lazy_next_id: 0,
|
|
111
109
|
current_css_hash: null,
|
|
110
|
+
filename: filename ?? null,
|
|
111
|
+
collect,
|
|
112
|
+
errors: collect ? options?.errors : undefined,
|
|
113
|
+
comments: options?.comments,
|
|
112
114
|
// Platforms can seed their own tracking state (e.g. solid's
|
|
113
115
|
// needs_show / needs_for flags) via `hooks.initialState`.
|
|
114
116
|
...(platform.hooks?.initialState?.() ?? {}),
|
|
115
|
-
}
|
|
117
|
+
};
|
|
116
118
|
|
|
117
119
|
preallocate_lazy_ids(/** @type {any} */ (ast), transform_context);
|
|
118
120
|
|
|
119
121
|
walk(/** @type {any} */ (ast), transform_context, {
|
|
120
122
|
ReturnStatement(node, { next, path }) {
|
|
121
123
|
if (get_component_from_path(path)) {
|
|
122
|
-
validate_component_return_statement(
|
|
124
|
+
validate_component_return_statement(
|
|
125
|
+
node,
|
|
126
|
+
filename,
|
|
127
|
+
transform_context.errors,
|
|
128
|
+
transform_context.comments,
|
|
129
|
+
);
|
|
123
130
|
}
|
|
124
131
|
|
|
125
132
|
return next();
|
|
@@ -143,9 +150,12 @@ export function createJsxTransform(platform) {
|
|
|
143
150
|
source,
|
|
144
151
|
);
|
|
145
152
|
} else if (!module_uses_server_directive) {
|
|
146
|
-
|
|
147
|
-
await_expression,
|
|
153
|
+
error(
|
|
148
154
|
`${platform.name} components can only use \`await\` when the module has a top-level "use server" directive.`,
|
|
155
|
+
state.filename,
|
|
156
|
+
await_expression,
|
|
157
|
+
state.errors,
|
|
158
|
+
state.comments,
|
|
149
159
|
);
|
|
150
160
|
}
|
|
151
161
|
|
|
@@ -212,10 +222,10 @@ export function createJsxTransform(platform) {
|
|
|
212
222
|
return /** @type {any} */ (tsx_node_to_jsx_expression(inner, in_jsx_child_context(path)));
|
|
213
223
|
},
|
|
214
224
|
|
|
215
|
-
TsxCompat(node, { next, path }) {
|
|
225
|
+
TsxCompat(node, { next, path, state }) {
|
|
216
226
|
const inner = /** @type {any} */ (next() ?? node);
|
|
217
227
|
return /** @type {any} */ (
|
|
218
|
-
tsx_compat_node_to_jsx_expression(inner,
|
|
228
|
+
tsx_compat_node_to_jsx_expression(inner, state, in_jsx_child_context(path))
|
|
219
229
|
);
|
|
220
230
|
},
|
|
221
231
|
|
|
@@ -1485,7 +1495,22 @@ const TEMPLATE_FRAGMENT_ERROR =
|
|
|
1485
1495
|
function to_jsx_element(node, transform_context, raw_children = node.children || []) {
|
|
1486
1496
|
if (node.type === 'JSXElement') return node;
|
|
1487
1497
|
if (!node.id) {
|
|
1488
|
-
|
|
1498
|
+
error(
|
|
1499
|
+
TEMPLATE_FRAGMENT_ERROR,
|
|
1500
|
+
transform_context.filename,
|
|
1501
|
+
node,
|
|
1502
|
+
transform_context.errors,
|
|
1503
|
+
transform_context.comments,
|
|
1504
|
+
);
|
|
1505
|
+
return set_loc(
|
|
1506
|
+
/** @type {any} */ ({
|
|
1507
|
+
type: 'JSXFragment',
|
|
1508
|
+
openingFragment: { type: 'JSXOpeningFragment' },
|
|
1509
|
+
closingFragment: { type: 'JSXClosingFragment' },
|
|
1510
|
+
children: [],
|
|
1511
|
+
}),
|
|
1512
|
+
node,
|
|
1513
|
+
);
|
|
1489
1514
|
}
|
|
1490
1515
|
if (is_dynamic_element_id(node.id)) {
|
|
1491
1516
|
return dynamic_element_to_jsx_child(node, transform_context);
|
|
@@ -2111,7 +2136,7 @@ function to_jsx_child(node, transform_context) {
|
|
|
2111
2136
|
// JSXExpressionContainer wrapper for bare `{expr}` children.
|
|
2112
2137
|
return tsx_node_to_jsx_expression(node, true);
|
|
2113
2138
|
case 'TsxCompat':
|
|
2114
|
-
return tsx_compat_node_to_jsx_expression(node, transform_context
|
|
2139
|
+
return tsx_compat_node_to_jsx_expression(node, transform_context, true);
|
|
2115
2140
|
case 'Element':
|
|
2116
2141
|
return to_jsx_element(node, transform_context);
|
|
2117
2142
|
case 'Text':
|
|
@@ -2282,9 +2307,12 @@ function find_key_expression_in_body(body_nodes) {
|
|
|
2282
2307
|
*/
|
|
2283
2308
|
function for_of_statement_to_jsx_child(node, transform_context) {
|
|
2284
2309
|
if (node.await) {
|
|
2285
|
-
|
|
2286
|
-
node,
|
|
2310
|
+
error(
|
|
2287
2311
|
`${transform_context.platform.name} TSRX does not support \`for await...of\` in component templates.`,
|
|
2312
|
+
transform_context.filename,
|
|
2313
|
+
node,
|
|
2314
|
+
transform_context.errors,
|
|
2315
|
+
transform_context.comments,
|
|
2288
2316
|
);
|
|
2289
2317
|
}
|
|
2290
2318
|
|
|
@@ -2460,23 +2488,33 @@ function try_statement_to_jsx_child(node, transform_context) {
|
|
|
2460
2488
|
const finalizer = node.finalizer;
|
|
2461
2489
|
|
|
2462
2490
|
if (finalizer) {
|
|
2463
|
-
|
|
2464
|
-
finalizer,
|
|
2491
|
+
error(
|
|
2465
2492
|
`${transform_context.platform.name} TSRX does not support JavaScript \`try/finally\` in component templates. \`finally\` is not part of TSRX control flow; move the try/finally into a function if you need cleanup logic.`,
|
|
2493
|
+
transform_context.filename,
|
|
2494
|
+
finalizer,
|
|
2495
|
+
transform_context.errors,
|
|
2496
|
+
transform_context.comments,
|
|
2466
2497
|
);
|
|
2467
2498
|
}
|
|
2468
2499
|
|
|
2469
2500
|
if (!pending && !handler) {
|
|
2470
|
-
|
|
2471
|
-
node,
|
|
2501
|
+
error(
|
|
2472
2502
|
'Component try statements must have a `pending` or `catch` block.',
|
|
2503
|
+
transform_context.filename,
|
|
2504
|
+
node,
|
|
2505
|
+
transform_context.errors,
|
|
2506
|
+
transform_context.comments,
|
|
2473
2507
|
);
|
|
2508
|
+
return to_jsx_expression_container(create_null_literal());
|
|
2474
2509
|
}
|
|
2475
2510
|
|
|
2476
2511
|
if (pending && transform_context.platform.validation.unsupportedTryPendingMessage) {
|
|
2477
|
-
|
|
2478
|
-
pending,
|
|
2512
|
+
error(
|
|
2479
2513
|
transform_context.platform.validation.unsupportedTryPendingMessage,
|
|
2514
|
+
transform_context.filename,
|
|
2515
|
+
pending,
|
|
2516
|
+
transform_context.errors,
|
|
2517
|
+
transform_context.comments,
|
|
2480
2518
|
);
|
|
2481
2519
|
}
|
|
2482
2520
|
|
|
@@ -2484,16 +2522,22 @@ function try_statement_to_jsx_child(node, transform_context) {
|
|
|
2484
2522
|
if (pending) {
|
|
2485
2523
|
const try_body = node.block.body || [];
|
|
2486
2524
|
if (!try_body.some(is_jsx_child)) {
|
|
2487
|
-
|
|
2488
|
-
node.block,
|
|
2525
|
+
error(
|
|
2489
2526
|
'Component try statements must contain a template in their main body. Move the try statement into a function if it does not render anything.',
|
|
2527
|
+
transform_context.filename,
|
|
2528
|
+
node.block,
|
|
2529
|
+
transform_context.errors,
|
|
2530
|
+
transform_context.comments,
|
|
2490
2531
|
);
|
|
2491
2532
|
}
|
|
2492
2533
|
const pending_body = pending.body || [];
|
|
2493
2534
|
if (!pending_body.some(is_jsx_child)) {
|
|
2494
|
-
|
|
2495
|
-
pending,
|
|
2535
|
+
error(
|
|
2496
2536
|
'Component try statements must contain a template in their "pending" body. Rendering a pending fallback is required to have a template.',
|
|
2537
|
+
transform_context.filename,
|
|
2538
|
+
pending,
|
|
2539
|
+
transform_context.errors,
|
|
2540
|
+
transform_context.comments,
|
|
2497
2541
|
);
|
|
2498
2542
|
}
|
|
2499
2543
|
}
|
|
@@ -2963,7 +3007,7 @@ function to_jsx_expression_container(expression, source_node = expression) {
|
|
|
2963
3007
|
* @returns {any[]}
|
|
2964
3008
|
*/
|
|
2965
3009
|
function transform_element_attributes_dispatch(attrs, transform_context, element) {
|
|
2966
|
-
validate_at_most_one_ref_attribute(attrs);
|
|
3010
|
+
validate_at_most_one_ref_attribute(attrs, transform_context);
|
|
2967
3011
|
const preprocess = transform_context.platform.hooks?.preprocessElementAttributes;
|
|
2968
3012
|
if (preprocess) {
|
|
2969
3013
|
attrs = preprocess(attrs, transform_context, element);
|
|
@@ -2987,9 +3031,11 @@ function transform_element_attributes_dispatch(attrs, transform_context, element
|
|
|
2987
3031
|
* the original `JSXAttribute`/`JSXIdentifier` shape, so we accept both.
|
|
2988
3032
|
*
|
|
2989
3033
|
* @param {any[]} raw_attrs
|
|
3034
|
+
* @param {TransformContext} [transform_context]
|
|
2990
3035
|
*/
|
|
2991
|
-
export function validate_at_most_one_ref_attribute(raw_attrs) {
|
|
2992
|
-
|
|
3036
|
+
export function validate_at_most_one_ref_attribute(raw_attrs, transform_context) {
|
|
3037
|
+
/** @type {any[]} */
|
|
3038
|
+
const refs = [];
|
|
2993
3039
|
for (const attr of raw_attrs) {
|
|
2994
3040
|
if (!attr) continue;
|
|
2995
3041
|
const is_ref_attr =
|
|
@@ -3002,14 +3048,25 @@ export function validate_at_most_one_ref_attribute(raw_attrs) {
|
|
|
3002
3048
|
attr.name.type === 'JSXIdentifier' &&
|
|
3003
3049
|
attr.name.name === 'ref');
|
|
3004
3050
|
if (!is_ref_attr) continue;
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3051
|
+
refs.push(attr.name);
|
|
3052
|
+
}
|
|
3053
|
+
if (refs.length < 2) {
|
|
3054
|
+
return;
|
|
3055
|
+
}
|
|
3056
|
+
for (let i = 0; i < refs.length; i++) {
|
|
3057
|
+
const node = refs[i];
|
|
3058
|
+
if (!transform_context?.collect && i === 0) {
|
|
3059
|
+
// when not collecting, only throw on the second duplicate
|
|
3060
|
+
continue;
|
|
3011
3061
|
}
|
|
3012
|
-
|
|
3062
|
+
error(
|
|
3063
|
+
'Element has multiple `ref={...}` attributes; an element may have at most one. ' +
|
|
3064
|
+
"Use Ripple's `{ref expr}` keyword form to combine multiple refs on one element.",
|
|
3065
|
+
transform_context?.filename ?? null,
|
|
3066
|
+
node,
|
|
3067
|
+
transform_context?.errors,
|
|
3068
|
+
transform_context?.comments,
|
|
3069
|
+
);
|
|
3013
3070
|
}
|
|
3014
3071
|
}
|
|
3015
3072
|
|
|
@@ -3040,18 +3097,34 @@ export function merge_duplicate_refs(jsx_attrs, transform_context) {
|
|
|
3040
3097
|
if (!strategy) return jsx_attrs;
|
|
3041
3098
|
|
|
3042
3099
|
let count = 0;
|
|
3100
|
+
let tsx_form_count = 0;
|
|
3043
3101
|
for (const attr of jsx_attrs) {
|
|
3044
|
-
if (is_jsx_ref_attribute(attr))
|
|
3102
|
+
if (!is_jsx_ref_attribute(attr)) continue;
|
|
3103
|
+
count += 1;
|
|
3104
|
+
if (!attr.metadata?.from_ref_keyword) tsx_form_count += 1;
|
|
3045
3105
|
}
|
|
3046
3106
|
if (count <= 1) return jsx_attrs;
|
|
3107
|
+
// Two or more genuine `ref={...}` (TSX-form) attributes are already a
|
|
3108
|
+
// validator-flagged compile error and TypeScript flags them as duplicate
|
|
3109
|
+
// JSX props. Leave them in place so the user gets all three signals
|
|
3110
|
+
// instead of silently composing them into `__mergeRefs(...)`.
|
|
3111
|
+
if (tsx_form_count >= 2) return jsx_attrs;
|
|
3047
3112
|
|
|
3048
3113
|
/** @type {any[]} */
|
|
3049
3114
|
const ref_exprs = [];
|
|
3050
3115
|
/** @type {any[]} */
|
|
3051
3116
|
const result = [];
|
|
3117
|
+
/** @type {any} */
|
|
3118
|
+
let source_attr = null;
|
|
3052
3119
|
for (const attr of jsx_attrs) {
|
|
3053
3120
|
if (is_jsx_ref_attribute(attr)) {
|
|
3054
3121
|
ref_exprs.push(attr.value.expression);
|
|
3122
|
+
// Inherit loc from the (at most one) `ref={expr}`-form attribute so
|
|
3123
|
+
// the kept `ref` keyword in the generated `ref={__mergeRefs(...)}`
|
|
3124
|
+
// retains a source mapping back to its original `ref=` keyword.
|
|
3125
|
+
if (!source_attr && !attr.metadata?.from_ref_keyword) {
|
|
3126
|
+
source_attr = attr;
|
|
3127
|
+
}
|
|
3055
3128
|
} else {
|
|
3056
3129
|
result.push(attr);
|
|
3057
3130
|
}
|
|
@@ -3080,23 +3153,23 @@ export function merge_duplicate_refs(jsx_attrs, transform_context) {
|
|
|
3080
3153
|
transform_context.needs_merge_refs = true;
|
|
3081
3154
|
}
|
|
3082
3155
|
|
|
3083
|
-
//
|
|
3084
|
-
//
|
|
3085
|
-
// the
|
|
3086
|
-
//
|
|
3087
|
-
|
|
3156
|
+
// Inherit start/end/loc from the (at most one) `ref={expr}`-form attribute
|
|
3157
|
+
// so segments.js emits a normal source-to-generated mapping for the
|
|
3158
|
+
// merged attribute and its name. Without this the kept `ref` keyword in
|
|
3159
|
+
// `ref={__mergeRefs(...)}` has no source mapping back to the user's `ref=`
|
|
3160
|
+
// keyword.
|
|
3161
|
+
const merged_name = build_jsx_id('ref', source_attr?.name);
|
|
3162
|
+
const merged_attr = build_jsx_attribute(
|
|
3163
|
+
merged_name,
|
|
3088
3164
|
/** @type {any} */ ({
|
|
3089
|
-
type: '
|
|
3090
|
-
|
|
3091
|
-
value: {
|
|
3092
|
-
type: 'JSXExpressionContainer',
|
|
3093
|
-
expression: merged_value,
|
|
3094
|
-
metadata: { path: [] },
|
|
3095
|
-
},
|
|
3096
|
-
shorthand: false,
|
|
3165
|
+
type: 'JSXExpressionContainer',
|
|
3166
|
+
expression: merged_value,
|
|
3097
3167
|
metadata: { path: [] },
|
|
3098
3168
|
}),
|
|
3169
|
+
false,
|
|
3170
|
+
source_attr,
|
|
3099
3171
|
);
|
|
3172
|
+
result.push(merged_attr);
|
|
3100
3173
|
|
|
3101
3174
|
return result;
|
|
3102
3175
|
}
|
|
@@ -3150,13 +3223,16 @@ export function to_jsx_attribute(attr, transform_context) {
|
|
|
3150
3223
|
// so the source-to-generated mapping is imprecise — but pointing
|
|
3151
3224
|
// editors at the `{ref expr}` span is still useful for hover/jump,
|
|
3152
3225
|
// matching how shorthand `{name}` → `name={name}` carries loc.
|
|
3226
|
+
// `from_ref_keyword` lets `merge_duplicate_refs` tell this form apart
|
|
3227
|
+
// from genuine `ref={...}` attributes without inferring it from
|
|
3228
|
+
// whether `name.loc` happens to be present.
|
|
3153
3229
|
return set_loc(
|
|
3154
3230
|
/** @type {any} */ ({
|
|
3155
3231
|
type: 'JSXAttribute',
|
|
3156
3232
|
name: { type: 'JSXIdentifier', name: 'ref', metadata: { path: [] } },
|
|
3157
3233
|
value: to_jsx_expression_container(attr.argument),
|
|
3158
3234
|
shorthand: false,
|
|
3159
|
-
metadata: { path: [] },
|
|
3235
|
+
metadata: { path: [], from_ref_keyword: true },
|
|
3160
3236
|
}),
|
|
3161
3237
|
attr,
|
|
3162
3238
|
);
|
|
@@ -3189,13 +3265,7 @@ export function to_jsx_attribute(attr, transform_context) {
|
|
|
3189
3265
|
}
|
|
3190
3266
|
}
|
|
3191
3267
|
|
|
3192
|
-
const jsx_attribute =
|
|
3193
|
-
type: 'JSXAttribute',
|
|
3194
|
-
name,
|
|
3195
|
-
value: value || null,
|
|
3196
|
-
shorthand: false,
|
|
3197
|
-
metadata: { path: [] },
|
|
3198
|
-
});
|
|
3268
|
+
const jsx_attribute = build_jsx_attribute(name, value || null, attr.shorthand === true);
|
|
3199
3269
|
|
|
3200
3270
|
if (value_has_unmappable_jsx_loc(value)) {
|
|
3201
3271
|
/** @type {any} */ (jsx_attribute.metadata).has_unmappable_value = true;
|
|
@@ -3360,16 +3430,20 @@ function build_return_expression(render_nodes) {
|
|
|
3360
3430
|
|
|
3361
3431
|
/**
|
|
3362
3432
|
* @param {any} node
|
|
3363
|
-
* @param {
|
|
3433
|
+
* @param {TransformContext} transform_context
|
|
3364
3434
|
* @param {boolean} [in_jsx_child]
|
|
3365
3435
|
* @returns {any}
|
|
3366
3436
|
*/
|
|
3367
|
-
function tsx_compat_node_to_jsx_expression(node,
|
|
3437
|
+
function tsx_compat_node_to_jsx_expression(node, transform_context, in_jsx_child = false) {
|
|
3438
|
+
const platform = transform_context.platform;
|
|
3368
3439
|
if (!platform.jsx.acceptedTsxKinds.includes(node.kind)) {
|
|
3369
3440
|
const accepted = platform.jsx.acceptedTsxKinds.map((k) => `<tsx:${k}>`).join(', ');
|
|
3370
|
-
|
|
3371
|
-
node,
|
|
3441
|
+
error(
|
|
3372
3442
|
`${platform.name} TSRX does not support <tsx:${node.kind}> blocks. Use <tsx> or one of: ${accepted}.`,
|
|
3443
|
+
transform_context.filename,
|
|
3444
|
+
node,
|
|
3445
|
+
transform_context.errors,
|
|
3446
|
+
transform_context.comments,
|
|
3373
3447
|
);
|
|
3374
3448
|
}
|
|
3375
3449
|
|
package/types/index.d.ts
CHANGED
|
@@ -8,19 +8,28 @@ import type { RequireAllOrNone } from '../src/helpers.js';
|
|
|
8
8
|
import type {
|
|
9
9
|
JsxPlatform,
|
|
10
10
|
JsxPlatformHooks,
|
|
11
|
+
JsxTransformContext,
|
|
11
12
|
JsxTransformOptions,
|
|
12
13
|
JsxTransformResult,
|
|
13
14
|
componentToFunctionDeclaration,
|
|
14
15
|
createJsxTransform,
|
|
15
16
|
} from './jsx-platform';
|
|
16
17
|
|
|
17
|
-
export type {
|
|
18
|
+
export type {
|
|
19
|
+
Parse,
|
|
20
|
+
JsxPlatform,
|
|
21
|
+
JsxPlatformHooks,
|
|
22
|
+
JsxTransformContext,
|
|
23
|
+
JsxTransformOptions,
|
|
24
|
+
JsxTransformResult,
|
|
25
|
+
};
|
|
18
26
|
export { createJsxTransform, componentToFunctionDeclaration };
|
|
19
27
|
|
|
20
28
|
/**
|
|
21
29
|
* Compile error interface
|
|
22
30
|
*/
|
|
23
31
|
export interface CompileError extends Error {
|
|
32
|
+
code: string | undefined;
|
|
24
33
|
pos: number | undefined;
|
|
25
34
|
raisedAt: number | undefined;
|
|
26
35
|
end: number | undefined;
|
|
@@ -42,6 +51,11 @@ export interface CompileOptions {
|
|
|
42
51
|
* When true, non-fatal errors are collected on the result's `errors`
|
|
43
52
|
* array instead of being thrown. Defaults to false (strict mode: throws).
|
|
44
53
|
*/
|
|
54
|
+
collect?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Enables editor-oriented parser recovery such as incomplete markup.
|
|
57
|
+
* Also collects non-fatal errors as `collect`.
|
|
58
|
+
*/
|
|
45
59
|
loose?: boolean;
|
|
46
60
|
}
|
|
47
61
|
|
|
@@ -1141,6 +1155,7 @@ export interface ParseError {
|
|
|
1141
1155
|
* Parse options
|
|
1142
1156
|
*/
|
|
1143
1157
|
export interface ParseOptions {
|
|
1158
|
+
collect?: boolean;
|
|
1144
1159
|
loose?: boolean;
|
|
1145
1160
|
errors?: CompileError[];
|
|
1146
1161
|
comments?: AST.CommentWithLocation[];
|
|
@@ -1270,7 +1285,7 @@ export interface ScopeConstructorInterface {
|
|
|
1270
1285
|
parent: ScopeInterface | null;
|
|
1271
1286
|
porous: boolean;
|
|
1272
1287
|
error_options: {
|
|
1273
|
-
|
|
1288
|
+
collect: boolean;
|
|
1274
1289
|
errors: CompileError[];
|
|
1275
1290
|
filename: string;
|
|
1276
1291
|
comments?: AST.CommentWithLocation[];
|
|
@@ -1363,7 +1378,7 @@ export interface AnalysisState extends BaseState {
|
|
|
1363
1378
|
};
|
|
1364
1379
|
elements?: AST.Element[];
|
|
1365
1380
|
function_depth?: number;
|
|
1366
|
-
|
|
1381
|
+
collect?: boolean;
|
|
1367
1382
|
configured_compat_kinds?: Set<string>;
|
|
1368
1383
|
metadata: BaseStateMetaData & {
|
|
1369
1384
|
styleClasses?: StyleClasses;
|
|
@@ -1574,7 +1589,7 @@ export interface CompileResult {
|
|
|
1574
1589
|
css: string;
|
|
1575
1590
|
/**
|
|
1576
1591
|
* Non-fatal errors collected during compilation. Populated only when the
|
|
1577
|
-
* caller passes `loose: true`; empty otherwise.
|
|
1592
|
+
* caller passes `collect: true` or `loose: true`; empty otherwise.
|
|
1578
1593
|
*/
|
|
1579
1594
|
errors: CompileError[];
|
|
1580
1595
|
}
|
package/types/jsx-platform.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type * as AST from 'estree';
|
|
2
2
|
import type { RawSourceMap } from 'source-map';
|
|
3
|
+
import type { CompileError } from './index';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Result returned by a JSX platform transform (React, Preact, Solid).
|
|
@@ -16,6 +17,38 @@ export interface JsxTransformResult {
|
|
|
16
17
|
css: { code: string; hash: string } | null;
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Shared base for the per-call transform context that the JSX factory passes
|
|
22
|
+
* into every visitor and helper. Platform-specific transforms (e.g. Solid)
|
|
23
|
+
* extend this with their own `needs_*` flags via `hooks.initialState`; helpers
|
|
24
|
+
* defined in `@tsrx/core` only ever rely on these base fields.
|
|
25
|
+
*/
|
|
26
|
+
export interface JsxTransformContext {
|
|
27
|
+
platform: JsxPlatform;
|
|
28
|
+
local_statement_component_index: number;
|
|
29
|
+
needs_error_boundary: boolean;
|
|
30
|
+
needs_suspense: boolean;
|
|
31
|
+
needs_merge_refs: boolean;
|
|
32
|
+
helper_state: {
|
|
33
|
+
base_name: string;
|
|
34
|
+
next_id: number;
|
|
35
|
+
helpers: any[];
|
|
36
|
+
statics: any[];
|
|
37
|
+
} | null;
|
|
38
|
+
available_bindings: Map<string, AST.Identifier>;
|
|
39
|
+
lazy_next_id: number;
|
|
40
|
+
current_css_hash: string | null;
|
|
41
|
+
inside_element_child?: boolean;
|
|
42
|
+
/** Source filename for diagnostics; null when the caller did not supply one. */
|
|
43
|
+
filename: string | null;
|
|
44
|
+
/** True when recoverable errors should be collected onto `errors` instead of thrown. */
|
|
45
|
+
collect: boolean;
|
|
46
|
+
/** Collected non-fatal errors. Undefined when `collect` is false. */
|
|
47
|
+
errors: CompileError[] | undefined;
|
|
48
|
+
/** Module-level comments used to honor `@tsrx-ignore` / `@tsrx-expect-error`. */
|
|
49
|
+
comments: AST.CommentWithLocation[] | undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
19
52
|
/**
|
|
20
53
|
* Optional per-call compile options passed to a created JSX transform.
|
|
21
54
|
*/
|
|
@@ -26,6 +59,26 @@ export interface JsxTransformOptions {
|
|
|
26
59
|
* host pick `preact/compat` vs. another compat entry point.
|
|
27
60
|
*/
|
|
28
61
|
suspenseSource?: string;
|
|
62
|
+
/**
|
|
63
|
+
* When true, recoverable transform errors are pushed onto `errors` instead
|
|
64
|
+
* of thrown so editor tooling can surface them as diagnostics. Errors that
|
|
65
|
+
* leave the transform in an unrecoverable state are still thrown.
|
|
66
|
+
*/
|
|
67
|
+
collect?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Don't collect allowable errors such as unclosed tags
|
|
70
|
+
*/
|
|
71
|
+
loose?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Collected non-fatal errors. The transform appends to this array when
|
|
74
|
+
* `collect` or `loose` is true; callers read it after the transform returns.
|
|
75
|
+
*/
|
|
76
|
+
errors?: CompileError[];
|
|
77
|
+
/**
|
|
78
|
+
* Module-level comments used to suppress diagnostics via `@tsrx-ignore` /
|
|
79
|
+
* `@tsrx-expect-error` line comments.
|
|
80
|
+
*/
|
|
81
|
+
comments?: AST.CommentWithLocation[];
|
|
29
82
|
}
|
|
30
83
|
|
|
31
84
|
/**
|
package/types/parse.d.ts
CHANGED
|
@@ -183,13 +183,15 @@ export namespace Parse {
|
|
|
183
183
|
|
|
184
184
|
export interface Options extends Omit<acorn.Options, 'onComment' | 'ecmaVersion'> {
|
|
185
185
|
tsrxOptions?: {
|
|
186
|
+
collect: boolean;
|
|
186
187
|
loose: boolean;
|
|
187
|
-
errors: CoreCompiler.CompileError[];
|
|
188
|
+
errors: CoreCompiler.CompileError[] | undefined;
|
|
188
189
|
filename: string | undefined;
|
|
189
190
|
};
|
|
190
191
|
rippleOptions?: {
|
|
192
|
+
collect: boolean;
|
|
191
193
|
loose: boolean;
|
|
192
|
-
errors: CoreCompiler.CompileError[];
|
|
194
|
+
errors: CoreCompiler.CompileError[] | undefined;
|
|
193
195
|
filename: string | undefined;
|
|
194
196
|
};
|
|
195
197
|
// The type has "latest" but it's converted to 1e8 at runtime
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type MergeableRefCallback<T> = (node: T | null) => void | (() => void);
|
|
2
|
+
export type MergeableRefObject<T> = { current: T | null };
|
|
3
|
+
export type MergeableVueRef<T> = { value: T | null };
|
|
4
|
+
|
|
5
|
+
export type MergeableRef<T> =
|
|
6
|
+
| MergeableRefCallback<T>
|
|
7
|
+
| MergeableRefObject<T>
|
|
8
|
+
| MergeableVueRef<T>
|
|
9
|
+
| null
|
|
10
|
+
| undefined;
|
|
11
|
+
|
|
12
|
+
export function mergeRefs<T = any>(...refs: Array<MergeableRef<T>>): (node: T | null) => () => void;
|