@tsrx/vue 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Dominic Gannaway
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@tsrx/vue",
3
+ "description": "Vue compiler built on @tsrx/core",
4
+ "license": "MIT",
5
+ "author": "Dominic Gannaway",
6
+ "version": "0.0.10",
7
+ "type": "module",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/Ripple-TS/ripple.git",
14
+ "directory": "packages/tsrx-vue"
15
+ },
16
+ "exports": {
17
+ ".": {
18
+ "types": "./types/index.d.ts",
19
+ "default": "./src/index.js"
20
+ },
21
+ "./error-boundary": {
22
+ "default": "./src/error-boundary.js"
23
+ }
24
+ },
25
+ "dependencies": {
26
+ "esrap": "^2.1.0",
27
+ "zimmerframe": "^1.1.2",
28
+ "@tsrx/core": "0.0.15"
29
+ },
30
+ "peerDependencies": {
31
+ "vue": ">=3.5",
32
+ "vue-jsx-vapor": ">=3.2.10"
33
+ },
34
+ "devDependencies": {
35
+ "@types/estree": "^1.0.8",
36
+ "@types/estree-jsx": "^1.0.5",
37
+ "typescript": "^5.9.3"
38
+ },
39
+ "files": [
40
+ "src",
41
+ "types"
42
+ ]
43
+ }
@@ -0,0 +1,173 @@
1
+ import * as Vue from 'vue';
2
+ import { getCurrentInstance, onErrorCaptured, shallowRef } from 'vue';
3
+
4
+ const boundary_states = new WeakMap();
5
+
6
+ /** @typedef {any} BoundaryValue */
7
+
8
+ /**
9
+ * @param {BoundaryValue} nodes
10
+ * @param {Node} [anchor]
11
+ * @returns {BoundaryValue}
12
+ */
13
+ function create_fragment(nodes, anchor = document.createTextNode('')) {
14
+ const fragment = new /** @type {any} */ (Vue).VaporFragment(nodes);
15
+ fragment.anchor = anchor;
16
+ return fragment;
17
+ }
18
+
19
+ /**
20
+ * @param {BoundaryValue} value
21
+ * @returns {void}
22
+ */
23
+ function track_dynamic_values(value) {
24
+ if (!value || typeof value !== 'object') return;
25
+
26
+ for (const key of Object.keys(value)) {
27
+ const child = value[key];
28
+ if (key === 'content' || key === 'fallback' || key === 'children' || key === 'default')
29
+ continue;
30
+ if (key === '$' || key.startsWith('on')) continue;
31
+ if (typeof child === 'function') {
32
+ child();
33
+ }
34
+ }
35
+ }
36
+
37
+ /**
38
+ * @param {BoundaryValue} node
39
+ * @param {Node | undefined} anchor
40
+ * @returns {BoundaryValue}
41
+ */
42
+ function normalize_block(node, anchor) {
43
+ if (node instanceof Node || /** @type {any} */ (Vue).isFragment(node)) return node;
44
+ if (/** @type {any} */ (Vue).isVaporComponent(node)) {
45
+ if (!(node.rawProps && 'content' in node.rawProps && 'fallback' in node.rawProps)) {
46
+ track_dynamic_values(node.rawProps);
47
+ }
48
+ return create_fragment(node, anchor);
49
+ }
50
+ if (Array.isArray(node))
51
+ return create_fragment(
52
+ node.map((item) => normalize_block(item, undefined)),
53
+ anchor,
54
+ );
55
+
56
+ const result = node == null || typeof node === 'boolean' ? '' : String(node);
57
+ if (anchor) {
58
+ anchor.textContent = result;
59
+ return anchor;
60
+ }
61
+ return document.createTextNode(result);
62
+ }
63
+
64
+ /**
65
+ * @param {BoundaryValue} current
66
+ * @param {BoundaryValue} value
67
+ * @param {Node | undefined} anchor
68
+ * @returns {BoundaryValue}
69
+ */
70
+ function resolve_value(current, value, anchor) {
71
+ anchor = anchor || (current instanceof Node && current.nodeType === 3 ? current : undefined);
72
+ const node = normalize_block(value, anchor);
73
+
74
+ if (current) {
75
+ if (/** @type {any} */ (Vue).isFragment(current)) {
76
+ if (current.anchor && current.anchor.parentNode) {
77
+ /** @type {any} */ (Vue).remove(current.nodes, current.anchor.parentNode);
78
+ /** @type {any} */ (Vue).insert(node, current.anchor.parentNode, current.anchor);
79
+ if (current.scope) current.scope.stop();
80
+ }
81
+ } else if (current instanceof Node) {
82
+ if (current.nodeType === 3 && (!(node instanceof Node) || node.nodeType !== 3)) {
83
+ current.textContent = '';
84
+ }
85
+ if (/** @type {any} */ (Vue).isFragment(node) && current.parentNode) {
86
+ /** @type {any} */ (Vue).insert(node, current.parentNode, current);
87
+ if (current.nodeType !== 3) current.parentNode.removeChild(current);
88
+ } else if (node instanceof Node) {
89
+ if (current.nodeType === 3 && node.nodeType === 3) {
90
+ current.textContent = node.textContent;
91
+ return current;
92
+ }
93
+ if (current.parentNode) current.parentNode.replaceChild(node, current);
94
+ }
95
+ }
96
+ }
97
+
98
+ return node;
99
+ }
100
+
101
+ /**
102
+ * @param {() => BoundaryValue} render
103
+ * @returns {BoundaryValue[]}
104
+ */
105
+ function create_boundary_nodes(render) {
106
+ /** @type {BoundaryValue[]} */
107
+ const nodes = [];
108
+ /** @type {any} */
109
+ let scope;
110
+
111
+ /** @type {any} */ (Vue).renderEffect(() => {
112
+ if (scope) scope.stop();
113
+ scope = new /** @type {any} */ (Vue).EffectScope();
114
+ nodes[0] = scope.run(() => resolve_value(nodes[0], render(), undefined));
115
+ });
116
+
117
+ return nodes;
118
+ }
119
+
120
+ /**
121
+ * A reusable Vue error boundary component.
122
+ *
123
+ * Used by the `@tsrx/vue` compiler to implement `try/catch` blocks.
124
+ * The `fallback` prop receives the caught error and a `reset` function
125
+ * that clears the error state to re-render the children.
126
+ */
127
+ /**
128
+ * @param {{ content: () => any, fallback: (error: unknown, reset: () => void) => any }} props
129
+ * @returns {any}
130
+ */
131
+ export function TsrxErrorBoundary(props) {
132
+ const instance = getCurrentInstance();
133
+ const state = instance ? boundary_states.get(instance) : undefined;
134
+ const error = state?.error ?? shallowRef(/** @type {unknown} */ (null));
135
+ const reset =
136
+ state?.reset ??
137
+ (() => {
138
+ error.value = null;
139
+ });
140
+
141
+ return create_boundary_nodes(() => {
142
+ if (error.value !== null) {
143
+ return props.fallback(error.value, reset);
144
+ }
145
+
146
+ try {
147
+ return props.content();
148
+ } catch (caught_error) {
149
+ error.value = caught_error;
150
+ return props.fallback(caught_error, reset);
151
+ }
152
+ });
153
+ }
154
+
155
+ /** @returns {void} */
156
+ TsrxErrorBoundary.__setup = function setup() {
157
+ const instance = getCurrentInstance();
158
+ if (!instance || boundary_states.has(instance)) {
159
+ return;
160
+ }
161
+
162
+ const error = shallowRef(/** @type {unknown} */ (null));
163
+ const reset = () => {
164
+ error.value = null;
165
+ };
166
+
167
+ boundary_states.set(instance, { error, reset });
168
+
169
+ onErrorCaptured((captured_error) => {
170
+ error.value = captured_error;
171
+ return false;
172
+ });
173
+ };
package/src/index.js ADDED
@@ -0,0 +1,60 @@
1
+ /** @import * as AST from 'estree' */
2
+ /** @import { CompileError, ParseOptions } from '@tsrx/core/types' */
3
+
4
+ import { createVolarMappingsResult, dedupeMappings, parseModule } from '@tsrx/core';
5
+ import { transform } from './transform.js';
6
+
7
+ /**
8
+ * Parse tsrx-vue source code to an ESTree AST.
9
+ * @param {string} source
10
+ * @param {string} [filename]
11
+ * @param {ParseOptions} [options]
12
+ * @returns {AST.Program}
13
+ */
14
+ export function parse(source, filename, options) {
15
+ return parseModule(source, filename, options);
16
+ }
17
+
18
+ /**
19
+ * Compile tsrx-vue source code to a TSX module suitable for consumption by
20
+ * vue-jsx-vapor or another Vue JSX transform.
21
+ *
22
+ * @param {string} source
23
+ * @param {string} [filename]
24
+ * @param {{ loose?: boolean }} [options]
25
+ * @returns {{ code: string, map: any, css: { code: string, hash: string } | null, errors: CompileError[] }}
26
+ */
27
+ export function compile(source, filename, options) {
28
+ const errors = /** @type {CompileError[]} */ ([]);
29
+ const collect = !!options?.loose;
30
+ const ast = parseModule(source, filename, collect ? { loose: true, errors } : undefined);
31
+ const { ast: _ast, ...result } = transform(ast, source, filename);
32
+ return { ...result, errors };
33
+ }
34
+
35
+ /**
36
+ * Compile tsrx-vue source to virtual TSX plus Volar mappings for editor tooling.
37
+ *
38
+ * @param {string} source
39
+ * @param {string} [filename]
40
+ * @param {ParseOptions} [options]
41
+ * @returns {import('@tsrx/core/types').VolarMappingsResult}
42
+ */
43
+ export function compile_to_volar_mappings(source, filename, options) {
44
+ const errors = /** @type {import('@tsrx/core/types').CompileError[]} */ ([]);
45
+ const ast = parseModule(source, filename, { ...options, errors });
46
+ const transformed = transform(ast, source, filename);
47
+ const result = createVolarMappingsResult({
48
+ ast: transformed.ast,
49
+ ast_from_source: ast,
50
+ source,
51
+ generated_code: transformed.code,
52
+ source_map: transformed.map,
53
+ errors,
54
+ });
55
+
56
+ return {
57
+ ...result,
58
+ mappings: dedupeMappings(result.mappings),
59
+ };
60
+ }
@@ -0,0 +1,714 @@
1
+ /** @import { JsxPlatform } from '@tsrx/core/types' */
2
+
3
+ import {
4
+ builders,
5
+ clone_expression_node,
6
+ clone_identifier,
7
+ componentToFunctionDeclaration,
8
+ createJsxTransform,
9
+ create_compile_error,
10
+ identifier_to_jsx_name,
11
+ setLocation,
12
+ } from '@tsrx/core';
13
+
14
+ /**
15
+ * Minimal Vue platform descriptor consumed by `createJsxTransform`.
16
+ *
17
+ * Vue largely reuses the shared JSX lowering while wrapping compiled
18
+ * components in `defineVaporComponent(...)` and handling its extra imports.
19
+ * Async component bodies still stay explicitly unsupported.
20
+ *
21
+ * @type {JsxPlatform}
22
+ */
23
+ const vue_platform = {
24
+ name: 'Vue',
25
+ imports: {
26
+ suspense: 'vue',
27
+ errorBoundary: '@tsrx/vue/error-boundary',
28
+ },
29
+ jsx: {
30
+ rewriteClassAttr: false,
31
+ acceptedTsxKinds: ['vue'],
32
+ },
33
+ validation: {
34
+ requireUseServerForAwait: true,
35
+ scanUseServerDirectiveForAwaitWithCustomValidator: false,
36
+ unsupportedTryPendingMessage:
37
+ 'Vue TSRX does not support `pending` blocks in component templates yet. Vue Suspense uses fallback slots rather than a `fallback` prop, so `try { ... } pending { ... }` cannot be lowered correctly for this target yet.',
38
+ },
39
+ hooks: {
40
+ initialState: () => ({
41
+ needs_define_vapor_component: false,
42
+ }),
43
+ isTopLevelSetupCall(call_expression) {
44
+ return is_vue_setup_call(call_expression);
45
+ },
46
+ wrapHelperComponent(helper_fn, helper_id, ctx, source_node) {
47
+ ctx.needs_define_vapor_component = true;
48
+ return wrap_helper_component(helper_fn, helper_id, source_node);
49
+ },
50
+ canHoistStaticNode(node) {
51
+ return !contains_component_jsx(node);
52
+ },
53
+ preprocessElementAttributes(attrs, ctx, element) {
54
+ return preprocess_ref_attributes(attrs, element, ctx);
55
+ },
56
+ renderForOf: (node, loop_params, body_statements) =>
57
+ render_for_of_as_vapor_template(node, loop_params, body_statements),
58
+ createErrorBoundaryContent(try_content) {
59
+ return {
60
+ type: 'ArrowFunctionExpression',
61
+ params: [],
62
+ body: try_content.expression,
63
+ async: false,
64
+ generator: false,
65
+ expression: true,
66
+ metadata: { path: [] },
67
+ };
68
+ },
69
+ transformElementChildren(node, walked_children, raw_children, attributes) {
70
+ return rewrite_host_text_or_html_children(node, walked_children, raw_children, attributes);
71
+ },
72
+ validateComponentAwait(await_expression) {
73
+ throw create_compile_error(
74
+ await_expression,
75
+ '`await` is not yet supported in Vue TSRX components.',
76
+ );
77
+ },
78
+ componentToFunction(component, ctx, helper_state) {
79
+ ctx.needs_define_vapor_component = true;
80
+ return component_to_vapor_component_declaration(component, ctx, helper_state);
81
+ },
82
+ injectImports(program, ctx) {
83
+ inject_vue_imports(program, ctx);
84
+ },
85
+ },
86
+ };
87
+
88
+ export const transform = createJsxTransform(vue_platform);
89
+
90
+ /**
91
+ * @param {any} component
92
+ * @param {any} transform_context
93
+ * @param {any} helper_state
94
+ * @returns {any}
95
+ */
96
+ function component_to_vapor_component_declaration(component, transform_context, helper_state) {
97
+ const fn = componentToFunctionDeclaration(component, transform_context, helper_state);
98
+ const generated_helpers = helper_state?.helpers || [];
99
+ const generated_statics = helper_state?.statics || [];
100
+ const call = create_define_vapor_component_call(
101
+ function_declaration_to_expression(fn),
102
+ generated_helpers,
103
+ generated_statics,
104
+ component,
105
+ );
106
+
107
+ if (component.default || !component.id) {
108
+ return call;
109
+ }
110
+
111
+ const component_id = clone_identifier(component.id);
112
+ component_id.metadata = {
113
+ ...component_id.metadata,
114
+ ...(fn.id?.metadata || {}),
115
+ path: component_id.metadata?.path || [],
116
+ };
117
+ /** @type {any} */ (component_id.metadata).hover = create_component_hover_replacement(fn.params);
118
+
119
+ return setLocation(
120
+ /** @type {any} */ ({
121
+ type: 'VariableDeclaration',
122
+ kind: 'const',
123
+ declarations: [
124
+ {
125
+ type: 'VariableDeclarator',
126
+ id: component_id,
127
+ init: call,
128
+ metadata: { path: [] },
129
+ },
130
+ ],
131
+ metadata: {
132
+ path: [],
133
+ generated_helpers,
134
+ generated_statics,
135
+ },
136
+ }),
137
+ component,
138
+ );
139
+ }
140
+
141
+ /**
142
+ * @param {any} helper_fn
143
+ * @param {any} helper_id
144
+ * @param {any} source_node
145
+ * @returns {any}
146
+ */
147
+ function wrap_helper_component(helper_fn, helper_id, source_node) {
148
+ return setLocation(
149
+ /** @type {any} */ ({
150
+ type: 'VariableDeclaration',
151
+ kind: 'const',
152
+ declarations: [
153
+ {
154
+ type: 'VariableDeclarator',
155
+ id: clone_identifier(helper_id),
156
+ init: create_define_vapor_component_call(
157
+ function_declaration_to_expression(helper_fn),
158
+ [],
159
+ [],
160
+ source_node,
161
+ ),
162
+ metadata: { path: [] },
163
+ },
164
+ ],
165
+ metadata: { path: [] },
166
+ }),
167
+ source_node,
168
+ );
169
+ }
170
+
171
+ /**
172
+ * @param {any} fn_expression
173
+ * @param {any[]} generated_helpers
174
+ * @param {any[]} generated_statics
175
+ * @param {any} source_node
176
+ * @returns {any}
177
+ */
178
+ function create_define_vapor_component_call(
179
+ fn_expression,
180
+ generated_helpers,
181
+ generated_statics,
182
+ source_node,
183
+ ) {
184
+ return setLocation(
185
+ /** @type {any} */ ({
186
+ type: 'CallExpression',
187
+ callee: {
188
+ type: 'Identifier',
189
+ name: 'defineVaporComponent',
190
+ metadata: { path: [] },
191
+ },
192
+ arguments: [fn_expression],
193
+ optional: false,
194
+ metadata: {
195
+ path: [],
196
+ generated_helpers,
197
+ generated_statics,
198
+ },
199
+ }),
200
+ source_node,
201
+ );
202
+ }
203
+
204
+ /**
205
+ * @param {any} node
206
+ * @param {any[]} loop_params
207
+ * @param {any[]} body_statements
208
+ * @returns {any | null}
209
+ */
210
+ function render_for_of_as_vapor_template(node, loop_params, body_statements) {
211
+ if (body_statements.length !== 1) {
212
+ return null;
213
+ }
214
+
215
+ const statement = body_statements[0];
216
+ if (statement?.type !== 'ReturnStatement' || !statement.argument) {
217
+ return null;
218
+ }
219
+
220
+ const rendered = statement.argument;
221
+ const key_expression = node.key
222
+ ? clone_expression_node(node.key)
223
+ : find_jsx_key_expression(rendered);
224
+ strip_top_level_jsx_keys(rendered);
225
+ const children = rendered.type === 'JSXFragment' ? rendered.children : [rendered];
226
+ const attributes = [
227
+ {
228
+ type: 'JSXAttribute',
229
+ name: { type: 'JSXIdentifier', name: 'v-for', metadata: { path: [] } },
230
+ value: to_jsx_expression_container({
231
+ type: 'BinaryExpression',
232
+ operator: 'in',
233
+ left: create_v_for_left(loop_params),
234
+ right: clone_expression_node(node.right),
235
+ metadata: { path: [] },
236
+ }),
237
+ metadata: { path: [] },
238
+ },
239
+ ];
240
+
241
+ if (key_expression) {
242
+ attributes.push({
243
+ type: 'JSXAttribute',
244
+ name: { type: 'JSXIdentifier', name: 'key', metadata: { path: [] } },
245
+ value: to_jsx_expression_container(key_expression),
246
+ metadata: { path: [] },
247
+ });
248
+ }
249
+
250
+ return to_jsx_expression_container({
251
+ type: 'JSXElement',
252
+ openingElement: {
253
+ type: 'JSXOpeningElement',
254
+ name: { type: 'JSXIdentifier', name: 'template', metadata: { path: [] } },
255
+ attributes,
256
+ selfClosing: false,
257
+ metadata: { path: [] },
258
+ },
259
+ closingElement: {
260
+ type: 'JSXClosingElement',
261
+ name: { type: 'JSXIdentifier', name: 'template', metadata: { path: [] } },
262
+ metadata: { path: [] },
263
+ },
264
+ children,
265
+ metadata: { path: [] },
266
+ });
267
+ }
268
+
269
+ /**
270
+ * @param {any[]} loop_params
271
+ * @returns {any}
272
+ */
273
+ function create_v_for_left(loop_params) {
274
+ if (loop_params.length === 1) {
275
+ return clone_expression_node(loop_params[0]);
276
+ }
277
+
278
+ return {
279
+ type: 'SequenceExpression',
280
+ expressions: loop_params.map((param) => clone_expression_node(param)),
281
+ metadata: { path: [] },
282
+ };
283
+ }
284
+
285
+ /**
286
+ * @param {any} node
287
+ * @returns {any | null}
288
+ */
289
+ function find_jsx_key_expression(node) {
290
+ if (node?.type !== 'JSXElement') {
291
+ return null;
292
+ }
293
+
294
+ for (const attr of node.openingElement?.attributes || []) {
295
+ if (
296
+ attr.type === 'JSXAttribute' &&
297
+ attr.name?.type === 'JSXIdentifier' &&
298
+ attr.name.name === 'key'
299
+ ) {
300
+ return attr.value?.type === 'JSXExpressionContainer'
301
+ ? clone_expression_node(attr.value.expression)
302
+ : clone_expression_node(attr.value);
303
+ }
304
+ }
305
+
306
+ return null;
307
+ }
308
+
309
+ /**
310
+ * @param {any} node
311
+ * @returns {void}
312
+ */
313
+ function strip_top_level_jsx_keys(node) {
314
+ if (node?.type === 'JSXElement') {
315
+ node.openingElement.attributes = (node.openingElement.attributes || []).filter(
316
+ (/** @type {any} */ attr) =>
317
+ !(
318
+ attr.type === 'JSXAttribute' &&
319
+ attr.name?.type === 'JSXIdentifier' &&
320
+ attr.name.name === 'key'
321
+ ),
322
+ );
323
+ return;
324
+ }
325
+
326
+ if (node?.type === 'JSXFragment') {
327
+ for (const child of node.children || []) {
328
+ strip_top_level_jsx_keys(child);
329
+ }
330
+ }
331
+ }
332
+
333
+ /**
334
+ * @param {any} fn
335
+ * @returns {any}
336
+ */
337
+ function function_declaration_to_expression(fn) {
338
+ return {
339
+ ...fn,
340
+ type: 'FunctionExpression',
341
+ metadata: {
342
+ ...(fn.metadata || {}),
343
+ path: fn.metadata?.path || [],
344
+ },
345
+ };
346
+ }
347
+
348
+ const VUE_COMPONENT_HOVER_LABEL_REGEX = /(function|\((property|method)\))/;
349
+
350
+ /**
351
+ * @param {any[]} [params]
352
+ * @returns {(content: string) => string}
353
+ */
354
+ function create_component_hover_replacement(params) {
355
+ const lazy_param_regexes = (params || [])
356
+ .filter((param) => param.type === 'Identifier' && /^__lazy\d+$/.test(param.name))
357
+ .map((param) => new RegExp(`\\b${param.name}\\s*:\\s*`, 'g'));
358
+
359
+ return (content) => {
360
+ let next = content.replace(VUE_COMPONENT_HOVER_LABEL_REGEX, (_, fn, kind) => {
361
+ if (fn === 'function') return 'component';
362
+ return `(component ${kind})`;
363
+ });
364
+ for (const regex of lazy_param_regexes) {
365
+ next = next.replace(regex, '&');
366
+ }
367
+ return next;
368
+ };
369
+ }
370
+
371
+ const VUE_SETUP_CALLS = new Set([
372
+ 'ref',
373
+ 'shallowRef',
374
+ 'computed',
375
+ 'reactive',
376
+ 'shallowReactive',
377
+ 'customRef',
378
+ 'toRef',
379
+ 'toRefs',
380
+ 'useTemplateRef',
381
+ ]);
382
+
383
+ /**
384
+ * @param {any} call_expression
385
+ * @returns {boolean}
386
+ */
387
+ function is_vue_setup_call(call_expression) {
388
+ const callee = call_expression?.callee;
389
+ if (!callee) return false;
390
+
391
+ if (callee.type === 'Identifier') {
392
+ return VUE_SETUP_CALLS.has(callee.name);
393
+ }
394
+
395
+ if (
396
+ callee.type === 'MemberExpression' &&
397
+ callee.computed === false &&
398
+ callee.property?.type === 'Identifier'
399
+ ) {
400
+ return VUE_SETUP_CALLS.has(callee.property.name);
401
+ }
402
+
403
+ return false;
404
+ }
405
+
406
+ /**
407
+ * @param {any[]} attrs
408
+ * @param {any} element
409
+ * @param {any} transform_context
410
+ * @returns {any[]}
411
+ */
412
+ function preprocess_ref_attributes(attrs, element, transform_context) {
413
+ /** @type {any[]} */
414
+ const result = [];
415
+ /** @type {any[]} */
416
+ const ref_attrs = [];
417
+
418
+ for (const attr of attrs) {
419
+ if (!attr) continue;
420
+ if (attr.type === 'RefAttribute') {
421
+ ref_attrs.push(attr);
422
+ continue;
423
+ }
424
+ result.push(attr);
425
+ }
426
+
427
+ if (ref_attrs.length > 0 && is_component_like_element(element)) {
428
+ throw create_compile_error(
429
+ ref_attrs[0],
430
+ '`{ref ...}` on the Vue target is only supported on host elements. Vue component refs resolve to component instances rather than the rendered DOM node, so Ripple-style component refs are not supported here.',
431
+ );
432
+ }
433
+
434
+ if (ref_attrs.length === 1) {
435
+ result.push(ref_attrs[0]);
436
+ } else if (ref_attrs.length > 1) {
437
+ result.push({
438
+ type: 'RefAttribute',
439
+ argument: create_combined_ref_callback(ref_attrs),
440
+ loc: ref_attrs[0].loc,
441
+ metadata: { path: [] },
442
+ });
443
+ }
444
+
445
+ return result;
446
+ }
447
+
448
+ /**
449
+ * @param {any[]} ref_attrs
450
+ * @returns {any}
451
+ */
452
+ function create_combined_ref_callback(ref_attrs) {
453
+ const node_id = builders.id('node');
454
+
455
+ return {
456
+ type: 'ArrowFunctionExpression',
457
+ params: [node_id],
458
+ body: {
459
+ type: 'BlockStatement',
460
+ body: ref_attrs.map((attr) => ({
461
+ type: 'ExpressionStatement',
462
+ expression: {
463
+ type: 'CallExpression',
464
+ callee: attr.argument,
465
+ arguments: [clone_identifier(node_id)],
466
+ optional: false,
467
+ metadata: { path: [] },
468
+ },
469
+ metadata: { path: [] },
470
+ })),
471
+ metadata: { path: [] },
472
+ },
473
+ expression: false,
474
+ async: false,
475
+ generator: false,
476
+ metadata: { path: [] },
477
+ };
478
+ }
479
+
480
+ /**
481
+ * @param {any} node
482
+ * @param {any[]} walked_children
483
+ * @param {any[]} raw_children
484
+ * @param {any[]} attributes
485
+ * @returns {{ children: any[]; selfClosing?: boolean } | null}
486
+ */
487
+ function rewrite_host_text_or_html_children(node, walked_children, raw_children, attributes) {
488
+ const source_children = raw_children || walked_children;
489
+ const is_composite = is_component_like_element(node);
490
+ const html_children = source_children.filter((child) => child?.type === 'Html');
491
+
492
+ if (html_children.length > 0) {
493
+ if (
494
+ is_composite ||
495
+ source_children.length !== 1 ||
496
+ has_dom_content_attribute(attributes, 'innerHTML') ||
497
+ has_dom_content_attribute(attributes, 'textContent')
498
+ ) {
499
+ throw create_compile_error(
500
+ html_children[0],
501
+ '`{html ...}` on the Vue target is only supported as the sole child of a host element. Use `innerHTML={...}` as an element attribute when you need the explicit prop form.',
502
+ );
503
+ }
504
+
505
+ const walked_html = walked_children[0] || html_children[0];
506
+ attributes.push(
507
+ create_jsx_attribute(
508
+ 'innerHTML',
509
+ to_jsx_expression_container(walked_html.expression, html_children[0]),
510
+ html_children[0],
511
+ ),
512
+ );
513
+
514
+ return { children: [], selfClosing: true };
515
+ }
516
+
517
+ if (!is_composite && source_children.length === 1 && source_children[0]?.type === 'Text') {
518
+ return null;
519
+ }
520
+
521
+ return null;
522
+ }
523
+
524
+ /**
525
+ * @param {any} node
526
+ * @returns {boolean}
527
+ */
528
+ function is_component_like_element(node) {
529
+ const id = node?.id;
530
+ if (!id) return false;
531
+ if (id.type === 'Identifier') return /^[A-Z]/.test(id.name);
532
+ if (id.type === 'MemberExpression') return true;
533
+ return false;
534
+ }
535
+
536
+ /**
537
+ * @param {any[]} attributes
538
+ * @param {string} name
539
+ * @returns {boolean}
540
+ */
541
+ function has_dom_content_attribute(attributes, name) {
542
+ return attributes.some(
543
+ (/** @type {any} */ attribute) =>
544
+ attribute &&
545
+ (attribute.type === 'JSXSpreadAttribute' ||
546
+ (attribute.type === 'JSXAttribute' &&
547
+ attribute.name?.type === 'JSXIdentifier' &&
548
+ attribute.name.name === name)),
549
+ );
550
+ }
551
+
552
+ /**
553
+ * @param {string} name
554
+ * @param {any} value
555
+ * @param {any} source_node
556
+ * @returns {any}
557
+ */
558
+ function create_jsx_attribute(name, value, source_node) {
559
+ return setLocation(
560
+ /** @type {any} */ ({
561
+ type: 'JSXAttribute',
562
+ name: identifier_to_jsx_name(builders.id(name)),
563
+ value,
564
+ shorthand: false,
565
+ metadata: { path: [] },
566
+ }),
567
+ source_node,
568
+ );
569
+ }
570
+
571
+ /**
572
+ * @param {any} expression
573
+ * @param {any} source_node
574
+ * @returns {any}
575
+ */
576
+ function to_jsx_expression_container(expression, source_node = expression) {
577
+ void source_node;
578
+ return {
579
+ type: 'JSXExpressionContainer',
580
+ expression,
581
+ metadata: { path: [] },
582
+ };
583
+ }
584
+
585
+ /**
586
+ * @param {any} node
587
+ * @returns {boolean}
588
+ */
589
+ function contains_component_jsx(node) {
590
+ if (!node || typeof node !== 'object') {
591
+ return false;
592
+ }
593
+
594
+ if (node.type === 'JSXElement') {
595
+ if (is_component_jsx_name(node.openingElement?.name)) {
596
+ return true;
597
+ }
598
+
599
+ return node.children.some(contains_component_jsx);
600
+ }
601
+
602
+ if (node.type === 'JSXFragment') {
603
+ return node.children.some(contains_component_jsx);
604
+ }
605
+
606
+ if (node.type === 'JSXExpressionContainer') {
607
+ return contains_component_jsx(node.expression);
608
+ }
609
+
610
+ if (Array.isArray(node)) {
611
+ return node.some(contains_component_jsx);
612
+ }
613
+
614
+ return false;
615
+ }
616
+
617
+ /**
618
+ * @param {any} name
619
+ * @returns {boolean}
620
+ */
621
+ function is_component_jsx_name(name) {
622
+ if (!name || typeof name !== 'object') {
623
+ return false;
624
+ }
625
+
626
+ if (name.type === 'JSXIdentifier') {
627
+ const first = name.name?.[0];
628
+ return first != null && first >= 'A' && first <= 'Z';
629
+ }
630
+
631
+ if (name.type === 'JSXMemberExpression') {
632
+ return true;
633
+ }
634
+
635
+ return false;
636
+ }
637
+
638
+ /**
639
+ * @param {import('estree').Program} program
640
+ * @param {any} transform_context
641
+ * @returns {void}
642
+ */
643
+ function inject_vue_imports(program, transform_context) {
644
+ if (transform_context.needs_define_vapor_component) {
645
+ ensure_named_import(program, 'vue-jsx-vapor', 'defineVaporComponent');
646
+ }
647
+
648
+ if (transform_context.needs_suspense) {
649
+ ensure_named_import(program, 'vue', 'Suspense');
650
+ }
651
+
652
+ if (transform_context.needs_error_boundary) {
653
+ ensure_named_import(program, '@tsrx/vue/error-boundary', 'TsrxErrorBoundary');
654
+ }
655
+ }
656
+
657
+ /**
658
+ * @param {import('estree').Program} program
659
+ * @param {string} source
660
+ * @param {string} name
661
+ * @returns {void}
662
+ */
663
+ function ensure_named_import(program, source, name) {
664
+ for (const statement of program.body) {
665
+ if (statement.type !== 'ImportDeclaration' || statement.source?.value !== source) {
666
+ continue;
667
+ }
668
+
669
+ const has_specifier = statement.specifiers.some(
670
+ (/** @type {any} */ specifier) =>
671
+ specifier.type === 'ImportSpecifier' &&
672
+ specifier.imported?.type === 'Identifier' &&
673
+ specifier.imported.name === name,
674
+ );
675
+
676
+ if (!has_specifier) {
677
+ statement.specifiers.push(create_import_specifier(name));
678
+ }
679
+
680
+ return;
681
+ }
682
+
683
+ program.body.unshift(create_import_declaration(source, [create_import_specifier(name)]));
684
+ }
685
+
686
+ /**
687
+ * @param {string} name
688
+ * @returns {any}
689
+ */
690
+ function create_import_specifier(name) {
691
+ return {
692
+ type: 'ImportSpecifier',
693
+ imported: builders.id(name),
694
+ local: builders.id(name),
695
+ importKind: 'value',
696
+ metadata: { path: [] },
697
+ };
698
+ }
699
+
700
+ /**
701
+ * @param {string} source
702
+ * @param {any[]} specifiers
703
+ * @returns {any}
704
+ */
705
+ function create_import_declaration(source, specifiers) {
706
+ return {
707
+ type: 'ImportDeclaration',
708
+ attributes: [],
709
+ specifiers,
710
+ importKind: 'value',
711
+ source: builders.literal(source),
712
+ metadata: { path: [] },
713
+ };
714
+ }
@@ -0,0 +1,21 @@
1
+ import type { Program } from 'estree';
2
+ import type { CompileError, ParseOptions, VolarMappingsResult } from '@tsrx/core/types';
3
+
4
+ export function parse(source: string, filename?: string, options?: ParseOptions): Program;
5
+
6
+ export function compile(
7
+ source: string,
8
+ filename?: string,
9
+ options?: { loose?: boolean },
10
+ ): {
11
+ code: string;
12
+ map: unknown;
13
+ css: { code: string; hash: string } | null;
14
+ errors: CompileError[];
15
+ };
16
+
17
+ export function compile_to_volar_mappings(
18
+ source: string,
19
+ filename?: string,
20
+ options?: ParseOptions,
21
+ ): VolarMappingsResult;