@tsrx/vue 0.0.21 → 0.0.23

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": "Vue compiler built on @tsrx/core",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.0.21",
6
+ "version": "0.0.23",
7
7
  "type": "module",
8
8
  "publishConfig": {
9
9
  "access": "public"
@@ -22,19 +22,20 @@
22
22
  "types": "./types/error-boundary.d.ts",
23
23
  "default": "./src/error-boundary.js"
24
24
  },
25
- "./merge-refs": {
26
- "types": "./types/merge-refs.d.ts",
27
- "default": "./src/merge-refs.js"
25
+ "./ref": {
26
+ "types": "./types/ref.d.ts",
27
+ "default": "./src/ref.js"
28
28
  }
29
29
  },
30
30
  "dependencies": {
31
31
  "esrap": "^2.1.0",
32
+ "is-reference": "^3.0.3",
32
33
  "zimmerframe": "^1.1.2",
33
- "@tsrx/core": "0.0.26"
34
+ "@tsrx/core": "0.0.28"
34
35
  },
35
36
  "peerDependencies": {
36
37
  "vue": ">=3.5",
37
- "vue-jsx-vapor": ">=3.2.10"
38
+ "vue-jsx-vapor": ">=3.2.12"
38
39
  },
39
40
  "devDependencies": {
40
41
  "@types/estree": "^1.0.8",
package/src/index.js CHANGED
@@ -4,6 +4,8 @@
4
4
  import { createVolarMappingsResult, dedupeMappings, parseModule } from '@tsrx/core';
5
5
  import { transform } from './transform.js';
6
6
 
7
+ export { isRefProp } from './ref.js';
8
+
7
9
  /**
8
10
  * Parse tsrx-vue source code to an ESTree AST.
9
11
  * @param {string} source
@@ -22,7 +24,7 @@ export function parse(source, filename, options) {
22
24
  * @param {string} source
23
25
  * @param {string} [filename]
24
26
  * @param {{ collect?: boolean, loose?: boolean }} [options]
25
- * @returns {{ code: string, map: any, css: { code: string, hash: string } | null, errors: CompileError[] }}
27
+ * @returns {{ code: string, map: any, css: string, cssHash: string | null, errors: CompileError[] }}
26
28
  */
27
29
  export function compile(source, filename, options) {
28
30
  const errors = /** @type {CompileError[]} */ ([]);
@@ -63,6 +65,7 @@ export function compile_to_volar_mappings(source, filename, options) {
63
65
  const transformed = transform(ast, source, filename, {
64
66
  collect: true,
65
67
  loose: !!options?.loose,
68
+ typeOnly: true,
66
69
  errors,
67
70
  comments,
68
71
  });
package/src/ref.js ADDED
@@ -0,0 +1 @@
1
+ export * from '@tsrx/core/runtime/ref';
package/src/transform.js CHANGED
@@ -1,14 +1,20 @@
1
1
  /** @import { JsxPlatform } from '@tsrx/core/types' */
2
2
 
3
+ import { walk } from 'zimmerframe';
4
+ import is_reference from 'is-reference';
3
5
  import {
4
6
  builders,
5
7
  clone_expression_node,
6
8
  clone_identifier,
9
+ CREATE_REF_PROP_INTERNAL_NAME,
10
+ create_generated_identifier,
7
11
  componentToFunctionDeclaration,
8
12
  createJsxTransform,
9
13
  error,
10
- identifier_to_jsx_name,
14
+ MERGE_REFS_INTERNAL_NAME,
15
+ NORMALIZE_SPREAD_PROPS_INTERNAL_NAME,
11
16
  setLocation,
17
+ toJsxAttribute,
12
18
  } from '@tsrx/core';
13
19
 
14
20
  /**
@@ -25,12 +31,14 @@ const vue_platform = {
25
31
  imports: {
26
32
  suspense: 'vue',
27
33
  errorBoundary: '@tsrx/vue/error-boundary',
28
- mergeRefs: '@tsrx/vue/merge-refs',
34
+ mergeRefs: '@tsrx/vue/ref',
35
+ refProp: '@tsrx/vue/ref',
29
36
  },
30
37
  jsx: {
31
38
  rewriteClassAttr: false,
32
39
  acceptedTsxKinds: ['vue'],
33
40
  multiRefStrategy: 'merge-refs',
41
+ hostSpreadRefStrategy: 'explicit-ref-attr',
34
42
  },
35
43
  validation: {
36
44
  requireUseServerForAwait: true,
@@ -41,6 +49,7 @@ const vue_platform = {
41
49
  hooks: {
42
50
  initialState: () => ({
43
51
  needs_define_vapor_component: false,
52
+ needs_vapor_for: false,
44
53
  }),
45
54
  isTopLevelSetupCall(call_expression) {
46
55
  return is_vue_setup_call(call_expression);
@@ -55,18 +64,17 @@ const vue_platform = {
55
64
  preprocessElementAttributes(attrs, ctx, element) {
56
65
  return preprocess_ref_attributes(attrs, element, ctx);
57
66
  },
58
- renderForOf: (node, loop_params, body_statements) =>
59
- render_for_of_as_vapor_template(node, loop_params, body_statements),
67
+ transformElementAttributes(attrs, ctx, element) {
68
+ const result = attrs.map((attr) => toJsxAttribute(attr, ctx));
69
+ if (!ctx.typeOnly || is_component_like_element(element)) {
70
+ return result;
71
+ }
72
+ return result.map(mark_type_only_host_ref_attribute);
73
+ },
74
+ renderForOf: (node, loop_params, body_statements, ctx) =>
75
+ render_for_of_as_vapor_for(node, loop_params, body_statements, ctx),
60
76
  createErrorBoundaryContent(try_content) {
61
- return {
62
- type: 'ArrowFunctionExpression',
63
- params: [],
64
- body: try_content.expression,
65
- async: false,
66
- generator: false,
67
- expression: true,
68
- metadata: { path: [] },
69
- };
77
+ return builders.arrow([], try_content.expression);
70
78
  },
71
79
  transformElementChildren(node, walked_children, raw_children, attributes, ctx) {
72
80
  return rewrite_host_text_or_html_children(
@@ -98,6 +106,34 @@ const vue_platform = {
98
106
 
99
107
  export const transform = createJsxTransform(vue_platform);
100
108
 
109
+ /**
110
+ * Vue's `VNodeRef` type is wider than TSRX host refs because it also supports
111
+ * component instances and null teardown values. In editor-only TSX, keep the ref
112
+ * expression unchanged but stop TypeScript verification from reporting that
113
+ * Vue-specific assignability diagnostic on the generated `ref` prop token.
114
+ *
115
+ * @param {any} attr
116
+ * @returns {any}
117
+ */
118
+ function mark_type_only_host_ref_attribute(attr) {
119
+ if (
120
+ !attr ||
121
+ attr.type !== 'JSXAttribute' ||
122
+ attr.name?.type !== 'JSXIdentifier' ||
123
+ attr.name.name !== 'ref'
124
+ ) {
125
+ return attr;
126
+ }
127
+
128
+ return {
129
+ ...attr,
130
+ name: {
131
+ ...attr.name,
132
+ metadata: { ...(attr.name.metadata || {}), disable_verification: true },
133
+ },
134
+ };
135
+ }
136
+
101
137
  /**
102
138
  * @param {any} component
103
139
  * @param {any} transform_context
@@ -128,26 +164,12 @@ function component_to_vapor_component_declaration(component, transform_context,
128
164
  };
129
165
  /** @type {any} */ (component_id.metadata).hover = create_component_hover_replacement(fn.params);
130
166
 
131
- return setLocation(
132
- /** @type {any} */ ({
133
- type: 'VariableDeclaration',
134
- kind: 'const',
135
- declarations: [
136
- {
137
- type: 'VariableDeclarator',
138
- id: component_id,
139
- init: call,
140
- metadata: { path: [] },
141
- },
142
- ],
143
- metadata: {
144
- path: [],
145
- generated_helpers,
146
- generated_statics,
147
- },
148
- }),
149
- component,
150
- );
167
+ const declaration = builders.declaration('const', [builders.declarator(component_id, call)]);
168
+ Object.assign(/** @type {any} */ (declaration.metadata), {
169
+ generated_helpers,
170
+ generated_statics,
171
+ });
172
+ return setLocation(/** @type {any} */ (declaration), component);
151
173
  }
152
174
 
153
175
  /**
@@ -158,24 +180,17 @@ function component_to_vapor_component_declaration(component, transform_context,
158
180
  */
159
181
  function wrap_helper_component(helper_fn, helper_id, source_node) {
160
182
  return setLocation(
161
- /** @type {any} */ ({
162
- type: 'VariableDeclaration',
163
- kind: 'const',
164
- declarations: [
165
- {
166
- type: 'VariableDeclarator',
167
- id: clone_identifier(helper_id),
168
- init: create_define_vapor_component_call(
169
- function_declaration_to_expression(helper_fn),
170
- [],
171
- [],
172
- source_node,
173
- ),
174
- metadata: { path: [] },
175
- },
176
- ],
177
- metadata: { path: [] },
178
- }),
183
+ builders.declaration('const', [
184
+ builders.declarator(
185
+ clone_identifier(helper_id),
186
+ create_define_vapor_component_call(
187
+ function_declaration_to_expression(helper_fn),
188
+ [],
189
+ [],
190
+ source_node,
191
+ ),
192
+ ),
193
+ ]),
179
194
  source_node,
180
195
  );
181
196
  }
@@ -193,33 +208,22 @@ function create_define_vapor_component_call(
193
208
  generated_statics,
194
209
  source_node,
195
210
  ) {
196
- return setLocation(
197
- /** @type {any} */ ({
198
- type: 'CallExpression',
199
- callee: {
200
- type: 'Identifier',
201
- name: 'defineVaporComponent',
202
- metadata: { path: [] },
203
- },
204
- arguments: [fn_expression],
205
- optional: false,
206
- metadata: {
207
- path: [],
208
- generated_helpers,
209
- generated_statics,
210
- },
211
- }),
212
- source_node,
213
- );
211
+ const call = builders.call('defineVaporComponent', fn_expression);
212
+ Object.assign(/** @type {any} */ (call.metadata), {
213
+ generated_helpers,
214
+ generated_statics,
215
+ });
216
+ return setLocation(call, source_node);
214
217
  }
215
218
 
216
219
  /**
217
220
  * @param {any} node
218
221
  * @param {any[]} loop_params
219
222
  * @param {any[]} body_statements
223
+ * @param {any} transform_context
220
224
  * @returns {any | null}
221
225
  */
222
- function render_for_of_as_vapor_template(node, loop_params, body_statements) {
226
+ function render_for_of_as_vapor_for(node, loop_params, body_statements, transform_context) {
223
227
  if (body_statements.length !== 1) {
224
228
  return null;
225
229
  }
@@ -238,49 +242,43 @@ function render_for_of_as_vapor_template(node, loop_params, body_statements) {
238
242
  ? clone_expression_node(node.key)
239
243
  : (find_jsx_key_expression(rendered) ??
240
244
  (node.index ? clone_expression_node(node.index) : null));
241
- strip_top_level_jsx_keys(rendered);
242
- const children = rendered.type === 'JSXFragment' ? rendered.children : [rendered];
245
+
246
+ const slot = key_expression
247
+ ? create_keyed_vapor_for_slot(loop_params, rendered)
248
+ : { params: loop_params, body: rendered, expression: true };
249
+ if (!slot) {
250
+ return null;
251
+ }
252
+
253
+ transform_context.needs_vapor_for = true;
254
+
255
+ if (key_expression) {
256
+ strip_top_level_jsx_keys(slot.body);
257
+ }
258
+
243
259
  const attributes = [
244
- {
245
- type: 'JSXAttribute',
246
- name: { type: 'JSXIdentifier', name: 'v-for', metadata: { path: [] } },
247
- value: to_jsx_expression_container({
248
- type: 'BinaryExpression',
249
- operator: 'in',
250
- left: create_v_for_left(loop_params),
251
- right: clone_expression_node(node.right),
252
- metadata: { path: [] },
253
- }),
254
- metadata: { path: [] },
255
- },
260
+ builders.jsx_attribute(
261
+ builders.jsx_id('in'),
262
+ to_jsx_expression_container(clone_expression_node(node.right)),
263
+ ),
256
264
  ];
257
265
 
258
266
  if (key_expression) {
259
- attributes.push({
260
- type: 'JSXAttribute',
261
- name: { type: 'JSXIdentifier', name: 'key', metadata: { path: [] } },
262
- value: to_jsx_expression_container(key_expression),
263
- metadata: { path: [] },
264
- });
267
+ attributes.push(
268
+ builders.jsx_attribute(
269
+ builders.jsx_id('getKey'),
270
+ to_jsx_expression_container(create_loop_callback(loop_params, key_expression, true)),
271
+ ),
272
+ );
265
273
  }
266
274
 
267
- return to_jsx_expression_container({
268
- type: 'JSXElement',
269
- openingElement: {
270
- type: 'JSXOpeningElement',
271
- name: { type: 'JSXIdentifier', name: 'template', metadata: { path: [] } },
272
- attributes,
273
- selfClosing: false,
274
- metadata: { path: [] },
275
- },
276
- closingElement: {
277
- type: 'JSXClosingElement',
278
- name: { type: 'JSXIdentifier', name: 'template', metadata: { path: [] } },
279
- metadata: { path: [] },
280
- },
281
- children,
282
- metadata: { path: [] },
283
- });
275
+ return to_jsx_expression_container(
276
+ builders.jsx_element_fresh(
277
+ builders.jsx_opening_element(builders.jsx_id('VaporFor'), attributes),
278
+ builders.jsx_closing_element(builders.jsx_id('VaporFor')),
279
+ [to_jsx_expression_container(create_loop_callback(slot.params, slot.body, slot.expression))],
280
+ ),
281
+ );
284
282
  }
285
283
 
286
284
  /**
@@ -290,47 +288,20 @@ function render_for_of_as_vapor_template(node, loop_params, body_statements) {
290
288
  * @returns {any}
291
289
  */
292
290
  function render_for_of_as_flat_map(node, loop_params, rendered) {
293
- return to_jsx_expression_container({
294
- type: 'CallExpression',
295
- callee: {
296
- type: 'MemberExpression',
297
- object: clone_expression_node(node.right),
298
- property: { type: 'Identifier', name: 'flatMap', metadata: { path: [] } },
299
- computed: false,
300
- optional: false,
301
- metadata: { path: [] },
302
- },
303
- arguments: [
304
- {
305
- type: 'ArrowFunctionExpression',
306
- params: loop_params,
307
- body: {
308
- type: 'BlockStatement',
309
- body: [
310
- {
311
- type: 'ReturnStatement',
312
- argument: to_array_render_expression(rendered),
313
- metadata: { path: [] },
314
- },
315
- ],
316
- metadata: { path: [] },
317
- },
318
- async: false,
319
- generator: false,
320
- expression: false,
321
- metadata: { path: [] },
322
- },
323
- ],
324
- async: false,
325
- optional: false,
326
- metadata: { path: [] },
327
- });
291
+ return to_jsx_expression_container(
292
+ builders.call(
293
+ builders.member(clone_expression_node(node.right), 'flatMap'),
294
+ builders.arrow(
295
+ loop_params,
296
+ builders.block([builders.return(to_array_render_expression(rendered))]),
297
+ ),
298
+ ),
299
+ );
328
300
  }
329
301
 
330
302
  /**
331
- * `<template v-for>` preserves one rendered slot per source item in the current
332
- * Vue runtime path. Loop bodies that can return `null` need the shared callback
333
- * lowering so `continue` truly skips the iteration.
303
+ * Loop bodies that can return `null` need the shared callback lowering so
304
+ * `continue` truly skips the iteration.
334
305
  *
335
306
  * @param {any} node
336
307
  * @returns {boolean}
@@ -386,22 +357,6 @@ function to_array_render_expression(node) {
386
357
  return builders.array([node]);
387
358
  }
388
359
 
389
- /**
390
- * @param {any[]} loop_params
391
- * @returns {any}
392
- */
393
- function create_v_for_left(loop_params) {
394
- if (loop_params.length === 1) {
395
- return clone_expression_node(loop_params[0]);
396
- }
397
-
398
- return {
399
- type: 'SequenceExpression',
400
- expressions: loop_params.map((param) => clone_expression_node(param)),
401
- metadata: { path: [] },
402
- };
403
- }
404
-
405
360
  /**
406
361
  * @param {any} node
407
362
  * @returns {any | null}
@@ -450,6 +405,360 @@ function strip_top_level_jsx_keys(node) {
450
405
  }
451
406
  }
452
407
 
408
+ /**
409
+ * @param {any[]} loop_params
410
+ * @param {any} body
411
+ * @param {boolean} expression
412
+ * @returns {any}
413
+ */
414
+ function create_loop_callback(loop_params, body, expression) {
415
+ const callback = builders.arrow(
416
+ loop_params.map((param) => clone_expression_node(param)),
417
+ body,
418
+ );
419
+ callback.expression = expression;
420
+ return callback;
421
+ }
422
+
423
+ /**
424
+ * @param {any[]} loop_params
425
+ * @param {any} rendered
426
+ * @returns {{ params: any[], body: any, expression: boolean } | null}
427
+ */
428
+ function create_keyed_vapor_for_slot(loop_params, rendered) {
429
+ if (loop_params[0]?.type === 'Identifier') {
430
+ return {
431
+ params: loop_params,
432
+ body: rewrite_vapor_for_keyed_slot_refs(rendered, loop_params),
433
+ expression: true,
434
+ };
435
+ }
436
+
437
+ const item_ref = create_generated_identifier('__vapor_item');
438
+ const item_ref_value = create_value_member_expression(item_ref);
439
+ const replacements = create_pattern_replacements(loop_params[0], item_ref_value);
440
+ if (!replacements) {
441
+ return null;
442
+ }
443
+
444
+ const params = [item_ref, ...loop_params.slice(1)];
445
+ const rewritten_rendered = rewrite_vapor_for_keyed_slot_refs(
446
+ rendered,
447
+ loop_params.slice(1),
448
+ replacements,
449
+ );
450
+
451
+ return {
452
+ params,
453
+ body: rewritten_rendered,
454
+ expression: true,
455
+ };
456
+ }
457
+
458
+ /**
459
+ * Vue's `VaporFor` passes plain item values to unkeyed slots, but keyed slots
460
+ * receive shallow refs so row instances can update in place. Match that runtime
461
+ * shape by reading loop params through `.value` inside the slot body.
462
+ *
463
+ * @param {any} node
464
+ * @param {any[]} loop_params
465
+ * @param {Map<string, any>} [replacements]
466
+ * @returns {any}
467
+ */
468
+ function rewrite_vapor_for_keyed_slot_refs(node, loop_params, replacements = new Map()) {
469
+ const loop_param_names = new Set();
470
+ for (const param of loop_params) {
471
+ collect_pattern_names(param, loop_param_names);
472
+ }
473
+
474
+ if (loop_param_names.size === 0 && replacements.size === 0) {
475
+ return node;
476
+ }
477
+
478
+ return walk(
479
+ node,
480
+ { loop_param_names, shadowed_names: new Set() },
481
+ {
482
+ Identifier(identifier, { path, state, next }) {
483
+ const parent = path.at(-1);
484
+ if (
485
+ (state.loop_param_names.has(identifier.name) || replacements.has(identifier.name)) &&
486
+ !state.shadowed_names.has(identifier.name) &&
487
+ parent &&
488
+ is_runtime_reference(identifier, parent)
489
+ ) {
490
+ const replacement = replacements.get(identifier.name);
491
+ if (replacement) {
492
+ return clone_expression_node(replacement);
493
+ }
494
+ return create_value_member_expression(identifier);
495
+ }
496
+
497
+ return next();
498
+ },
499
+ FunctionDeclaration: rewrite_function_shadowed_refs,
500
+ FunctionExpression: rewrite_function_shadowed_refs,
501
+ ArrowFunctionExpression: rewrite_function_shadowed_refs,
502
+ BlockStatement: rewrite_block_shadowed_refs,
503
+ },
504
+ );
505
+ }
506
+
507
+ /**
508
+ * @param {any} identifier
509
+ * @param {any} parent
510
+ * @returns {boolean}
511
+ */
512
+ function is_runtime_reference(identifier, parent) {
513
+ if (parent.type === 'JSXExpressionContainer') {
514
+ return parent.expression === identifier;
515
+ }
516
+ if (parent.type === 'JSXAttribute') {
517
+ return parent.value === identifier || parent.value?.expression === identifier;
518
+ }
519
+ return is_reference(identifier, parent);
520
+ }
521
+
522
+ /**
523
+ * @param {any} pattern
524
+ * @param {any} source
525
+ * @returns {Map<string, any> | null}
526
+ */
527
+ function create_pattern_replacements(pattern, source) {
528
+ const replacements = new Map();
529
+ return collect_pattern_replacements(pattern, source, replacements) ? replacements : null;
530
+ }
531
+
532
+ /**
533
+ * @param {any} pattern
534
+ * @param {any} source
535
+ * @param {Map<string, any>} replacements
536
+ * @returns {boolean}
537
+ */
538
+ function collect_pattern_replacements(pattern, source, replacements) {
539
+ if (!pattern) return true;
540
+
541
+ switch (pattern.type) {
542
+ case 'Identifier':
543
+ replacements.set(pattern.name, source);
544
+ return true;
545
+ case 'ObjectPattern':
546
+ for (const property of pattern.properties || []) {
547
+ if (property.type === 'RestElement' || property.computed) {
548
+ return false;
549
+ }
550
+ if (
551
+ property.type !== 'Property' ||
552
+ !collect_pattern_replacements(
553
+ property.value,
554
+ create_property_member_expression(source, property.key),
555
+ replacements,
556
+ )
557
+ ) {
558
+ return false;
559
+ }
560
+ }
561
+ return true;
562
+ case 'ArrayPattern':
563
+ for (let index = 0; index < (pattern.elements || []).length; index++) {
564
+ const element = pattern.elements[index];
565
+ if (
566
+ element &&
567
+ !collect_pattern_replacements(
568
+ element,
569
+ create_index_member_expression(source, index),
570
+ replacements,
571
+ )
572
+ ) {
573
+ return false;
574
+ }
575
+ }
576
+ return true;
577
+ default:
578
+ return false;
579
+ }
580
+ }
581
+
582
+ /**
583
+ * @param {any} node
584
+ * @param {{ state: { loop_param_names: Set<string>, shadowed_names: Set<string> }, next: (state?: any) => any }} context
585
+ * @returns {any}
586
+ */
587
+ function rewrite_function_shadowed_refs(node, { state, next }) {
588
+ const shadowed_names = new Set(state.shadowed_names);
589
+ if (node.id) {
590
+ collect_pattern_names(node.id, shadowed_names);
591
+ }
592
+ for (const param of node.params || []) {
593
+ collect_pattern_names(param, shadowed_names);
594
+ }
595
+ collect_function_var_names(node.body, shadowed_names);
596
+ return next({ ...state, shadowed_names });
597
+ }
598
+
599
+ /**
600
+ * @param {any} node
601
+ * @param {{ state: { loop_param_names: Set<string>, shadowed_names: Set<string> }, next: (state?: any) => any }} context
602
+ * @returns {any}
603
+ */
604
+ function rewrite_block_shadowed_refs(node, { state, next }) {
605
+ const shadowed_names = new Set(state.shadowed_names);
606
+ collect_block_lexical_names(node.body, shadowed_names);
607
+ return next({ ...state, shadowed_names });
608
+ }
609
+
610
+ /**
611
+ * @param {any[]} statements
612
+ * @param {Set<string>} names
613
+ * @returns {void}
614
+ */
615
+ function collect_block_lexical_names(statements, names) {
616
+ for (const statement of statements || []) {
617
+ if (statement.type === 'VariableDeclaration' && statement.kind !== 'var') {
618
+ for (const declaration of statement.declarations || []) {
619
+ collect_pattern_names(declaration.id, names);
620
+ }
621
+ continue;
622
+ }
623
+
624
+ if (
625
+ (statement.type === 'FunctionDeclaration' || statement.type === 'ClassDeclaration') &&
626
+ statement.id
627
+ ) {
628
+ collect_pattern_names(statement.id, names);
629
+ }
630
+ }
631
+ }
632
+
633
+ /**
634
+ * @param {any} node
635
+ * @param {Set<string>} names
636
+ * @returns {void}
637
+ */
638
+ function collect_function_var_names(node, names) {
639
+ if (!node || typeof node !== 'object') return;
640
+
641
+ if (Array.isArray(node)) {
642
+ for (const child of node) {
643
+ collect_function_var_names(child, names);
644
+ }
645
+ return;
646
+ }
647
+
648
+ if (
649
+ node.type === 'FunctionDeclaration' ||
650
+ node.type === 'FunctionExpression' ||
651
+ node.type === 'ArrowFunctionExpression' ||
652
+ node.type === 'ClassDeclaration' ||
653
+ node.type === 'ClassExpression'
654
+ ) {
655
+ return;
656
+ }
657
+
658
+ if (node.type === 'VariableDeclaration' && node.kind === 'var') {
659
+ for (const declaration of node.declarations || []) {
660
+ collect_pattern_names(declaration.id, names);
661
+ }
662
+ }
663
+
664
+ for (const key of Object.keys(node)) {
665
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
666
+ continue;
667
+ }
668
+ collect_function_var_names(node[key], names);
669
+ }
670
+ }
671
+
672
+ /**
673
+ * @param {any} node
674
+ * @param {Set<string>} names
675
+ * @returns {void}
676
+ */
677
+ function collect_pattern_names(node, names) {
678
+ if (!node) return;
679
+
680
+ switch (node.type) {
681
+ case 'Identifier':
682
+ names.add(node.name);
683
+ break;
684
+ case 'RestElement':
685
+ collect_pattern_names(node.argument, names);
686
+ break;
687
+ case 'AssignmentPattern':
688
+ collect_pattern_names(node.left, names);
689
+ break;
690
+ case 'ArrayPattern':
691
+ for (const element of node.elements || []) {
692
+ collect_pattern_names(element, names);
693
+ }
694
+ break;
695
+ case 'ObjectPattern':
696
+ for (const property of node.properties || []) {
697
+ collect_pattern_names(property, names);
698
+ }
699
+ break;
700
+ case 'Property':
701
+ collect_pattern_names(node.value, names);
702
+ break;
703
+ }
704
+ }
705
+
706
+ /**
707
+ * @param {any} object
708
+ * @param {any} key
709
+ * @returns {any}
710
+ */
711
+ function create_property_member_expression(object, key) {
712
+ if (key?.type === 'Identifier') {
713
+ return create_member_expression(
714
+ clone_expression_node(object),
715
+ clone_identifier(key),
716
+ false,
717
+ key,
718
+ );
719
+ }
720
+
721
+ return create_member_expression(
722
+ clone_expression_node(object),
723
+ clone_expression_node(key),
724
+ true,
725
+ key,
726
+ );
727
+ }
728
+
729
+ /**
730
+ * @param {any} object
731
+ * @param {number} index
732
+ * @returns {any}
733
+ */
734
+ function create_index_member_expression(object, index) {
735
+ return create_member_expression(
736
+ clone_expression_node(object),
737
+ builders.literal(index),
738
+ true,
739
+ object,
740
+ );
741
+ }
742
+
743
+ /**
744
+ * @param {any} identifier
745
+ * @returns {any}
746
+ */
747
+ function create_value_member_expression(identifier) {
748
+ return create_member_expression(clone_identifier(identifier), 'value', false, identifier);
749
+ }
750
+
751
+ /**
752
+ * @param {any} object
753
+ * @param {any} property
754
+ * @param {boolean} computed
755
+ * @param {any} source_node
756
+ * @returns {any}
757
+ */
758
+ function create_member_expression(object, property, computed, source_node) {
759
+ return builders.member(object, property, computed, false, source_node);
760
+ }
761
+
453
762
  /**
454
763
  * @param {any} fn
455
764
  * @returns {any}
@@ -549,6 +858,7 @@ function preprocess_ref_attributes(attrs, element, transform_context) {
549
858
  if (!is_component_like_element(element)) {
550
859
  return attrs;
551
860
  }
861
+ const result = [];
552
862
  for (const attr of attrs) {
553
863
  if (attr?.type === 'RefAttribute') {
554
864
  error(
@@ -559,8 +869,72 @@ function preprocess_ref_attributes(attrs, element, transform_context) {
559
869
  transform_context?.comments,
560
870
  );
561
871
  }
872
+ if (!transform_context.typeOnly && is_vue_named_ref_attribute(attr)) {
873
+ result.push(create_vue_named_ref_spread(attr));
874
+ continue;
875
+ }
876
+ result.push(attr);
877
+ }
878
+ return result;
879
+ }
880
+
881
+ /**
882
+ * Vue's JSX transform treats prop names ending in `ref` as template-ref
883
+ * sugar on components. Keep named TSRX refs as ordinary runtime props by
884
+ * hiding the static prop name behind an object spread before Vue sees the JSX.
885
+ * Type-only virtual TSX skips that spread so Volar can offer completions on
886
+ * the real component prop name.
887
+ *
888
+ * @param {any} attr
889
+ * @returns {boolean}
890
+ */
891
+ function is_vue_named_ref_attribute(attr) {
892
+ const attr_name = get_vue_attribute_name(attr);
893
+ const value = get_vue_attribute_expression(attr);
894
+ return !!(
895
+ attr_name &&
896
+ attr_name !== 'ref' &&
897
+ (attr?.type === 'Attribute' || attr?.type === 'JSXAttribute') &&
898
+ (value?.type === 'RefExpression' ||
899
+ (value?.type === 'CallExpression' &&
900
+ value.callee?.type === 'Identifier' &&
901
+ value.callee.name === CREATE_REF_PROP_INTERNAL_NAME))
902
+ );
903
+ }
904
+
905
+ /**
906
+ * @param {any} attr
907
+ * @returns {any}
908
+ */
909
+ function create_vue_named_ref_spread(attr) {
910
+ const attr_name = get_vue_attribute_name(attr);
911
+ const value = get_vue_attribute_expression(attr);
912
+ if (attr_name === null) return attr;
913
+ const prop = builders.prop('init', builders.key(attr_name), value, false, false);
914
+ return builders.jsx_spread_attribute(builders.object([prop], attr), attr);
915
+ }
916
+
917
+ /**
918
+ * @param {any} attr
919
+ * @returns {string | null}
920
+ */
921
+ function get_vue_attribute_name(attr) {
922
+ if (attr?.type === 'Attribute') {
923
+ return typeof attr.name === 'string' ? attr.name : (attr.name?.name ?? null);
924
+ }
925
+ if (attr?.type === 'JSXAttribute') {
926
+ return attr.name?.type === 'JSXIdentifier' ? attr.name.name : null;
562
927
  }
563
- return attrs;
928
+ return null;
929
+ }
930
+
931
+ /**
932
+ * @param {any} attr
933
+ * @returns {any}
934
+ */
935
+ function get_vue_attribute_expression(attr) {
936
+ const value = attr?.value;
937
+ return value?.type === 'JSXExpressionContainer' ? value.expression : value;
564
938
  }
565
939
 
566
940
  /**
@@ -652,16 +1026,7 @@ function has_dom_content_attribute(attributes, name) {
652
1026
  * @returns {any}
653
1027
  */
654
1028
  function create_jsx_attribute(name, value, source_node) {
655
- return setLocation(
656
- /** @type {any} */ ({
657
- type: 'JSXAttribute',
658
- name: identifier_to_jsx_name(builders.id(name)),
659
- value,
660
- shorthand: false,
661
- metadata: { path: [] },
662
- }),
663
- source_node,
664
- );
1029
+ return builders.jsx_attribute(builders.jsx_id(name), value, false, source_node);
665
1030
  }
666
1031
 
667
1032
  /**
@@ -670,12 +1035,7 @@ function create_jsx_attribute(name, value, source_node) {
670
1035
  * @returns {any}
671
1036
  */
672
1037
  function to_jsx_expression_container(expression, source_node = expression) {
673
- void source_node;
674
- return {
675
- type: 'JSXExpressionContainer',
676
- expression,
677
- metadata: { path: [] },
678
- };
1038
+ return builders.jsx_expression_container(expression, source_node);
679
1039
  }
680
1040
 
681
1041
  /**
@@ -741,6 +1101,10 @@ function inject_vue_imports(program, transform_context) {
741
1101
  ensure_named_import(program, 'vue-jsx-vapor', 'defineVaporComponent');
742
1102
  }
743
1103
 
1104
+ if (transform_context.needs_vapor_for) {
1105
+ ensure_named_import(program, 'vue-jsx-vapor', 'VaporFor');
1106
+ }
1107
+
744
1108
  if (transform_context.needs_suspense) {
745
1109
  ensure_named_import(program, 'vue', 'Suspense');
746
1110
  }
@@ -750,7 +1114,20 @@ function inject_vue_imports(program, transform_context) {
750
1114
  }
751
1115
 
752
1116
  if (transform_context.needs_merge_refs) {
753
- ensure_named_import(program, '@tsrx/vue/merge-refs', 'mergeRefs', '__mergeRefs');
1117
+ ensure_named_import(program, '@tsrx/vue/ref', 'mergeRefs', MERGE_REFS_INTERNAL_NAME);
1118
+ }
1119
+
1120
+ if (transform_context.needs_ref_prop) {
1121
+ ensure_named_import(program, '@tsrx/vue/ref', 'create_ref_prop', CREATE_REF_PROP_INTERNAL_NAME);
1122
+ }
1123
+
1124
+ if (transform_context.needs_normalize_spread_props) {
1125
+ ensure_named_import(
1126
+ program,
1127
+ '@tsrx/vue/ref',
1128
+ 'normalize_spread_props',
1129
+ NORMALIZE_SPREAD_PROPS_INTERNAL_NAME,
1130
+ );
754
1131
  }
755
1132
  }
756
1133
 
@@ -782,7 +1159,7 @@ function ensure_named_import(program, source, name, local = name) {
782
1159
  return;
783
1160
  }
784
1161
 
785
- program.body.unshift(create_import_declaration(source, [create_import_specifier(name, local)]));
1162
+ program.body.unshift(builders.imports([[name, local, 'value']], source));
786
1163
  }
787
1164
 
788
1165
  /**
@@ -799,19 +1176,3 @@ function create_import_specifier(name, local = name) {
799
1176
  metadata: { path: [] },
800
1177
  };
801
1178
  }
802
-
803
- /**
804
- * @param {string} source
805
- * @param {any[]} specifiers
806
- * @returns {any}
807
- */
808
- function create_import_declaration(source, specifiers) {
809
- return {
810
- type: 'ImportDeclaration',
811
- attributes: [],
812
- specifiers,
813
- importKind: 'value',
814
- source: builders.literal(source),
815
- metadata: { path: [] },
816
- };
817
- }
package/types/index.d.ts CHANGED
@@ -1,21 +1,10 @@
1
1
  import type { Program } from 'estree';
2
- import type { CompileError, ParseOptions, VolarMappingsResult } from '@tsrx/core/types';
2
+ import type { CompileFn, ParseOptions, VolarCompileFn } from '@tsrx/core/types';
3
3
 
4
4
  export function parse(source: string, filename?: string, options?: ParseOptions): Program;
5
5
 
6
- export function compile(
7
- source: string,
8
- filename?: string,
9
- options?: { collect?: boolean; loose?: boolean },
10
- ): {
11
- code: string;
12
- map: unknown;
13
- css: { code: string; hash: string } | null;
14
- errors: CompileError[];
15
- };
6
+ export { isRefProp } from './ref.js';
16
7
 
17
- export function compile_to_volar_mappings(
18
- source: string,
19
- filename?: string,
20
- options?: ParseOptions,
21
- ): VolarMappingsResult;
8
+ export const compile: CompileFn;
9
+
10
+ export const compile_to_volar_mappings: VolarCompileFn;
package/types/ref.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from '@tsrx/core/runtime/ref';
package/src/merge-refs.js DELETED
@@ -1 +0,0 @@
1
- export { mergeRefs } from '@tsrx/core/runtime/merge-refs';
@@ -1 +0,0 @@
1
- export { mergeRefs } from '@tsrx/core/runtime/merge-refs';