@tsrx/core 0.0.3 → 0.0.5

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.3",
6
+ "version": "0.0.5",
7
7
  "type": "module",
8
8
  "repository": {
9
9
  "type": "git",
package/src/index.js CHANGED
@@ -132,7 +132,29 @@ export { escape } from './utils/escaping.js';
132
132
 
133
133
  // Transform
134
134
  export { render_stylesheets as renderStylesheets } from './transform/stylesheet.js';
135
- export { convert_source_map_to_mappings as convertSourceMapToMappings } from './transform/segments.js';
135
+ export {
136
+ prepare_stylesheet_for_render as prepareStylesheetForRender,
137
+ is_style_element as isStyleElement,
138
+ is_composite_element as isCompositeElement,
139
+ annotate_with_hash as annotateWithHash,
140
+ annotate_component_with_hash as annotateComponentWithHash,
141
+ add_hash_class as addHashClass,
142
+ } from './transform/scoping.js';
143
+ export {
144
+ convert_source_map_to_mappings as convertSourceMapToMappings,
145
+ create_volar_mappings_result as createVolarMappingsResult,
146
+ dedupe_mappings as dedupeMappings,
147
+ serialize_mapping_value as serializeMappingValue,
148
+ } from './transform/segments.js';
149
+ export {
150
+ create_lazy_context as createLazyContext,
151
+ collect_lazy_bindings as collectLazyBindings,
152
+ collect_lazy_bindings_from_component as collectLazyBindingsFromComponent,
153
+ collect_lazy_bindings_from_statements as collectLazyBindingsFromStatements,
154
+ preallocate_lazy_ids as preallocateLazyIds,
155
+ apply_lazy_transforms as applyLazyTransforms,
156
+ replace_lazy_params as replaceLazyParams,
157
+ } from './transform/lazy.js';
136
158
 
137
159
  // Analyze
138
160
  export { analyze_css as analyzeCss } from './analyze/css-analyze.js';
@@ -110,6 +110,68 @@ export function isWhitespaceTextNode(node) {
110
110
  return false;
111
111
  }
112
112
 
113
+ /**
114
+ * @type {AcornPlugin}
115
+ */
116
+ function elementTemplateClosingTagPlugin(Base) {
117
+ const jsxTagStart = /** @type {any} */ (Base).acornTypeScript?.tokTypes?.jsxTagStart;
118
+ if (!jsxTagStart) return Base;
119
+
120
+ /**
121
+ * @param {any} parser
122
+ */
123
+ function inElementTemplateBodyDirect(parser) {
124
+ const stack = parser.context;
125
+ const top = stack[stack.length - 1];
126
+ const below = stack[stack.length - 2];
127
+ return top && top.token === '{' && below && below.token === '<tag>...</tag>';
128
+ }
129
+
130
+ /**
131
+ * @param {any} parser
132
+ */
133
+ function inElementTemplateBodyAnywhere(parser) {
134
+ const stack = parser.context;
135
+ for (let i = 1; i < stack.length; i++) {
136
+ if (
137
+ stack[i] &&
138
+ stack[i].token === '{' &&
139
+ stack[i - 1] &&
140
+ stack[i - 1].token === '<tag>...</tag>'
141
+ ) {
142
+ return true;
143
+ }
144
+ }
145
+ return false;
146
+ }
147
+
148
+ return class extends Base {
149
+ /** @param {number} code */
150
+ // @ts-ignore — extending acorn's Parser with internal hooks
151
+ getTokenFromCode(code) {
152
+ if (code === 60 /* '<' */ && !(/** @type {any} */ (this).inType)) {
153
+ const self = /** @type {any} */ (this);
154
+ const nextChar =
155
+ self.pos + 1 < self.input.length ? self.input.charCodeAt(self.pos + 1) : -1;
156
+ if (nextChar === 47 /* '/' */ && inElementTemplateBodyDirect(self)) {
157
+ ++self.pos;
158
+ return self.finishToken(jsxTagStart);
159
+ }
160
+ }
161
+ // @ts-ignore — super dispatches to next layer in the plugin chain
162
+ return super.getTokenFromCode(code);
163
+ }
164
+
165
+ // @ts-ignore — extending acorn's Parser with internal hooks
166
+ canInsertSemicolon() {
167
+ const self = /** @type {any} */ (this);
168
+ if (self.type === jsxTagStart && inElementTemplateBodyAnywhere(self)) return true;
169
+ // @ts-ignore
170
+ return super.canInsertSemicolon();
171
+ }
172
+ };
173
+ }
174
+
113
175
  /**
114
176
  * Create a parser by composing Acorn with TypeScript/JSX support and optional framework plugins.
115
177
  *
@@ -125,6 +187,7 @@ export function createParser(...plugins) {
125
187
  acorn.Parser.extend(
126
188
  tsPlugin({ jsx: true }),
127
189
  ...plugins.map((p) => /** @type {AcornPlugin} */ (/** @type {unknown} */ (p))),
190
+ elementTemplateClosingTagPlugin,
128
191
  )
129
192
  )
130
193
  );
