@tsrx/core 0.0.8 → 0.0.10
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 +8 -5
- package/src/index.js +24 -1
- package/src/parse/style.js +2 -2
- package/src/plugin.js +2 -2
- package/src/source-map-utils.js +4 -2
- package/src/transform/jsx/ast-builders.js +321 -0
- package/src/transform/jsx/helpers.js +131 -0
- package/src/transform/jsx/index.js +2486 -0
- package/src/transform/scoping.js +120 -8
- package/src/transform/segments.js +27 -45
- package/src/utils/hashing.js +30 -2
- package/src/utils.js +1 -1
- package/types/index.d.ts +24 -10
- package/types/jsx-platform.d.ts +193 -0
- package/types/parse.d.ts +5 -5
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.10",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
@@ -26,18 +26,21 @@
|
|
|
26
26
|
},
|
|
27
27
|
"./types/acorn": {
|
|
28
28
|
"types": "./types/acorn.d.ts"
|
|
29
|
-
}
|
|
29
|
+
},
|
|
30
|
+
"./test-harness/source-mappings": "./tests/shared/source-mappings.js",
|
|
31
|
+
"./test-harness/compile": "./tests/shared/compile.js"
|
|
30
32
|
},
|
|
31
33
|
"dependencies": {
|
|
32
34
|
"@jridgewell/sourcemap-codec": "^1.5.5",
|
|
35
|
+
"@noble/hashes": "^2.2.0",
|
|
33
36
|
"@sveltejs/acorn-typescript": "^1.0.9",
|
|
37
|
+
"@types/estree-jsx": "^1.0.5",
|
|
38
|
+
"@types/estree": "^1.0.8",
|
|
34
39
|
"acorn": "^8.15.0",
|
|
35
40
|
"esrap": "^2.1.0",
|
|
36
41
|
"is-reference": "^3.0.3",
|
|
37
42
|
"magic-string": "^0.30.18",
|
|
38
|
-
"zimmerframe": "^1.1.2"
|
|
39
|
-
"@types/estree": "^1.0.8",
|
|
40
|
-
"@types/estree-jsx": "^1.0.5"
|
|
43
|
+
"zimmerframe": "^1.1.2"
|
|
41
44
|
},
|
|
42
45
|
"devDependencies": {
|
|
43
46
|
"@types/node": "^24.3.0",
|
package/src/index.js
CHANGED
|
@@ -68,7 +68,8 @@ export {
|
|
|
68
68
|
|
|
69
69
|
// Generic utils
|
|
70
70
|
export {
|
|
71
|
-
|
|
71
|
+
simple_hash as simpleHash,
|
|
72
|
+
strong_hash as strongHash,
|
|
72
73
|
is_void_element as isVoidElement,
|
|
73
74
|
is_reserved as isReserved,
|
|
74
75
|
is_boolean_attribute as isBooleanAttribute,
|
|
@@ -131,6 +132,28 @@ export { sanitize_template_string as sanitizeTemplateString } from './utils/sani
|
|
|
131
132
|
export { escape } from './utils/escaping.js';
|
|
132
133
|
|
|
133
134
|
// Transform
|
|
135
|
+
export { createJsxTransform } from './transform/jsx/index.js';
|
|
136
|
+
export {
|
|
137
|
+
ensure_function_metadata as ensureFunctionMetadata,
|
|
138
|
+
in_jsx_child_context as inJsxChildContext,
|
|
139
|
+
tsx_node_to_jsx_expression as tsxNodeToJsxExpression,
|
|
140
|
+
tsx_with_ts_locations as tsxWithTsLocations,
|
|
141
|
+
} from './transform/jsx/helpers.js';
|
|
142
|
+
export {
|
|
143
|
+
clone_expression_node,
|
|
144
|
+
clone_identifier,
|
|
145
|
+
clone_jsx_name,
|
|
146
|
+
create_compile_error,
|
|
147
|
+
create_generated_identifier,
|
|
148
|
+
create_null_literal,
|
|
149
|
+
flatten_switch_consequent,
|
|
150
|
+
get_for_of_iteration_params,
|
|
151
|
+
identifier_to_jsx_name,
|
|
152
|
+
is_dynamic_element_id,
|
|
153
|
+
is_jsx_child,
|
|
154
|
+
set_loc,
|
|
155
|
+
to_text_expression,
|
|
156
|
+
} from './transform/jsx/ast-builders.js';
|
|
134
157
|
export { render_stylesheets as renderStylesheets } from './transform/stylesheet.js';
|
|
135
158
|
export {
|
|
136
159
|
prepare_stylesheet_for_render as prepareStylesheetForRender,
|
package/src/parse/style.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** @import * as AST from 'estree' */
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { simple_hash } from '../utils/hashing.js';
|
|
4
4
|
|
|
5
5
|
const REGEX_MATCHER = /^[~^$*|]?=/;
|
|
6
6
|
const REGEX_ATTRIBUTE_FLAGS = /^[a-zA-Z]+/;
|
|
@@ -119,7 +119,7 @@ export function parse_style(content, options) {
|
|
|
119
119
|
|
|
120
120
|
return {
|
|
121
121
|
source: content,
|
|
122
|
-
hash: `tsrx-${
|
|
122
|
+
hash: `tsrx-${simple_hash(content)}`,
|
|
123
123
|
type: 'StyleSheet',
|
|
124
124
|
children: read_body(parser),
|
|
125
125
|
start: 0,
|
package/src/plugin.js
CHANGED
|
@@ -1006,7 +1006,7 @@ export function TSRXPlugin(config) {
|
|
|
1006
1006
|
if (this.type === tt.braceR) {
|
|
1007
1007
|
this.raise(
|
|
1008
1008
|
this.start,
|
|
1009
|
-
'"html" is a
|
|
1009
|
+
'"html" is a TSRX keyword and must be used in the form {html some_content}',
|
|
1010
1010
|
);
|
|
1011
1011
|
}
|
|
1012
1012
|
} else if (this.type === tt.name && this.value === 'text') {
|
|
@@ -1015,7 +1015,7 @@ export function TSRXPlugin(config) {
|
|
|
1015
1015
|
if (this.type === tt.braceR) {
|
|
1016
1016
|
this.raise(
|
|
1017
1017
|
this.start,
|
|
1018
|
-
'"text" is a
|
|
1018
|
+
'"text" is a TSRX keyword and must be used in the form {text some_value}',
|
|
1019
1019
|
);
|
|
1020
1020
|
}
|
|
1021
1021
|
}
|
package/src/source-map-utils.js
CHANGED
|
@@ -278,6 +278,8 @@ export function build_line_offsets(text) {
|
|
|
278
278
|
}
|
|
279
279
|
|
|
280
280
|
/**
|
|
281
|
+
* DO NOT EXPORT THIS FUNCTION!
|
|
282
|
+
* THE FIX NEEDS TO HAPPEN IN THE TRANSFORMER, SEGMENTS OR PARSER
|
|
281
283
|
* @param {AST.Node | AST.NodeWithLocation} node
|
|
282
284
|
* @param {CodeToGeneratedMap} src_to_gen_map
|
|
283
285
|
* @param {number[]} gen_line_offsets
|
|
@@ -286,7 +288,7 @@ export function build_line_offsets(text) {
|
|
|
286
288
|
* @param {number} [gen_max_len]
|
|
287
289
|
* @returns {CodeMapping | Error}
|
|
288
290
|
*/
|
|
289
|
-
|
|
291
|
+
function __maybe_get_mapping_from_node(
|
|
290
292
|
node,
|
|
291
293
|
src_to_gen_map,
|
|
292
294
|
gen_line_offsets,
|
|
@@ -341,7 +343,7 @@ export function get_mapping_from_node(
|
|
|
341
343
|
src_max_len,
|
|
342
344
|
gen_max_len,
|
|
343
345
|
) {
|
|
344
|
-
const mapping =
|
|
346
|
+
const mapping = __maybe_get_mapping_from_node(
|
|
345
347
|
node,
|
|
346
348
|
src_to_gen_map,
|
|
347
349
|
gen_line_offsets,
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/** @import * as AST from 'estree' */
|
|
2
|
+
/** @import * as ESTreeJSX from 'estree-jsx' */
|
|
3
|
+
|
|
4
|
+
import { set_location } from '../../utils/builders.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* AST-building utilities shared across every JSX target (React, Preact,
|
|
8
|
+
* Solid). These are pure, platform-agnostic helpers — anything that ends up
|
|
9
|
+
* branching on target semantics belongs elsewhere.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Attach `source_node`'s `loc` to `node` (deep), defaulting `node.metadata`
|
|
14
|
+
* so downstream walks / serializers don't trip on it being undefined.
|
|
15
|
+
*
|
|
16
|
+
* @template T
|
|
17
|
+
* @param {T} node
|
|
18
|
+
* @param {any} source_node
|
|
19
|
+
* @returns {T}
|
|
20
|
+
*/
|
|
21
|
+
export function set_loc(node, source_node) {
|
|
22
|
+
/** @type {any} */ (node).metadata ??= { path: [] };
|
|
23
|
+
if (source_node?.loc) {
|
|
24
|
+
return /** @type {T} */ (set_location(/** @type {any} */ (node), source_node, true));
|
|
25
|
+
}
|
|
26
|
+
return node;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Shallow-clone an Identifier (keeps name, copies loc via `set_loc`, fresh
|
|
31
|
+
* metadata). Used when the same identifier must appear in both a declaration
|
|
32
|
+
* and a reference without sharing mutable metadata.
|
|
33
|
+
*
|
|
34
|
+
* @param {AST.Identifier} identifier
|
|
35
|
+
* @returns {AST.Identifier}
|
|
36
|
+
*/
|
|
37
|
+
export function clone_identifier(identifier) {
|
|
38
|
+
return set_loc(
|
|
39
|
+
/** @type {any} */ ({
|
|
40
|
+
type: 'Identifier',
|
|
41
|
+
name: identifier.name,
|
|
42
|
+
metadata: { path: [] },
|
|
43
|
+
}),
|
|
44
|
+
identifier,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Clone a JSX element name (handles `JSXIdentifier`, `JSXMemberExpression`,
|
|
50
|
+
* and plain `Identifier`).
|
|
51
|
+
*
|
|
52
|
+
* @param {any} name
|
|
53
|
+
* @param {any} [source_node]
|
|
54
|
+
* @returns {any}
|
|
55
|
+
*/
|
|
56
|
+
export function clone_jsx_name(name, source_node = name) {
|
|
57
|
+
if (!name) return name;
|
|
58
|
+
if (name.type === 'JSXIdentifier') {
|
|
59
|
+
return set_loc(
|
|
60
|
+
/** @type {any} */ ({
|
|
61
|
+
type: 'JSXIdentifier',
|
|
62
|
+
name: name.name,
|
|
63
|
+
metadata: name.metadata || { path: [] },
|
|
64
|
+
}),
|
|
65
|
+
source_node,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
if (name.type === 'JSXMemberExpression') {
|
|
69
|
+
return set_loc(
|
|
70
|
+
/** @type {any} */ ({
|
|
71
|
+
type: 'JSXMemberExpression',
|
|
72
|
+
object: clone_jsx_name(name.object, source_node.object || name.object),
|
|
73
|
+
property: clone_jsx_name(name.property, source_node.property || name.property),
|
|
74
|
+
metadata: name.metadata || { path: [] },
|
|
75
|
+
}),
|
|
76
|
+
source_node,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
if (name.type === 'Identifier') {
|
|
80
|
+
return set_loc(
|
|
81
|
+
/** @type {any} */ ({
|
|
82
|
+
type: 'JSXIdentifier',
|
|
83
|
+
name: name.name,
|
|
84
|
+
metadata: name.metadata || { path: [] },
|
|
85
|
+
}),
|
|
86
|
+
source_node,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return name;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @returns {AST.Literal}
|
|
94
|
+
*/
|
|
95
|
+
export function create_null_literal() {
|
|
96
|
+
return /** @type {any} */ ({
|
|
97
|
+
type: 'Literal',
|
|
98
|
+
value: null,
|
|
99
|
+
raw: 'null',
|
|
100
|
+
metadata: { path: [] },
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @param {string} name
|
|
106
|
+
* @returns {AST.Identifier}
|
|
107
|
+
*/
|
|
108
|
+
export function create_generated_identifier(name) {
|
|
109
|
+
return /** @type {any} */ ({
|
|
110
|
+
type: 'Identifier',
|
|
111
|
+
name,
|
|
112
|
+
metadata: { path: [] },
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @param {any} node
|
|
118
|
+
* @param {string} message
|
|
119
|
+
* @returns {Error & { pos: number, end: number }}
|
|
120
|
+
*/
|
|
121
|
+
export function create_compile_error(node, message) {
|
|
122
|
+
const error = /** @type {Error & { pos: number, end: number }} */ (new Error(message));
|
|
123
|
+
error.pos = node.start ?? 0;
|
|
124
|
+
error.end = node.end ?? error.pos + 1;
|
|
125
|
+
return error;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Convert an Identifier / MemberExpression into a JSX element name. The
|
|
130
|
+
* top-level `Identifier` → `JSXIdentifier` case flags capitalised names as
|
|
131
|
+
* `is_component` so `segments.js` can extend the JSX element name's source
|
|
132
|
+
* mapping backwards to cover the `component ` keyword and attach the
|
|
133
|
+
* component hover label — without that flag those source-map adjustments
|
|
134
|
+
* and editor hover features silently drop for any composite element.
|
|
135
|
+
*
|
|
136
|
+
* @param {any} id
|
|
137
|
+
* @returns {any}
|
|
138
|
+
*/
|
|
139
|
+
export function identifier_to_jsx_name(id) {
|
|
140
|
+
if (!id) return id;
|
|
141
|
+
if (id.type === 'Identifier') {
|
|
142
|
+
return set_loc(
|
|
143
|
+
/** @type {any} */ ({
|
|
144
|
+
type: 'JSXIdentifier',
|
|
145
|
+
name: id.name,
|
|
146
|
+
metadata: { path: [], is_component: /^[A-Z]/.test(id.name) },
|
|
147
|
+
}),
|
|
148
|
+
id,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
if (id.type === 'MemberExpression') {
|
|
152
|
+
return set_loc(
|
|
153
|
+
/** @type {any} */ ({
|
|
154
|
+
type: 'JSXMemberExpression',
|
|
155
|
+
object: identifier_to_jsx_name(id.object),
|
|
156
|
+
property: identifier_to_jsx_name(id.property),
|
|
157
|
+
metadata: id.metadata || { path: [] },
|
|
158
|
+
}),
|
|
159
|
+
id,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
return id;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* @param {any} node
|
|
167
|
+
* @returns {boolean}
|
|
168
|
+
*/
|
|
169
|
+
export function is_jsx_child(node) {
|
|
170
|
+
if (!node) return false;
|
|
171
|
+
const t = node.type;
|
|
172
|
+
return (
|
|
173
|
+
t === 'JSXElement' ||
|
|
174
|
+
t === 'JSXFragment' ||
|
|
175
|
+
t === 'JSXExpressionContainer' ||
|
|
176
|
+
t === 'JSXText' ||
|
|
177
|
+
t === 'Tsx' ||
|
|
178
|
+
t === 'TsxCompat' ||
|
|
179
|
+
t === 'Element' ||
|
|
180
|
+
t === 'Text' ||
|
|
181
|
+
t === 'TSRXExpression' ||
|
|
182
|
+
t === 'Html' ||
|
|
183
|
+
t === 'IfStatement' ||
|
|
184
|
+
t === 'ForOfStatement' ||
|
|
185
|
+
t === 'SwitchStatement' ||
|
|
186
|
+
t === 'TryStatement'
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* A dynamic element id is one whose identifier is `tracked` — i.e. it was
|
|
192
|
+
* introduced by reactive destructuring so its value can change at runtime.
|
|
193
|
+
*
|
|
194
|
+
* @param {any} id
|
|
195
|
+
* @returns {boolean}
|
|
196
|
+
*/
|
|
197
|
+
export function is_dynamic_element_id(id) {
|
|
198
|
+
if (!id || typeof id !== 'object') {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
if (id.type === 'Identifier') {
|
|
202
|
+
return !!id.tracked;
|
|
203
|
+
}
|
|
204
|
+
if (id.type === 'MemberExpression') {
|
|
205
|
+
return is_dynamic_element_id(id.object);
|
|
206
|
+
}
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Gather the params a `for (x of y; index i)` loop should expose to its body
|
|
212
|
+
* JSX (value first, optional index second).
|
|
213
|
+
*
|
|
214
|
+
* @param {any} left
|
|
215
|
+
* @param {any} [index]
|
|
216
|
+
* @returns {any[]}
|
|
217
|
+
*/
|
|
218
|
+
export function get_for_of_iteration_params(left, index) {
|
|
219
|
+
/** @type {any[]} */
|
|
220
|
+
const params = [];
|
|
221
|
+
if (left?.type === 'VariableDeclaration' && left.declarations?.[0]) {
|
|
222
|
+
params.push(left.declarations[0].id);
|
|
223
|
+
} else {
|
|
224
|
+
params.push(left);
|
|
225
|
+
}
|
|
226
|
+
if (index) {
|
|
227
|
+
params.push(index);
|
|
228
|
+
}
|
|
229
|
+
return params;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Flatten a switch case's `consequent` so statements inside a top-level
|
|
234
|
+
* `BlockStatement` are treated as siblings of statements declared directly
|
|
235
|
+
* under the case. This lets `case` arms use `{ ... }` for readability
|
|
236
|
+
* without the block becoming a fresh scope at the JSX level.
|
|
237
|
+
*
|
|
238
|
+
* @param {any[]} consequent
|
|
239
|
+
* @returns {any[]}
|
|
240
|
+
*/
|
|
241
|
+
export function flatten_switch_consequent(consequent) {
|
|
242
|
+
const result = [];
|
|
243
|
+
for (const node of consequent) {
|
|
244
|
+
if (node.type === 'BlockStatement') {
|
|
245
|
+
result.push(...node.body);
|
|
246
|
+
} else {
|
|
247
|
+
result.push(node);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Build `expr == null ? '' : expr + ''` — the text-coerce form used when a
|
|
255
|
+
* Ripple `{expr}` child must render as a string in JSX (React/Preact drop
|
|
256
|
+
* booleans; Solid's default child semantics don't either). Solid uses this
|
|
257
|
+
* via `to_jsx_child`; React/Preact wrap it in a JSXExpressionContainer.
|
|
258
|
+
*
|
|
259
|
+
* @param {AST.Expression} expression
|
|
260
|
+
* @param {any} [source_node]
|
|
261
|
+
* @returns {AST.Expression}
|
|
262
|
+
*/
|
|
263
|
+
export function to_text_expression(expression, source_node = expression) {
|
|
264
|
+
return set_loc(
|
|
265
|
+
/** @type {AST.Expression} */ ({
|
|
266
|
+
type: 'ConditionalExpression',
|
|
267
|
+
test: {
|
|
268
|
+
type: 'BinaryExpression',
|
|
269
|
+
operator: '==',
|
|
270
|
+
left: clone_expression_node(expression),
|
|
271
|
+
right: create_null_literal(),
|
|
272
|
+
metadata: { path: [] },
|
|
273
|
+
},
|
|
274
|
+
consequent: {
|
|
275
|
+
type: 'Literal',
|
|
276
|
+
value: '',
|
|
277
|
+
raw: "''",
|
|
278
|
+
metadata: { path: [] },
|
|
279
|
+
},
|
|
280
|
+
alternate: {
|
|
281
|
+
type: 'BinaryExpression',
|
|
282
|
+
operator: '+',
|
|
283
|
+
left: clone_expression_node(expression),
|
|
284
|
+
right: {
|
|
285
|
+
type: 'Literal',
|
|
286
|
+
value: '',
|
|
287
|
+
raw: "''",
|
|
288
|
+
metadata: { path: [] },
|
|
289
|
+
},
|
|
290
|
+
metadata: { path: [] },
|
|
291
|
+
},
|
|
292
|
+
metadata: { path: [] },
|
|
293
|
+
}),
|
|
294
|
+
source_node,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Deep-clone an AST subtree. `loc` / `start` / `end` are shallow-shared by
|
|
300
|
+
* reference rather than recursed into — `loc` objects can contain back-refs
|
|
301
|
+
* to sub-objects that would blow the stack with a naive deep clone, and
|
|
302
|
+
* every other traversal in the targets treats these positional keys as
|
|
303
|
+
* shared.
|
|
304
|
+
*
|
|
305
|
+
* @param {any} node
|
|
306
|
+
* @returns {any}
|
|
307
|
+
*/
|
|
308
|
+
export function clone_expression_node(node) {
|
|
309
|
+
if (!node || typeof node !== 'object') return node;
|
|
310
|
+
if (Array.isArray(node)) return node.map(clone_expression_node);
|
|
311
|
+
const clone = { ...node };
|
|
312
|
+
for (const key of Object.keys(clone)) {
|
|
313
|
+
if (key === 'loc' || key === 'start' || key === 'end') continue;
|
|
314
|
+
if (key === 'metadata') {
|
|
315
|
+
clone.metadata = clone.metadata ? { ...clone.metadata } : { path: [] };
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
clone[key] = clone_expression_node(clone[key]);
|
|
319
|
+
}
|
|
320
|
+
return clone;
|
|
321
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/** @import * as AST from 'estree' */
|
|
2
|
+
/** @import { Visitors } from 'zimmerframe' */
|
|
3
|
+
|
|
4
|
+
import tsx from 'esrap/languages/tsx';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Zimmerframe provides `path` as the ancestor chain (in original pre-transform
|
|
8
|
+
* types, since visitors run bottom-up). A Tsx node whose parent is a ripple
|
|
9
|
+
* `Element` will render as a JSX child of that element; anywhere else it
|
|
10
|
+
* renders as a standalone expression (e.g. a return value).
|
|
11
|
+
*
|
|
12
|
+
* @param {any[]} path
|
|
13
|
+
* @returns {boolean}
|
|
14
|
+
*/
|
|
15
|
+
export function in_jsx_child_context(path) {
|
|
16
|
+
const parent = path[path.length - 1];
|
|
17
|
+
return !!parent && parent.type === 'Element';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Flatten a `<tsx>` / fragment node's children into a single expression. In a
|
|
22
|
+
* JSX-child position, a JSXExpressionContainer `{expr}` is valid and must stay
|
|
23
|
+
* wrapped. In an expression position (e.g. `return ...`), `{expr}` parses as
|
|
24
|
+
* a block/object literal, so unwrap to `expr`.
|
|
25
|
+
*
|
|
26
|
+
* @param {any} node
|
|
27
|
+
* @param {boolean} [in_jsx_child]
|
|
28
|
+
* @returns {any}
|
|
29
|
+
*/
|
|
30
|
+
export function tsx_node_to_jsx_expression(node, in_jsx_child = false) {
|
|
31
|
+
const children = (node.children || []).filter(
|
|
32
|
+
(/** @type {any} */ child) => child.type !== 'JSXText' || child.value.trim() !== '',
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
if (children.length === 1 && children[0].type !== 'JSXText') {
|
|
36
|
+
const only = children[0];
|
|
37
|
+
if (only.type === 'JSXExpressionContainer' && !in_jsx_child) {
|
|
38
|
+
return only.expression;
|
|
39
|
+
}
|
|
40
|
+
return only;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return /** @type {any} */ ({
|
|
44
|
+
type: 'JSXFragment',
|
|
45
|
+
openingFragment: { type: 'JSXOpeningFragment', metadata: { path: [] } },
|
|
46
|
+
closingFragment: { type: 'JSXClosingFragment', metadata: { path: [] } },
|
|
47
|
+
children,
|
|
48
|
+
metadata: { path: [] },
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Default `node.metadata` to `{ path: [] }` if missing, then continue the
|
|
54
|
+
* walk. Use as the `FunctionDeclaration` / `FunctionExpression` /
|
|
55
|
+
* `ArrowFunctionExpression` visitor in a zimmerframe walk so that downstream
|
|
56
|
+
* consumers (e.g. `segments.js` reading `node.value.metadata.is_component`
|
|
57
|
+
* on class methods) don't trip on an undefined metadata object.
|
|
58
|
+
*
|
|
59
|
+
* Ripple's analyze phase does this via `visit_function`; the tsrx-* targets
|
|
60
|
+
* have no analyze phase, so we default metadata during the main walk.
|
|
61
|
+
*
|
|
62
|
+
* @param {any} node
|
|
63
|
+
* @param {{ next: () => any }} ctx
|
|
64
|
+
*/
|
|
65
|
+
export function ensure_function_metadata(node, { next }) {
|
|
66
|
+
if (!node.metadata) {
|
|
67
|
+
node.metadata = { path: [] };
|
|
68
|
+
}
|
|
69
|
+
return next();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Wrap esrap's `tsx()` printer with location markers for nodes whose spans
|
|
74
|
+
* (e.g. the leading `new ` of a NewExpression or the angle-bracket delimiters
|
|
75
|
+
* around generic arguments) are otherwise invisible to the source map.
|
|
76
|
+
* Without these markers, Volar mapping collection in `segments.js` throws
|
|
77
|
+
* when looking up the node's start/end positions.
|
|
78
|
+
*
|
|
79
|
+
* Shared across all JSX-producing targets (React, Preact, Solid).
|
|
80
|
+
*
|
|
81
|
+
* @returns {any}
|
|
82
|
+
*/
|
|
83
|
+
export function tsx_with_ts_locations() {
|
|
84
|
+
const base = /** @type {any} */ (tsx());
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {any} node
|
|
88
|
+
* @param {any} context
|
|
89
|
+
* @param {any} visitor
|
|
90
|
+
*/
|
|
91
|
+
const wrap_with_locations = (node, context, visitor) => {
|
|
92
|
+
if (!node.loc) {
|
|
93
|
+
visitor(node, context);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
context.location(node.loc.start.line, node.loc.start.column);
|
|
97
|
+
visitor(node, context);
|
|
98
|
+
context.location(node.loc.end.line, node.loc.end.column);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/** @type {Record<string, (node: any, context: any) => void>} */
|
|
102
|
+
const wrappers = {};
|
|
103
|
+
for (const type of [
|
|
104
|
+
// JS nodes whose esrap printer emits no location marker, causing
|
|
105
|
+
// segments.js get_mapping_from_node() to throw when it asks for the
|
|
106
|
+
// generated position of the node's start (or end).
|
|
107
|
+
'NewExpression',
|
|
108
|
+
'MemberExpression',
|
|
109
|
+
'ObjectExpression',
|
|
110
|
+
'ReturnStatement',
|
|
111
|
+
'ForStatement',
|
|
112
|
+
'ForInStatement',
|
|
113
|
+
'TemplateLiteral',
|
|
114
|
+
'AwaitExpression',
|
|
115
|
+
'TaggedTemplateExpression',
|
|
116
|
+
// JSX wrapper nodes: esrap writes `<`, `>`, `</`, `{`, `}` without
|
|
117
|
+
// locations, so the opening/closing element's and expression
|
|
118
|
+
// container's start and end don't resolve.
|
|
119
|
+
'JSXOpeningElement',
|
|
120
|
+
'JSXClosingElement',
|
|
121
|
+
'JSXExpressionContainer',
|
|
122
|
+
// TS wrapper nodes with the same issue.
|
|
123
|
+
'TSTypeParameterInstantiation',
|
|
124
|
+
'TSTypeParameterDeclaration',
|
|
125
|
+
'TSTypeParameter',
|
|
126
|
+
]) {
|
|
127
|
+
wrappers[type] = (node, context) => wrap_with_locations(node, context, base[type]);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { ...base, ...wrappers };
|
|
131
|
+
}
|