@tsrx/core 0.0.8 → 0.0.9

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 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.8",
6
+ "version": "0.0.9",
7
7
  "type": "module",
8
8
  "repository": {
9
9
  "type": "git",
@@ -26,7 +26,9 @@
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",
package/src/index.js CHANGED
@@ -131,6 +131,28 @@ export { sanitize_template_string as sanitizeTemplateString } from './utils/sani
131
131
  export { escape } from './utils/escaping.js';
132
132
 
133
133
  // Transform
134
+ export { createJsxTransform } from './transform/jsx/index.js';
135
+ export {
136
+ ensure_function_metadata as ensureFunctionMetadata,
137
+ in_jsx_child_context as inJsxChildContext,
138
+ tsx_node_to_jsx_expression as tsxNodeToJsxExpression,
139
+ tsx_with_ts_locations as tsxWithTsLocations,
140
+ } from './transform/jsx/helpers.js';
141
+ export {
142
+ clone_expression_node,
143
+ clone_identifier,
144
+ clone_jsx_name,
145
+ create_compile_error,
146
+ create_generated_identifier,
147
+ create_null_literal,
148
+ flatten_switch_consequent,
149
+ get_for_of_iteration_params,
150
+ identifier_to_jsx_name,
151
+ is_dynamic_element_id,
152
+ is_jsx_child,
153
+ set_loc,
154
+ to_text_expression,
155
+ } from './transform/jsx/ast-builders.js';
134
156
  export { render_stylesheets as renderStylesheets } from './transform/stylesheet.js';
135
157
  export {
136
158
  prepare_stylesheet_for_render as prepareStylesheetForRender,
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 Ripple keyword and must be used in the form {html some_content}',
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 Ripple keyword and must be used in the form {text some_value}',
1018
+ '"text" is a TSRX keyword and must be used in the form {text some_value}',
1019
1019
  );
1020
1020
  }
1021
1021
  }
@@ -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
- export function maybe_get_mapping_from_node(
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 = maybe_get_mapping_from_node(
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
+ }