@@ -151,7 +214,7 @@ export function createParser(...plugins) {
151
214
  allowReturnOutsideFunction: true,
152
215
  locations: true,
153
216
  onComment,
154
- rippleOptions: {
217
+ tsrxOptions: {
155
218
  filename,
156
219
  errors: options?.errors ?? [],
157
220
  loose: options?.loose || false,
@@ -119,7 +119,7 @@ export function parse_style(content, options) {
119
119
 
120
120
  return {
121
121
  source: content,
122
- hash: `ripple-${hash(content)}`,
122
+ hash: `tsrx-${hash(content)}`,
123
123
  type: 'StyleSheet',
124
124
  children: read_body(parser),
125
125
  start: 0,
package/src/plugin.js CHANGED
@@ -51,9 +51,10 @@ export function TSRXPlugin(config) {
51
51
  */
52
52
  constructor(options, input) {
53
53
  super(options, input);
54
- this.#loose = options?.rippleOptions.loose === true;
55
- this.#errors = options?.rippleOptions.errors;
56
- this.#filename = options?.rippleOptions.filename || null;
54
+ const tsrx_options = options?.tsrxOptions ?? options?.rippleOptions;
55
+ this.#loose = tsrx_options?.loose === true;
56
+ this.#errors = tsrx_options?.errors;
57
+ this.#filename = tsrx_options?.filename || null;
57
58
  }
58
59
 
59
60
  /**
@@ -286,7 +286,7 @@ export function build_line_offsets(text) {
286
286
  * @param {number} [gen_max_len]
287
287
  * @returns {CodeMapping | Error}
288
288
  */
289
- function maybe_get_mapping_from_node(
289
+ export function maybe_get_mapping_from_node(
290
290
  node,
291
291
  src_to_gen_map,
292
292
  gen_line_offsets,
@@ -0,0 +1,664 @@
1
+ /** @import * as AST from 'estree' */
2
+
3
+ /**
4
+ * Lazy destructuring transform — framework-agnostic.
5
+ *
6
+ * Shared between `@tsrx/react` and `@tsrx/solid`. Walks an AST and rewrites
7
+ * references to names introduced by `&{ ... }` / `&[ ... ]` destructuring
8
+ * patterns into member-expression accesses on a generated source identifier.
9
+ *
10
+ * Usage:
11
+ * 1. Create a context with `createLazyContext()` (or provide any object with
12
+ * a `lazy_next_id: number` field).
13
+ * 2. Run `preallocateLazyIds(root, context)` once over the full program to
14
+ * assign stable `metadata.lazy_id` values to every lazy pattern.
15
+ * 3. For each function/component scope, collect bindings with
16
+ * `collectLazyBindingsFromComponent(params, body, context)` and pass the
17
+ * resulting map into `applyLazyTransforms(body, map)`.
18
+ * 4. If a component declares lazy params, pass its params through
19
+ * `replaceLazyParams(params)` before emitting.
20
+ *
21
+ * The transform is purely AST-to-AST and has no framework-specific knowledge.
22
+ */
23
+
24
+ /**
25
+ * @typedef {{ lazy_next_id: number }} LazyContext
26
+ */
27
+
28
+ /**
29
+ * @typedef {{ source_name: string, read: () => any }} LazyBinding
30
+ */
31
+
32
+ /**
33
+ * Create a fresh lazy-id allocation context.
34
+ *
35
+ * @returns {LazyContext}
36
+ */
37
+ export function create_lazy_context() {
38
+ return { lazy_next_id: 0 };
39
+ }
40
+
41
+ /**
42
+ * @param {LazyContext} context
43
+ * @returns {string}
44
+ */
45
+ function generate_lazy_id(context) {
46
+ return `__lazy${context.lazy_next_id++}`;
47
+ }
48
+
49
+ /**
50
+ * @param {string} name
51
+ * @returns {any}
52
+ */
53
+ function create_generated_identifier(name) {
54
+ return /** @type {any} */ ({ type: 'Identifier', name, metadata: { path: [] } });
55
+ }
56
+
57
+ /**
58
+ * Collect lazy bindings from a destructuring pattern.
59
+ *
60
+ * For `&{ name, age }` on source `S`, maps `name` → `S.name`, `age` → `S.age`.
61
+ * For `&[a, b]` on source `S`, maps `a` → `S[0]`, `b` → `S[1]`. Handles nested
62
+ * `AssignmentPattern` (default values); skips `RestElement`.
63
+ *
64
+ * @param {any} pattern
65
+ * @param {string} source_name
66
+ * @param {Map<string, LazyBinding>} lazy_bindings
67
+ */
68
+ export function collect_lazy_bindings(pattern, source_name, lazy_bindings) {
69
+ if (pattern.type === 'ObjectPattern') {
70
+ for (const prop of pattern.properties || []) {
71
+ if (prop.type === 'RestElement') continue;
72
+ const value = prop.value;
73
+ const actual = value.type === 'AssignmentPattern' ? value.left : value;
74
+ if (actual.type === 'Identifier') {
75
+ const key = prop.key;
76
+ const computed = prop.computed || key.type !== 'Identifier';
77
+ lazy_bindings.set(actual.name, {
78
+ source_name,
79
+ read: () => ({
80
+ type: 'MemberExpression',
81
+ object: create_generated_identifier(source_name),
82
+ property: computed
83
+ ? { ...key }
84
+ : { type: 'Identifier', name: key.name, metadata: { path: [] } },
85
+ computed,
86
+ optional: false,
87
+ metadata: { path: [] },
88
+ }),
89
+ });
90
+ }
91
+ }
92
+ } else if (pattern.type === 'ArrayPattern') {
93
+ for (let i = 0; i < (pattern.elements || []).length; i++) {
94
+ const element = pattern.elements[i];
95
+ if (!element) continue;
96
+ if (element.type === 'RestElement') continue;
97
+ const actual = element.type === 'AssignmentPattern' ? element.left : element;
98
+ if (actual.type === 'Identifier') {
99
+ const index = i;
100
+ lazy_bindings.set(actual.name, {
101
+ source_name,
102
+ read: () => ({
103
+ type: 'MemberExpression',
104
+ object: create_generated_identifier(source_name),
105
+ property: { type: 'Literal', value: index, raw: String(index), metadata: { path: [] } },
106
+ computed: true,
107
+ optional: false,
108
+ metadata: { path: [] },
109
+ }),
110
+ });
111
+ }
112
+ }
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Collect lazy bindings from a component's params and top-level body declarations.
118
+ * Mutates each lazy pattern's `metadata.lazy_id` in place (idempotent if already set).
119
+ *
120
+ * @param {any[]} params
121
+ * @param {any[]} body
122
+ * @param {LazyContext} context
123
+ * @returns {Map<string, LazyBinding>}
124
+ */
125
+ export function collect_lazy_bindings_from_component(params, body, context) {
126
+ /** @type {Map<string, LazyBinding>} */
127
+ const lazy_bindings = new Map();
128
+
129
+ for (const param of params) {
130
+ const pattern = param.type === 'AssignmentPattern' ? param.left : param;
131
+ if ((pattern.type === 'ObjectPattern' || pattern.type === 'ArrayPattern') && pattern.lazy) {
132
+ const lazy_name = pattern.metadata?.lazy_id || generate_lazy_id(context);
133
+ if (!pattern.metadata?.lazy_id) {
134
+ pattern.metadata = { ...pattern.metadata, lazy_id: lazy_name };
135
+ }
136
+ collect_lazy_bindings(pattern, lazy_name, lazy_bindings);
137
+ }
138
+ }
139
+
140
+ // VariableDeclaration lazy patterns already have their `lazy_id` assigned
141
+ // by `preallocate_lazy_ids` (run once over the whole program by the target
142
+ // transforms), so `collect_lazy_bindings_from_statements` handles them
143
+ // alongside the expression-statement assignment form.
144
+ collect_lazy_bindings_from_statements(body, lazy_bindings);
145
+
146
+ return lazy_bindings;
147
+ }
148
+
149
+ /**
150
+ * Collect lazy bindings from statements at the top level of a block. Reads
151
+ * already-allocated `lazy_id` values from pattern metadata. Handles both
152
+ * `let &[x] = ...` variable declarations and statement-level `&[x] = expr;`
153
+ * assignment expressions.
154
+ *
155
+ * @param {any[]} statements
156
+ * @param {Map<string, LazyBinding>} lazy_bindings
157
+ */
158
+ export function collect_lazy_bindings_from_statements(statements, lazy_bindings) {
159
+ for (const stmt of statements || []) {
160
+ if (stmt.type === 'VariableDeclaration') {
161
+ for (const declarator of stmt.declarations || []) {
162
+ const pattern = declarator.id;
163
+ if (
164
+ (pattern?.type === 'ObjectPattern' || pattern?.type === 'ArrayPattern') &&
165
+ pattern.lazy &&
166
+ pattern.metadata?.lazy_id &&
167
+ !lazy_bindings_contains(lazy_bindings, pattern)
168
+ ) {
169
+ collect_lazy_bindings(pattern, pattern.metadata.lazy_id, lazy_bindings);
170
+ }
171
+ }
172
+ } else if (
173
+ stmt.type === 'ExpressionStatement' &&
174
+ stmt.expression?.type === 'AssignmentExpression' &&
175
+ stmt.expression.operator === '=' &&
176
+ (stmt.expression.left?.type === 'ObjectPattern' ||
177
+ stmt.expression.left?.type === 'ArrayPattern') &&
178
+ stmt.expression.left.lazy &&
179
+ stmt.expression.left.metadata?.lazy_id
180
+ ) {
181
+ collect_lazy_bindings(
182
+ stmt.expression.left,
183
+ stmt.expression.left.metadata.lazy_id,
184
+ lazy_bindings,
185
+ );
186
+ }
187
+ }
188
+ }
189
+
190
+ /**
191
+ * @param {Map<string, LazyBinding>} lazy_bindings
192
+ * @param {any} pattern
193
+ * @returns {boolean}
194
+ */
195
+ function lazy_bindings_contains(lazy_bindings, pattern) {
196
+ if (pattern.type === 'ObjectPattern') {
197
+ for (const prop of pattern.properties || []) {
198
+ if (prop.type === 'RestElement') continue;
199
+ const value = prop.value;
200
+ const actual = value?.type === 'AssignmentPattern' ? value.left : value;
201
+ if (actual?.type === 'Identifier' && lazy_bindings.has(actual.name)) return true;
202
+ }
203
+ } else if (pattern.type === 'ArrayPattern') {
204
+ for (const element of pattern.elements || []) {
205
+ if (!element || element.type === 'RestElement') continue;
206
+ const actual = element.type === 'AssignmentPattern' ? element.left : element;
207
+ if (actual?.type === 'Identifier' && lazy_bindings.has(actual.name)) return true;
208
+ }
209
+ }
210
+ return false;
211
+ }
212
+
213
+ /**
214
+ * Walk the AST and pre-allocate `lazy_id` metadata on every lazy destructuring
215
+ * pattern: function/component params, variable declarator ids, and statement-level
216
+ * assignment LHS. Idempotent: skips patterns that already have a `lazy_id`.
217
+ *
218
+ * @param {any} root
219
+ * @param {LazyContext} context
220
+ */
221
+ export function preallocate_lazy_ids(root, context) {
222
+ /** @param {any} pattern */
223
+ const assign_id = (pattern) => {
224
+ if (
225
+ (pattern?.type === 'ObjectPattern' || pattern?.type === 'ArrayPattern') &&
226
+ pattern.lazy &&
227
+ !pattern.metadata?.lazy_id
228
+ ) {
229
+ pattern.metadata = {
230
+ ...pattern.metadata,
231
+ lazy_id: generate_lazy_id(context),
232
+ };
233
+ }
234
+ };
235
+
236
+ /** @param {any} node */
237
+ const visit = (node) => {
238
+ if (!node || typeof node !== 'object') return;
239
+ if (Array.isArray(node)) {
240
+ for (const child of node) visit(child);
241
+ return;
242
+ }
243
+
244
+ if (
245
+ node.type === 'FunctionDeclaration' ||
246
+ node.type === 'FunctionExpression' ||
247
+ node.type === 'ArrowFunctionExpression' ||
248
+ node.type === 'Component'
249
+ ) {
250
+ for (const param of node.params || []) {
251
+ assign_id(param?.type === 'AssignmentPattern' ? param.left : param);
252
+ }
253
+ }
254
+
255
+ if (node.type === 'VariableDeclarator') {
256
+ assign_id(node.id);
257
+ }
258
+
259
+ if (
260
+ node.type === 'ExpressionStatement' &&
261
+ node.expression?.type === 'AssignmentExpression' &&
262
+ node.expression.operator === '='
263
+ ) {
264
+ assign_id(node.expression.left);
265
+ }
266
+
267
+ for (const key of Object.keys(node)) {
268
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') continue;
269
+ visit(node[key]);
270
+ }
271
+ };
272
+
273
+ visit(root);
274
+ }
275
+
276
+ /**
277
+ * Recursively rewrite lazy-binding references in `node`.
278
+ *
279
+ * @param {any} node
280
+ * @param {Map<string, LazyBinding>} lazy_bindings
281
+ * @returns {any}
282
+ */
283
+ export function apply_lazy_transforms(node, lazy_bindings) {
284
+ if (!node || typeof node !== 'object') return node;
285
+ if (Array.isArray(node)) return node.map((child) => apply_lazy_transforms(child, lazy_bindings));
286
+
287
+ if (
288
+ node.type === 'FunctionDeclaration' ||
289
+ node.type === 'FunctionExpression' ||
290
+ node.type === 'ArrowFunctionExpression'
291
+ ) {
292
+ // Default parameter values are evaluated in the outer scope — transform them first.
293
+ let params_changed = false;
294
+ const new_params = (node.params || []).map((/** @type {any} */ param) => {
295
+ const transformed = transform_param_defaults(param, lazy_bindings);
296
+ if (transformed !== param) params_changed = true;
297
+ return transformed;
298
+ });
299
+
300
+ /** @type {Set<string>} */
301
+ const shadowed = new Set();
302
+ for (const param of node.params || []) {
303
+ collect_shadowed_names(param, lazy_bindings, shadowed);
304
+ }
305
+
306
+ const outer_minus_shadow =
307
+ shadowed.size > 0 ? remove_shadowed(lazy_bindings, shadowed) : lazy_bindings;
308
+
309
+ /** @type {Map<string, LazyBinding>} */
310
+ const own_bindings = new Map();
311
+ let had_lazy_param = false;
312
+ for (const param of node.params || []) {
313
+ const pattern = param?.type === 'AssignmentPattern' ? param.left : param;
314
+ if (
315
+ (pattern?.type === 'ObjectPattern' || pattern?.type === 'ArrayPattern') &&
316
+ pattern.lazy &&
317
+ pattern.metadata?.lazy_id
318
+ ) {
319
+ had_lazy_param = true;
320
+ collect_lazy_bindings(pattern, pattern.metadata.lazy_id, own_bindings);
321
+ }
322
+ }
323
+
324
+ // Own bindings override any outer binding with the same name.
325
+ const inner_bindings =
326
+ own_bindings.size > 0
327
+ ? new Map([...outer_minus_shadow, ...own_bindings])
328
+ : outer_minus_shadow;
329
+
330
+ if (inner_bindings.size === 0 && !params_changed && !had_lazy_param) return node;
331
+
332
+ const new_body =
333
+ inner_bindings.size > 0 ? apply_lazy_transforms(node.body, inner_bindings) : node.body;
334
+
335
+ const final_params_src = params_changed ? new_params : node.params;
336
+ const final_params = had_lazy_param ? replace_lazy_params(final_params_src) : final_params_src;
337
+
338
+ if (new_body !== node.body || final_params !== node.params) {
339
+ return { ...node, params: final_params, body: new_body };
340
+ }
341
+ return node;
342
+ }
343
+
344
+ if (node.type === 'BlockStatement' || node.type === 'Program') {
345
+ const block_bindings = collect_block_shadowed_names(node.body, lazy_bindings);
346
+ const after_shadow =
347
+ block_bindings.size > 0 ? remove_shadowed(lazy_bindings, block_bindings) : lazy_bindings;
348
+
349
+ /** @type {Map<string, LazyBinding>} */
350
+ const block_lazy = new Map();
351
+ collect_lazy_bindings_from_statements(node.body, block_lazy);
352
+
353
+ const effective_bindings =
354
+ block_lazy.size > 0 ? new Map([...after_shadow, ...block_lazy]) : after_shadow;
355
+
356
+ let changed = false;
357
+ const new_body = node.body.map((/** @type {any} */ stmt) => {
358
+ const transformed = apply_lazy_transforms(stmt, effective_bindings);
359
+ if (transformed !== stmt) changed = true;
360
+ return transformed;
361
+ });
362
+ return changed ? { ...node, body: new_body } : node;
363
+ }
364
+
365
+ if (node.type === 'CatchClause') {
366
+ /** @type {Set<string>} */
367
+ const shadowed = new Set();
368
+ if (node.param) collect_shadowed_names(node.param, lazy_bindings, shadowed);
369
+ const effective_bindings =
370
+ shadowed.size > 0 ? remove_shadowed(lazy_bindings, shadowed) : lazy_bindings;
371
+ const new_body = apply_lazy_transforms(node.body, effective_bindings);
372
+ if (new_body !== node.body) return { ...node, body: new_body };
373
+ return node;
374
+ }
375
+
376
+ if (node.type === 'ForStatement') {
377
+ /** @type {Set<string>} */
378
+ const shadowed = new Set();
379
+ if (node.init?.type === 'VariableDeclaration') {
380
+ for (const decl of node.init.declarations) {
381
+ if (decl.id) collect_shadowed_names(decl.id, lazy_bindings, shadowed);
382
+ }
383
+ }
384
+ const effective_bindings =
385
+ shadowed.size > 0 ? remove_shadowed(lazy_bindings, shadowed) : lazy_bindings;
386
+ let changed = false;
387
+ const new_init = apply_lazy_transforms(node.init, effective_bindings);
388
+ if (new_init !== node.init) changed = true;
389
+ const new_test = apply_lazy_transforms(node.test, effective_bindings);
390
+ if (new_test !== node.test) changed = true;
391
+ const new_update = apply_lazy_transforms(node.update, effective_bindings);
392
+ if (new_update !== node.update) changed = true;
393
+ const new_body = apply_lazy_transforms(node.body, effective_bindings);
394
+ if (new_body !== node.body) changed = true;
395
+ return changed
396
+ ? { ...node, init: new_init, test: new_test, update: new_update, body: new_body }
397
+ : node;
398
+ }
399
+
400
+ if (node.type === 'ForOfStatement' || node.type === 'ForInStatement') {
401
+ /** @type {Set<string>} */
402
+ const shadowed = new Set();
403
+ if (node.left?.type === 'VariableDeclaration') {
404
+ for (const decl of node.left.declarations) {
405
+ if (decl.id) collect_shadowed_names(decl.id, lazy_bindings, shadowed);
406
+ }
407
+ }
408
+ const effective_bindings =
409
+ shadowed.size > 0 ? remove_shadowed(lazy_bindings, shadowed) : lazy_bindings;
410
+ // `node.left` is a binding site, not an expression context: a declaration
411
+ // like `const x` or `const [a, b]` has no outer references to rewrite,
412
+ // and recursing here would hit the VariableDeclarator handler and
413
+ // rewrite a lazy declarator id that `preallocate_lazy_ids` already
414
+ // tagged — double-processing the loop variable. Leave `node.left`
415
+ // untouched; the body and right-hand side are the only scopes with
416
+ // live references.
417
+ let changed = false;
418
+ // The right-hand side is evaluated in the outer scope (before the loop
419
+ // variable is bound), so use the unshadowed bindings there.
420
+ const new_right = apply_lazy_transforms(node.right, lazy_bindings);
421
+ if (new_right !== node.right) changed = true;
422
+ const new_body = apply_lazy_transforms(node.body, effective_bindings);
423
+ if (new_body !== node.body) changed = true;
424
+ return changed ? { ...node, right: new_right, body: new_body } : node;
425
+ }
426
+
427
+ if (node.type === 'SwitchStatement') {
428
+ let changed = false;
429
+ const new_discriminant = apply_lazy_transforms(node.discriminant, lazy_bindings);
430
+ if (new_discriminant !== node.discriminant) changed = true;
431
+ const new_cases = node.cases.map((/** @type {any} */ switch_case) => {
432
+ const case_bindings = collect_block_shadowed_names(switch_case.consequent, lazy_bindings);
433
+ const effective_bindings =
434
+ case_bindings.size > 0 ? remove_shadowed(lazy_bindings, case_bindings) : lazy_bindings;
435
+ let case_changed = false;
436
+ const new_test = switch_case.test
437
+ ? apply_lazy_transforms(switch_case.test, lazy_bindings)
438
+ : null;
439
+ if (new_test !== switch_case.test) case_changed = true;
440
+ const new_consequent = switch_case.consequent.map((/** @type {any} */ stmt) => {
441
+ const transformed = apply_lazy_transforms(stmt, effective_bindings);
442
+ if (transformed !== stmt) case_changed = true;
443
+ return transformed;
444
+ });
445
+ if (case_changed) {
446
+ changed = true;
447
+ return { ...switch_case, test: new_test, consequent: new_consequent };
448
+ }
449
+ return switch_case;
450
+ });
451
+ return changed ? { ...node, discriminant: new_discriminant, cases: new_cases } : node;
452
+ }
453
+
454
+ // Standalone lazy destructuring assignment: `&[data] = track(0);` becomes
455
+ // `const __lazy0 = track(0);`. Individual name bindings are already in scope
456
+ // via the enclosing BlockStatement handler.
457
+ if (
458
+ node.type === 'ExpressionStatement' &&
459
+ node.expression?.type === 'AssignmentExpression' &&
460
+ node.expression.operator === '=' &&
461
+ (node.expression.left?.type === 'ObjectPattern' ||
462
+ node.expression.left?.type === 'ArrayPattern') &&
463
+ node.expression.left.lazy &&
464
+ node.expression.left.metadata?.lazy_id
465
+ ) {
466
+ const pattern = node.expression.left;
467
+ const lazy_id = create_generated_identifier(pattern.metadata.lazy_id);
468
+ if (pattern.typeAnnotation) lazy_id.typeAnnotation = pattern.typeAnnotation;
469
+ const init = apply_lazy_transforms(node.expression.right, lazy_bindings);
470
+ return /** @type {any} */ ({
471
+ type: 'VariableDeclaration',
472
+ kind: 'const',
473
+ declarations: [
474
+ {
475
+ type: 'VariableDeclarator',
476
+ id: lazy_id,
477
+ init,
478
+ metadata: { path: [] },
479
+ },
480
+ ],
481
+ metadata: { path: [] },
482
+ });
483
+ }
484
+
485
+ // AssignmentExpression / UpdateExpression whose target is a lazy identifier.
486
+ if (
487
+ node.type === 'AssignmentExpression' &&
488
+ node.left?.type === 'Identifier' &&
489
+ lazy_bindings.has(node.left.name)
490
+ ) {
491
+ const binding = /** @type {LazyBinding} */ (lazy_bindings.get(node.left.name));
492
+ return {
493
+ ...node,
494
+ left: binding.read(),
495
+ right: apply_lazy_transforms(node.right, lazy_bindings),
496
+ };
497
+ }
498
+
499
+ if (
500
+ node.type === 'UpdateExpression' &&
501
+ node.argument?.type === 'Identifier' &&
502
+ lazy_bindings.has(node.argument.name)
503
+ ) {
504
+ const binding = /** @type {LazyBinding} */ (lazy_bindings.get(node.argument.name));
505
+ return { ...node, argument: binding.read() };
506
+ }
507
+
508
+ // Replace lazy variable declaration patterns with generated identifiers.
509
+ if (node.type === 'VariableDeclarator' && node.id?.metadata?.lazy_id) {
510
+ const lazy_id = create_generated_identifier(node.id.metadata.lazy_id);
511
+ if (node.id.typeAnnotation) lazy_id.typeAnnotation = node.id.typeAnnotation;
512
+ return {
513
+ ...node,
514
+ id: lazy_id,
515
+ init: apply_lazy_transforms(node.init, lazy_bindings),
516
+ };
517
+ }
518
+
519
+ // Shorthand object properties `{ name }` → `{ name: __lazy0.name }`.
520
+ if (node.type === 'Property' && node.shorthand && node.value?.type === 'Identifier') {
521
+ const binding = lazy_bindings.get(node.value.name);
522
+ if (binding) {
523
+ return { ...node, shorthand: false, value: binding.read() };
524
+ }
525
+ }
526
+
527
+ // Bare identifier reference.
528
+ if (node.type === 'Identifier' && lazy_bindings.has(node.name)) {
529
+ const binding = /** @type {LazyBinding} */ (lazy_bindings.get(node.name));
530
+ return binding.read();
531
+ }
532
+
533
+ // JSXIdentifier is a label (component/element name), never a reference.
534
+ if (node.type === 'JSXIdentifier') return node;
535
+
536
+ let changed = false;
537
+ /** @type {any} */
538
+ const clone = { ...node };
539
+ for (const key of Object.keys(node)) {
540
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') continue;
541
+
542
+ // Skip non-computed, non-shorthand property keys (they are labels).
543
+ if (key === 'key' && node.type === 'Property' && !node.computed && !node.shorthand) continue;
544
+ // Skip non-computed member expression property access.
545
+ if (key === 'property' && node.type === 'MemberExpression' && !node.computed) continue;
546
+ // Skip JSXMemberExpression property (label, not reference).
547
+ if (key === 'property' && node.type === 'JSXMemberExpression') continue;
548
+ // Skip JSXAttribute names (labels).
549
+ if (key === 'name' && node.type === 'JSXAttribute') continue;
550
+ // Skip VariableDeclarator id (already handled above).
551
+ if (key === 'id' && node.type === 'VariableDeclarator') continue;
552
+
553
+ const new_value = apply_lazy_transforms(node[key], lazy_bindings);
554
+ if (new_value !== node[key]) {
555
+ clone[key] = new_value;
556
+ changed = true;
557
+ }
558
+ }
559
+ return changed ? clone : node;
560
+ }
561
+
562
+ /**
563
+ * @param {any} param
564
+ * @param {Map<string, LazyBinding>} lazy_bindings
565
+ */
566
+ function transform_param_defaults(param, lazy_bindings) {
567
+ if (param?.type === 'AssignmentPattern') {
568
+ const new_right = apply_lazy_transforms(param.right, lazy_bindings);
569
+ if (new_right !== param.right) return { ...param, right: new_right };
570
+ }
571
+ return param;
572
+ }
573
+
574
+ /**
575
+ * @param {any} pattern
576
+ * @param {Map<string, LazyBinding>} lazy_bindings
577
+ * @param {Set<string>} shadowed
578
+ */
579
+ function collect_shadowed_names(pattern, lazy_bindings, shadowed) {
580
+ if (!pattern || typeof pattern !== 'object') return;
581
+ if (pattern.type === 'Identifier' && lazy_bindings.has(pattern.name)) {
582
+ shadowed.add(pattern.name);
583
+ return;
584
+ }
585
+ if (pattern.type === 'AssignmentPattern') {
586
+ collect_shadowed_names(pattern.left, lazy_bindings, shadowed);
587
+ return;
588
+ }
589
+ if (pattern.type === 'RestElement') {
590
+ collect_shadowed_names(pattern.argument, lazy_bindings, shadowed);
591
+ return;
592
+ }
593
+ if (pattern.type === 'ObjectPattern') {
594
+ for (const prop of pattern.properties || []) {
595
+ if (prop.type === 'RestElement') {
596
+ collect_shadowed_names(prop.argument, lazy_bindings, shadowed);
597
+ } else {
598
+ collect_shadowed_names(prop.value, lazy_bindings, shadowed);
599
+ }
600
+ }
601
+ return;
602
+ }
603
+ if (pattern.type === 'ArrayPattern') {
604
+ for (const element of pattern.elements || []) {
605
+ if (element) collect_shadowed_names(element, lazy_bindings, shadowed);
606
+ }
607
+ }
608
+ }
609
+
610
+ /**
611
+ * @param {any[]} statements
612
+ * @param {Map<string, LazyBinding>} lazy_bindings
613
+ * @returns {Set<string>}
614
+ */
615
+ function collect_block_shadowed_names(statements, lazy_bindings) {
616
+ /** @type {Set<string>} */
617
+ const shadowed = new Set();
618
+ for (const stmt of statements) {
619
+ if (stmt.type === 'VariableDeclaration') {
620
+ for (const decl of stmt.declarations) {
621
+ if (decl.id?.metadata?.lazy_id) continue;
622
+ if (decl.id) collect_shadowed_names(decl.id, lazy_bindings, shadowed);
623
+ }
624
+ } else if (stmt.type === 'FunctionDeclaration' && stmt.id) {
625
+ if (lazy_bindings.has(stmt.id.name)) shadowed.add(stmt.id.name);
626
+ }
627
+ }
628
+ return shadowed;
629
+ }
630
+
631
+ /**
632
+ * @param {Map<string, LazyBinding>} lazy_bindings
633
+ * @param {Set<string>} shadowed
634
+ * @returns {Map<string, LazyBinding>}
635
+ */
636
+ function remove_shadowed(lazy_bindings, shadowed) {
637
+ const result = new Map(lazy_bindings);
638
+ for (const name of shadowed) result.delete(name);
639
+ return result;
640
+ }
641
+
642
+ /**
643
+ * Replace any lazy `&{}` / `&[]` patterns in a parameter list with their
644
+ * generated lazy identifiers. Leaves non-lazy params untouched.
645
+ *
646
+ * @param {any[]} params
647
+ * @returns {any[]}
648
+ */
649
+ export function replace_lazy_params(params) {
650
+ return params.map((param) => {
651
+ const pattern = param.type === 'AssignmentPattern' ? param.left : param;
652
+ if (
653
+ (pattern.type === 'ObjectPattern' || pattern.type === 'ArrayPattern') &&
654
+ pattern.lazy &&
655
+ pattern.metadata?.lazy_id
656
+ ) {
657
+ const lazy_id = create_generated_identifier(pattern.metadata.lazy_id);
658
+ if (pattern.typeAnnotation) lazy_id.typeAnnotation = pattern.typeAnnotation;
659
+ if (param.type === 'AssignmentPattern') return { ...param, left: lazy_id };
660
+ return lazy_id;
661
+ }
662
+ return param;
663
+ });
664
+ }
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Framework-agnostic CSS scoping utilities shared between the `@tsrx/react`
3
+ * and `@tsrx/solid` transforms. These walk the template AST and annotate
4
+ * `Element` nodes with a hash class so scope-qualified selectors
5
+ * (e.g. `.foo.hash`) match after rendering.
6
+ */
7
+
8
+ import { walk } from 'zimmerframe';
9
+
10
+ /**
11
+ * Mark every selector inside the stylesheet as "used" so `renderStylesheets`
12
+ * does not comment it out. We skip selector-pruning because component
13
+ * boundaries can be dynamic — any selector authored inside the component's
14
+ * `<style>` block is considered intentional.
15
+ *
16
+ * @param {any} stylesheet
17
+ * @returns {any}
18
+ */
19
+ export function prepare_stylesheet_for_render(stylesheet) {
20
+ walk(stylesheet, null, {
21
+ _(node, { next }) {
22
+ if (node && node.metadata && typeof node.metadata === 'object') {
23
+ node.metadata.used = true;
24
+ if (node.type === 'RelativeSelector' && !node.metadata.is_global) {
25
+ node.metadata.scoped = true;
26
+ }
27
+ }
28
+ return next();
29
+ },
30
+ });
31
+ return stylesheet;
32
+ }
33
+
34
+ /**
35
+ * @param {any} node
36
+ * @returns {boolean}
37
+ */
38
+ export function is_style_element(node) {
39
+ return (
40
+ node &&
41
+ node.type === 'Element' &&
42
+ node.id &&
43
+ node.id.type === 'Identifier' &&
44
+ node.id.name === 'style'
45
+ );
46
+ }
47
+
48
+ /**
49
+ * @param {any} node
50
+ * @returns {boolean}
51
+ */
52
+ export function is_composite_element(node) {
53
+ if (!node || node.type !== 'Element' || !node.id) {
54
+ return false;
55
+ }
56
+
57
+ if (node.id.type === 'Identifier') {
58
+ return /^[A-Z]/.test(node.id.name);
59
+ }
60
+
61
+ return node.id.type === 'MemberExpression';
62
+ }
63
+
64
+ /**
65
+ * Recursively walk `Element` nodes within a component body and add the hash
66
+ * class name so scope-qualified selectors (e.g. `.foo.hash`) match.
67
+ *
68
+ * @param {any} node
69
+ * @param {string} hash
70
+ * @returns {any}
71
+ */
72
+ export function annotate_with_hash(node, hash) {
73
+ if (!node || typeof node !== 'object') return node;
74
+ if (
75
+ node.type === 'Component' ||
76
+ node.type === 'FunctionDeclaration' ||
77
+ node.type === 'FunctionExpression' ||
78
+ node.type === 'ArrowFunctionExpression'
79
+ ) {
80
+ return node;
81
+ }
82
+
83
+ if (node.type === 'Element') {
84
+ if (!is_style_element(node) && !is_composite_element(node)) {
85
+ add_hash_class(node, hash);
86
+ }
87
+ if (Array.isArray(node.children)) {
88
+ node.children = node.children
89
+ .filter((/** @type {any} */ child) => !is_style_element(child))
90
+ .map((/** @type {any} */ child) => annotate_with_hash(child, hash));
91
+ }
92
+ return node;
93
+ }
94
+
95
+ for (const key of Object.keys(node)) {
96
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata' || key === 'css') {
97
+ continue;
98
+ }
99
+
100
+ const value = node[key];
101
+ if (Array.isArray(value)) {
102
+ node[key] = value.map((/** @type {any} */ child) => annotate_with_hash(child, hash));
103
+ } else if (value && typeof value === 'object') {
104
+ node[key] = annotate_with_hash(value, hash);
105
+ }
106
+ }
107
+
108
+ return node;
109
+ }
110
+
111
+ /**
112
+ * @param {any} component
113
+ * @param {string} hash
114
+ * @returns {void}
115
+ */
116
+ export function annotate_component_with_hash(component, hash) {
117
+ /** @type {any[]} */
118
+ const body = component.body;
119
+ component.body = body
120
+ .filter((/** @type {any} */ child) => !is_style_element(child))
121
+ .map((/** @type {any} */ child) => annotate_with_hash(child, hash));
122
+ }
123
+
124
+ /**
125
+ * Ensure the element carries a `class` attribute containing the scoping hash.
126
+ *
127
+ * @param {any} element
128
+ * @param {string} hash
129
+ * @returns {void}
130
+ */
131
+ export function add_hash_class(element, hash) {
132
+ const attrs = element.attributes || (element.attributes = []);
133
+ const existing = attrs.find(
134
+ (/** @type {any} */ a) =>
135
+ a.type === 'Attribute' &&
136
+ a.name &&
137
+ a.name.type === 'Identifier' &&
138
+ (a.name.name === 'class' || a.name.name === 'className'),
139
+ );
140
+
141
+ if (!existing) {
142
+ attrs.push({
143
+ type: 'Attribute',
144
+ name: { type: 'Identifier', name: 'class' },
145
+ value: { type: 'Literal', value: hash, raw: JSON.stringify(hash) },
146
+ });
147
+ return;
148
+ }
149
+
150
+ const value = existing.value;
151
+ if (!value) {
152
+ existing.value = { type: 'Literal', value: hash, raw: JSON.stringify(hash) };
153
+ return;
154
+ }
155
+
156
+ if (value.type === 'Literal' && typeof value.value === 'string') {
157
+ const merged = `${value.value} ${hash}`;
158
+ existing.value = { type: 'Literal', value: merged, raw: JSON.stringify(merged) };
159
+ return;
160
+ }
161
+
162
+ // Dynamic expression. Concatenate at runtime via template literal.
163
+ const expression = value.type === 'JSXExpressionContainer' ? value.expression : value;
164
+ existing.value = {
165
+ type: 'TemplateLiteral',
166
+ expressions: [expression],
167
+ quasis: [
168
+ {
169
+ type: 'TemplateElement',
170
+ value: { raw: '', cooked: '' },
171
+ tail: false,
172
+ },
173
+ {
174
+ type: 'TemplateElement',
175
+ value: { raw: ` ${hash}`, cooked: ` ${hash}` },
176
+ tail: true,
177
+ },
178
+ ],
179
+ };
180
+ }
@@ -9,6 +9,7 @@
9
9
  CodeMapping,
10
10
  VolarMappingsResult,
11
11
  PostProcessingChanges,
12
+ LineOffsets,
12
13
  } from '../../types/index';
13
14
  @import { CodeMapping as VolarCodeMapping } from '@volar/language-core';
14
15
  */
@@ -51,6 +52,7 @@ import {
51
52
  mapping_data_verify_complete,
52
53
  build_line_offsets,
53
54
  get_mapping_from_node,
55
+ maybe_get_mapping_from_node,
54
56
  } from '../source-map-utils.js';
55
57
 
56
58
  const LABEL_TO_COMPONENT_REPLACE_REGEX = /(function|\((property|method)\))/;
@@ -668,9 +670,21 @@ export function convert_source_map_to_mappings(
668
670
  return;
669
671
  } else if (node.type === 'JSXExpressionContainer') {
670
672
  if (node.loc) {
671
- mappings.push(
672
- get_mapping_from_node(node, src_to_gen_map, gen_line_offsets, mapping_data_verify_only),
673
+ // Use maybe_get_mapping_from_node because a transform may set the
674
+ // container's loc to the source range of the original `{...}`
675
+ // construct (e.g. a Ripple TSRXExpression or Text node), while
676
+ // esrap only emits a segment for the inner expression. In that
677
+ // case the container's start/end won't resolve — skip rather
678
+ // than hard-failing, and rely on the inner expression's mapping.
679
+ const mapping = maybe_get_mapping_from_node(
680
+ node,
681
+ src_to_gen_map,
682
+ gen_line_offsets,
683
+ mapping_data_verify_only,
673
684
  );
685
+ if (!(mapping instanceof Error)) {
686
+ mappings.push(mapping);
687
+ }
674
688
  }
675
689
  // Visit the expression inside {}
676
690
  if (node.expression) {
@@ -726,24 +740,38 @@ export function convert_source_map_to_mappings(
726
740
  }
727
741
  }
728
742
 
729
- if (closing || opening.selfClosing) {
730
- // Add the whole closing tag or the self-closing
731
- const mapping = get_mapping_from_node(
732
- closing ? closing : opening,
733
- src_to_gen_map,
734
- gen_line_offsets,
735
- mapping_data_verify_only,
736
- );
743
+ if ((closing?.loc || opening.loc) && (closing || opening.selfClosing)) {
744
+ // Add the whole closing tag or the self-closing.
745
+ // For self-closing elements, use maybe_get_mapping_from_node because
746
+ // attribute transforms (e.g. class→className, {ref fn}→ref={fn}) can shift
747
+ // the position of `/>` in the generated output, making the source map
748
+ // entry for the opening element's end position unresolvable.
749
+ const target_node = closing ? closing : opening;
750
+ const mapping = closing
751
+ ? get_mapping_from_node(
752
+ target_node,
753
+ src_to_gen_map,
754
+ gen_line_offsets,
755
+ mapping_data_verify_only,
756
+ )
757
+ : maybe_get_mapping_from_node(
758
+ target_node,
759
+ src_to_gen_map,
760
+ gen_line_offsets,
761
+ mapping_data_verify_only,
762
+ );
737
763
 
738
- // The generated code includes a semicolon after the closing or self-closed tag
739
- // We're extending the mapping to include the semicolon
740
- // because the diagnostics errors can include the whole element
741
- // and we need to account for the semicolon as it's a part of the diagnostic
742
- // At the same time, we could've instead applied this logic to the whole `node` element
743
- // but since we already map the opening - start, we just need the proper end
744
- // and it was causing some issues with mappings
745
- mapping.generatedLengths = [mapping.generatedLengths[0] + 1];
746
- mappings.push(mapping);
764
+ if (!(mapping instanceof Error)) {
765
+ // The generated code includes a semicolon after the closing or self-closed tag
766
+ // We're extending the mapping to include the semicolon
767
+ // because the diagnostics errors can include the whole element
768
+ // and we need to account for the semicolon as it's a part of the diagnostic
769
+ // At the same time, we could've instead applied this logic to the whole `node` element
770
+ // but since we already map the opening - start, we just need the proper end
771
+ // and it was causing some issues with mappings
772
+ mapping.generatedLengths = [mapping.generatedLengths[0] + 1];
773
+ mappings.push(mapping);
774
+ }
747
775
  }
748
776
 
749
777
  if (closing) {
@@ -769,37 +797,52 @@ export function convert_source_map_to_mappings(
769
797
  let start = node_fn.start;
770
798
  const async_keyword = 'async';
771
799
 
772
- if (node_fn.async) {
773
- // We explicitly mapped async and function in esrap
800
+ if (is_component && node_fn.id?.loc) {
801
+ const mapping = get_mapping_from_node(node_fn.id, src_to_gen_map, gen_line_offsets);
802
+ const generated_id_start = mapping.generatedOffsets[0];
803
+ const generated_keyword_start = find_component_keyword_offset(
804
+ generated_code,
805
+ generated_id_start,
806
+ );
807
+ mapping.sourceOffsets = [start];
808
+ mapping.lengths = [source_func_keyword.length];
809
+ mapping.generatedOffsets = [generated_keyword_start];
810
+ mapping.generatedLengths = ['function'.length];
811
+ mapping.data.customData.hover = replace_label_to_component;
812
+ mappings.push(mapping);
813
+ } else {
814
+ if (node_fn.async) {
815
+ // We explicitly mapped async and function in esrap
816
+ tokens.push({
817
+ source: async_keyword,
818
+ generated: async_keyword,
819
+ loc: {
820
+ start: { line: node_fn.loc.start.line, column: start_col },
821
+ end: {
822
+ line: node_fn.loc.start.line,
823
+ column: start_col + async_keyword.length,
824
+ },
825
+ },
826
+ metadata: {},
827
+ });
828
+
829
+ start_col += async_keyword.length + 1; // +1 for space
830
+ start += async_keyword.length + 1;
831
+ }
832
+
774
833
  tokens.push({
775
- source: async_keyword,
776
- generated: async_keyword,
834
+ source: source_func_keyword,
835
+ generated: 'function',
777
836
  loc: {
778
837
  start: { line: node_fn.loc.start.line, column: start_col },
779
838
  end: {
780
839
  line: node_fn.loc.start.line,
781
- column: start_col + async_keyword.length,
840
+ column: start_col + source_func_keyword.length,
782
841
  },
783
842
  },
784
- metadata: {},
843
+ metadata: is_component ? { hover: replace_label_to_component } : {},
785
844
  });
786
-
787
- start_col += async_keyword.length + 1; // +1 for space
788
- start += async_keyword.length + 1;
789
845
  }
790
-
791
- tokens.push({
792
- source: source_func_keyword,
793
- generated: 'function',
794
- loc: {
795
- start: { line: node_fn.loc.start.line, column: start_col },
796
- end: {
797
- line: node_fn.loc.start.line,
798
- column: start_col + source_func_keyword.length,
799
- },
800
- },
801
- metadata: is_component ? { hover: replace_label_to_component } : {},
802
- });
803
846
  }
804
847
 
805
848
  // Visit in source order: id, params, body
@@ -982,9 +1025,11 @@ export function convert_source_map_to_mappings(
982
1025
  visit(node.body);
983
1026
  }
984
1027
 
985
- mappings.push(
986
- get_mapping_from_node(node, src_to_gen_map, gen_line_offsets, mapping_data_verify_only),
987
- );
1028
+ if (node.loc) {
1029
+ mappings.push(
1030
+ get_mapping_from_node(node, src_to_gen_map, gen_line_offsets, mapping_data_verify_only),
1031
+ );
1032
+ }
988
1033
 
989
1034
  return;
990
1035
  } else if (node.type === 'WhileStatement' || node.type === 'DoWhileStatement') {
@@ -1321,9 +1366,11 @@ export function convert_source_map_to_mappings(
1321
1366
  }
1322
1367
  }
1323
1368
 
1324
- mappings.push(
1325
- get_mapping_from_node(node, src_to_gen_map, gen_line_offsets, mapping_data_verify_only),
1326
- );
1369
+ if (node.loc) {
1370
+ mappings.push(
1371
+ get_mapping_from_node(node, src_to_gen_map, gen_line_offsets, mapping_data_verify_only),
1372
+ );
1373
+ }
1327
1374
 
1328
1375
  return;
1329
1376
  } else if (node.type === 'SwitchCase') {
@@ -2030,11 +2077,16 @@ export function convert_source_map_to_mappings(
2030
2077
  );
2031
2078
  const source_length = source_text.length;
2032
2079
  const gen_length = gen_text.length;
2033
- const gen_line_col = get_generated_position(
2034
- token.loc.start.line,
2035
- token.loc.start.column,
2036
- src_to_gen_map,
2037
- );
2080
+ let gen_line_col;
2081
+ try {
2082
+ gen_line_col = get_generated_position(
2083
+ token.loc.start.line,
2084
+ token.loc.start.column,
2085
+ src_to_gen_map,
2086
+ );
2087
+ } catch {
2088
+ continue;
2089
+ }
2038
2090
  const gen_start = loc_to_offset(gen_line_col.line, gen_line_col.column, gen_line_offsets);
2039
2091
 
2040
2092
  /** @type {CustomMappingData} */
@@ -2138,3 +2190,117 @@ export function convert_source_map_to_mappings(
2138
2190
  cssMappings,
2139
2191
  };
2140
2192
  }
2193
+
2194
+ /**
2195
+ * @param {string} generated_code
2196
+ * @param {number} generated_id_start
2197
+ * @returns {number}
2198
+ */
2199
+ function find_component_keyword_offset(generated_code, generated_id_start) {
2200
+ const function_keyword_index = generated_code.lastIndexOf('function', generated_id_start);
2201
+
2202
+ if (function_keyword_index === -1) {
2203
+ return generated_id_start;
2204
+ }
2205
+
2206
+ return function_keyword_index;
2207
+ }
2208
+
2209
+ /**
2210
+ * Build a `VolarMappingsResult` from generated code plus source-map metadata.
2211
+ *
2212
+ * Framework packages are responsible for producing the generated AST/code/map.
2213
+ * Core owns the generic mapping conversion and result envelope so the editor
2214
+ * integration is not coupled to any specific framework package.
2215
+ *
2216
+ * @param {{
2217
+ * ast: AST.Program,
2218
+ * ast_from_source: AST.Program,
2219
+ * source: string,
2220
+ * generated_code: string,
2221
+ * source_map: RawSourceMap,
2222
+ * errors?: import('../../types/index').CompileError[],
2223
+ * post_processing_changes?: PostProcessingChanges,
2224
+ * line_offsets?: LineOffsets,
2225
+ * }} params
2226
+ * @returns {VolarMappingsResult}
2227
+ */
2228
+ export function create_volar_mappings_result({
2229
+ ast,
2230
+ ast_from_source,
2231
+ source,
2232
+ generated_code,
2233
+ source_map,
2234
+ errors = [],
2235
+ post_processing_changes,
2236
+ line_offsets,
2237
+ }) {
2238
+ return {
2239
+ ...convert_source_map_to_mappings(
2240
+ ast,
2241
+ ast_from_source,
2242
+ source,
2243
+ generated_code,
2244
+ source_map,
2245
+ /** @type {PostProcessingChanges} */ (post_processing_changes),
2246
+ line_offsets ?? build_line_offsets(generated_code),
2247
+ ),
2248
+ errors,
2249
+ };
2250
+ }
2251
+
2252
+ /**
2253
+ * Remove byte-for-byte duplicate mappings. Framework compilers that extract
2254
+ * shared helpers or replay JSX can emit identical mapping entries for the
2255
+ * same source and generated span; Volar merges duplicates into a single
2256
+ * hover/navigation result, so deduping upstream avoids a stutter.
2257
+ *
2258
+ * @param {CodeMapping[]} mappings
2259
+ * @returns {CodeMapping[]}
2260
+ */
2261
+ export function dedupe_mappings(mappings) {
2262
+ const deduped = [];
2263
+ const seen = new Set();
2264
+
2265
+ for (const mapping of mappings) {
2266
+ const key = JSON.stringify(serialize_mapping_value(mapping));
2267
+
2268
+ if (seen.has(key)) {
2269
+ continue;
2270
+ }
2271
+
2272
+ seen.add(key);
2273
+ deduped.push(mapping);
2274
+ }
2275
+
2276
+ return deduped;
2277
+ }
2278
+
2279
+ /**
2280
+ * Serialize a mapping (or any nested value) into a stable JSON-friendly
2281
+ * shape so {@link dedupe_mappings} can compare two entries by content.
2282
+ * Object keys are sorted and functions are reduced to their source so
2283
+ * structurally-identical entries produce the same string.
2284
+ *
2285
+ * @param {unknown} value
2286
+ * @returns {unknown}
2287
+ */
2288
+ export function serialize_mapping_value(value) {
2289
+ if (typeof value === 'function') {
2290
+ return value.toString();
2291
+ }
2292
+
2293
+ if (Array.isArray(value)) {
2294
+ return value.map(serialize_mapping_value);
2295
+ }
2296
+
2297
+ if (value && typeof value === 'object') {
2298
+ return Object.fromEntries(
2299
+ Object.entries(value)
2300
+ .sort(([left], [right]) => left.localeCompare(right))
2301
+ .map(([key, nested_value]) => [key, serialize_mapping_value(nested_value)]),
2302
+ );
2303
+ }
2304
+
2305
+ return value;
2306
+ }
package/types/index.d.ts CHANGED
@@ -1387,7 +1387,7 @@ export interface TransformClientState extends BaseState {
1387
1387
  applyParentCssScope?: AST.CSS.StyleSheet['hash'];
1388
1388
  skip_children_traversal: boolean;
1389
1389
  return_flags?: Map<AST.ReturnStatement, { name: string; tracked: boolean }>;
1390
- is_ripple_element?: boolean;
1390
+ is_tsrx_element?: boolean;
1391
1391
  }
1392
1392
 
1393
1393
  /** Override zimmerframe types and provide our own */
package/types/parse.d.ts CHANGED
@@ -182,7 +182,12 @@ export namespace Parse {
182
182
  }
183
183
 
184
184
  export interface Options extends Omit<acorn.Options, 'onComment' | 'ecmaVersion'> {
185
- rippleOptions: {
185
+ tsrxOptions?: {
186
+ loose: boolean;
187
+ errors: CoreCompiler.CompileError[];
188
+ filename: string | undefined;
189
+ };
190
+ rippleOptions?: {
186
191
  loose: boolean;
187
192
  errors: CoreCompiler.CompileError[];
188
193
  filename: string | undefined;