@tsrx/vue 0.1.10 → 0.1.12

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.1.10",
6
+ "version": "0.1.12",
7
7
  "type": "module",
8
8
  "publishConfig": {
9
9
  "access": "public"
@@ -22,6 +22,10 @@
22
22
  "types": "./types/error-boundary.d.ts",
23
23
  "default": "./src/error-boundary.js"
24
24
  },
25
+ "./interop": {
26
+ "types": "./types/interop.d.ts",
27
+ "default": "./src/interop.js"
28
+ },
25
29
  "./ref": {
26
30
  "types": "./types/ref.d.ts",
27
31
  "default": "./src/ref.js"
@@ -31,11 +35,11 @@
31
35
  "esrap": "^2.2.8",
32
36
  "is-reference": "^3.0.3",
33
37
  "zimmerframe": "^1.1.2",
34
- "@tsrx/core": "0.1.10"
38
+ "@tsrx/core": "0.1.12"
35
39
  },
36
40
  "peerDependencies": {
37
41
  "vue": ">=3.5",
38
- "vue-jsx-vapor": ">=3.2.12"
42
+ "vue-jsx-vapor": ">=3.2.14"
39
43
  },
40
44
  "devDependencies": {
41
45
  "@types/estree": "^1.0.8",
@@ -130,6 +130,9 @@ function create_boundary_nodes(render) {
130
130
  */
131
131
  export function TsrxErrorBoundary(props) {
132
132
  const instance = getCurrentInstance();
133
+ if (instance) {
134
+ initialize_boundary_state(instance);
135
+ }
133
136
  const state = instance ? boundary_states.get(instance) : undefined;
134
137
  const error = state?.error ?? shallowRef(/** @type {unknown} */ (null));
135
138
  const reset =
@@ -155,6 +158,16 @@ export function TsrxErrorBoundary(props) {
155
158
  /** @returns {void} */
156
159
  TsrxErrorBoundary.__setup = function setup() {
157
160
  const instance = getCurrentInstance();
161
+ if (instance) {
162
+ initialize_boundary_state(instance);
163
+ }
164
+ };
165
+
166
+ /**
167
+ * @param {BoundaryValue} instance
168
+ * @returns {void}
169
+ */
170
+ function initialize_boundary_state(instance) {
158
171
  if (!instance || boundary_states.has(instance)) {
159
172
  return;
160
173
  }
@@ -170,4 +183,4 @@ TsrxErrorBoundary.__setup = function setup() {
170
183
  error.value = captured_error;
171
184
  return false;
172
185
  });
173
- };
186
+ }
package/src/interop.js ADDED
@@ -0,0 +1,93 @@
1
+ const vue_import_pattern = /import(?!\s+type)\s*\{([\s\S]*?)\}\s*from\s*(['"])vue\2\s*;?/g;
2
+
3
+ /**
4
+ * Vue's built-in renderer primitives, including `Suspense`, need Vapor/VDOM
5
+ * interop when mounted from a Vapor app. TSRX can emit `Suspense` for
6
+ * `try/pending`, so bundler plugins use this rewrite to make Vapor app
7
+ * creation install the interop plugin without every app entry point needing to
8
+ * remember it.
9
+ *
10
+ * @param {string} source
11
+ * @returns {string}
12
+ */
13
+ export function addVaporInteropToCreateVaporApp(source) {
14
+ if (!/\bcreateVaporApp\b/.test(source) || !/\bfrom\s*['"]vue['"]/.test(source)) {
15
+ return source;
16
+ }
17
+
18
+ /** @type {string | null} */
19
+ let existing_interop_local = null;
20
+
21
+ for (const match of source.matchAll(vue_import_pattern)) {
22
+ for (const specifier of split_import_specifiers(match[1])) {
23
+ const parsed = parse_import_specifier(specifier);
24
+ if (parsed?.imported === 'vaporInteropPlugin') {
25
+ existing_interop_local = parsed.local;
26
+ break;
27
+ }
28
+ }
29
+ if (existing_interop_local) break;
30
+ }
31
+
32
+ if (existing_interop_local && source.includes(`.use(${existing_interop_local})`)) {
33
+ return source;
34
+ }
35
+
36
+ const interop_local = existing_interop_local ?? 'vaporInteropPlugin';
37
+ let added_interop_import = existing_interop_local !== null;
38
+
39
+ return source.replace(vue_import_pattern, (full, specifier_text, quote) => {
40
+ const specifiers = split_import_specifiers(specifier_text);
41
+ /** @type {string[]} */
42
+ const wrappers = [];
43
+ let import_changed = false;
44
+
45
+ const next_specifiers = specifiers.map((specifier) => {
46
+ const parsed = parse_import_specifier(specifier);
47
+ if (parsed?.imported !== 'createVaporApp') {
48
+ return specifier.trim();
49
+ }
50
+
51
+ const wrapped_local = `__tsrx_${parsed.local}`;
52
+ wrappers.push(
53
+ `const ${parsed.local} = (...args) => ${wrapped_local}(...args).use(${interop_local});`,
54
+ );
55
+ import_changed = true;
56
+ return `createVaporApp as ${wrapped_local}`;
57
+ });
58
+
59
+ if (!import_changed) {
60
+ return full;
61
+ }
62
+
63
+ if (!added_interop_import) {
64
+ next_specifiers.push(interop_local);
65
+ added_interop_import = true;
66
+ }
67
+
68
+ return `import { ${next_specifiers.join(', ')} } from ${quote}vue${quote};\n${wrappers.join('\n')}`;
69
+ });
70
+ }
71
+
72
+ /**
73
+ * @param {string} specifier_text
74
+ * @returns {string[]}
75
+ */
76
+ function split_import_specifiers(specifier_text) {
77
+ return specifier_text
78
+ .split(',')
79
+ .map((specifier) => specifier.trim())
80
+ .filter(Boolean);
81
+ }
82
+
83
+ /**
84
+ * @param {string} specifier
85
+ * @returns {{ imported: string, local: string } | null}
86
+ */
87
+ function parse_import_specifier(specifier) {
88
+ const match = /^([A-Za-z_$][\w$]*)(?:\s+as\s+([A-Za-z_$][\w$]*))?$/.exec(specifier.trim());
89
+ if (!match) {
90
+ return null;
91
+ }
92
+ return { imported: match[1], local: match[2] ?? match[1] };
93
+ }
package/src/transform.js CHANGED
@@ -4,10 +4,12 @@ import { walk } from 'zimmerframe';
4
4
  import is_reference from 'is-reference';
5
5
  import {
6
6
  builders,
7
+ addJsxSetupDeclaration,
7
8
  clone_expression_node,
8
9
  clone_identifier,
9
10
  contains_component_jsx,
10
11
  CREATE_REF_PROP_INTERNAL_NAME,
12
+ createHookSafeHelper,
11
13
  create_generated_identifier,
12
14
  componentToFunctionDeclaration,
13
15
  createJsxTransform,
@@ -46,8 +48,6 @@ const vue_platform = {
46
48
  validation: {
47
49
  requireUseServerForAwait: true,
48
50
  scanUseServerDirectiveForAwaitWithCustomValidator: false,
49
- unsupportedTryPendingMessage:
50
- '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.',
51
51
  },
52
52
  hooks: {
53
53
  // Hoist to module scope
@@ -83,8 +83,51 @@ const vue_platform = {
83
83
  },
84
84
  renderForOf: (node, loop_params, body_statements, ctx) =>
85
85
  render_for_of_as_vapor_for(node, loop_params, body_statements, ctx),
86
+ createPendingBoundary(try_content, fallback_content) {
87
+ return create_vapor_pending_boundary(try_content, fallback_content);
88
+ },
89
+ createErrorFallbackComponent(catch_body_nodes, catch_params, ctx, node) {
90
+ if (ctx.typeOnly) return null;
91
+ return create_module_scoped_error_fallback_component(
92
+ catch_body_nodes,
93
+ catch_params,
94
+ ctx,
95
+ node,
96
+ );
97
+ },
98
+ createErrorBoundary(try_content, raw_try_content, fallback_fn, ctx, node, info) {
99
+ if (!node.pending) {
100
+ return null;
101
+ }
102
+ const fallback_content = /** @type {any} */ (try_content.metadata)?.vapor_pending_fallback;
103
+ if (!fallback_content) {
104
+ return create_vapor_error_boundary(try_content, fallback_fn);
105
+ }
106
+ const fallback_component = info?.fallbackComponent ?? null;
107
+ const fallback_renderer = fallback_component
108
+ ? create_fallback_component_renderer(fallback_component, fallback_fn)
109
+ : fallback_fn;
110
+ const default_slot = ctx.typeOnly
111
+ ? builders.arrow([], jsx_child_to_expression(raw_try_content))
112
+ : create_sync_error_boundary_slot(
113
+ raw_try_content,
114
+ fallback_fn,
115
+ fallback_component,
116
+ node.block,
117
+ node,
118
+ );
119
+ const suspense = create_vapor_pending_boundary_from_default_slot(
120
+ default_slot,
121
+ fallback_content,
122
+ );
123
+ const boundary = create_vapor_error_boundary(suspense, fallback_renderer);
124
+ for (const statement of fallback_component?.setup_statements ?? []) {
125
+ addJsxSetupDeclaration(boundary, statement);
126
+ }
127
+ return boundary;
128
+ },
86
129
  createErrorBoundaryContent(try_content) {
87
- return builders.arrow([], try_content.expression);
130
+ return builders.arrow([], jsx_child_to_expression(try_content));
88
131
  },
89
132
  transformElementChildren(node, walked_children, raw_children, attributes, ctx) {
90
133
  return rewrite_host_text_or_html_children(
@@ -116,6 +159,207 @@ const vue_platform = {
116
159
 
117
160
  export const transform = createJsxTransform(vue_platform);
118
161
 
162
+ /**
163
+ * @param {any} try_content
164
+ * @param {any} fallback_content
165
+ * @returns {any}
166
+ */
167
+ function create_vapor_pending_boundary(try_content, fallback_content) {
168
+ return create_vapor_pending_boundary_from_default_slot(
169
+ builders.arrow([], jsx_child_to_expression(try_content)),
170
+ fallback_content,
171
+ );
172
+ }
173
+
174
+ /**
175
+ * @param {any} default_slot
176
+ * @param {any} fallback_content
177
+ * @returns {any}
178
+ */
179
+ function create_vapor_pending_boundary_from_default_slot(default_slot, fallback_content) {
180
+ const fallback_expression = jsx_child_to_expression(fallback_content);
181
+ const slots_properties = [
182
+ builders.init('_', builders.literal(1)),
183
+ builders.init('default', default_slot),
184
+ ];
185
+
186
+ if (fallback_expression.type !== 'Literal' || fallback_expression.value !== null) {
187
+ slots_properties.push(builders.init('fallback', builders.arrow([], fallback_expression)));
188
+ }
189
+
190
+ const slots = builders.object(slots_properties);
191
+
192
+ const boundary = builders.jsx_element_fresh(
193
+ builders.jsx_opening_element(
194
+ builders.jsx_id('Suspense'),
195
+ [builders.jsx_attribute(builders.jsx_id('v-slots'), to_jsx_expression_container(slots))],
196
+ true,
197
+ ),
198
+ null,
199
+ [],
200
+ );
201
+ /** @type {any} */ (boundary.metadata).vapor_pending_fallback = fallback_content;
202
+ return boundary;
203
+ }
204
+
205
+ /**
206
+ * @param {any[]} catch_body_nodes
207
+ * @param {any[]} catch_params
208
+ * @param {any} ctx
209
+ * @param {any} node
210
+ * @returns {any}
211
+ */
212
+ function create_module_scoped_error_fallback_component(catch_body_nodes, catch_params, ctx, node) {
213
+ const saved_module_scoped = ctx.module_scoped_hook_components;
214
+ ctx.module_scoped_hook_components = true;
215
+ try {
216
+ return createHookSafeHelper(catch_body_nodes, undefined, node.handler ?? node, ctx, undefined, {
217
+ transientBindings: get_pattern_names(catch_params),
218
+ });
219
+ } finally {
220
+ ctx.module_scoped_hook_components = saved_module_scoped;
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Catch synchronous setup errors directly in the Suspense default slot so
226
+ * Suspense can still observe async children while `catch` handles immediate
227
+ * render failures.
228
+ *
229
+ * @param {any} content
230
+ * @param {any} fallback_fn
231
+ * @param {{ component_element: any } | null} fallback_component
232
+ * @param {any} source_block
233
+ * @param {any} source_try
234
+ * @returns {any}
235
+ */
236
+ function create_sync_error_boundary_slot(
237
+ content,
238
+ fallback_fn,
239
+ fallback_component,
240
+ source_block,
241
+ source_try,
242
+ ) {
243
+ const error_id = create_generated_identifier('_error');
244
+ const content_expression = jsx_child_to_expression(content);
245
+ const fallback_expression = fallback_component
246
+ ? create_fallback_component_element(fallback_component, fallback_fn, [
247
+ error_id,
248
+ builders.arrow([], builders.block([])),
249
+ ])
250
+ : builders.call(
251
+ builders.parenthesized(fallback_fn),
252
+ clone_identifier(error_id),
253
+ builders.arrow([], builders.block([])),
254
+ );
255
+ const try_block = setLocation(
256
+ builders.block([builders.return(content_expression)]),
257
+ source_block,
258
+ true,
259
+ );
260
+ const try_statement = setLocation(
261
+ builders.try(
262
+ try_block,
263
+ {
264
+ type: 'CatchClause',
265
+ param: error_id,
266
+ body: builders.block([builders.return(fallback_expression)]),
267
+ metadata: { path: [] },
268
+ },
269
+ null,
270
+ null,
271
+ ),
272
+ source_try,
273
+ true,
274
+ );
275
+ return builders.arrow([], builders.block([try_statement]));
276
+ }
277
+
278
+ /**
279
+ * @param {{ component_element: any }} fallback_component
280
+ * @param {any} fallback_fn
281
+ * @returns {any}
282
+ */
283
+ function create_fallback_component_renderer(fallback_component, fallback_fn) {
284
+ return builders.arrow(
285
+ fallback_fn.params.map((/** @type {any} */ param) => clone_expression_node(param, false)),
286
+ builders.block([
287
+ builders.return(create_fallback_component_element(fallback_component, fallback_fn)),
288
+ ]),
289
+ );
290
+ }
291
+
292
+ /**
293
+ * @param {{ component_element: any }} fallback_component
294
+ * @param {any} fallback_fn
295
+ * @param {any[]} [replacement_args]
296
+ * @returns {any}
297
+ */
298
+ function create_fallback_component_element(fallback_component, fallback_fn, replacement_args = []) {
299
+ const element = clone_expression_node(fallback_component.component_element, false);
300
+ const replacements = new Map();
301
+ for (let i = 0; i < fallback_fn.params.length && i < replacement_args.length; i += 1) {
302
+ const param = fallback_fn.params[i];
303
+ if (param?.type === 'Identifier') {
304
+ replacements.set(param.name, replacement_args[i]);
305
+ }
306
+ }
307
+
308
+ for (const attr of element.openingElement?.attributes ?? []) {
309
+ const attr_name = attr.name?.name;
310
+ if (!attr_name || !replacements.has(attr_name)) continue;
311
+ attr.value = to_jsx_expression_container(replacements.get(attr_name), attr.value ?? attr);
312
+ }
313
+
314
+ return element;
315
+ }
316
+
317
+ /**
318
+ * @param {any[]} patterns
319
+ * @returns {Set<string>}
320
+ */
321
+ function get_pattern_names(patterns) {
322
+ const names = new Set();
323
+ for (const pattern of patterns) {
324
+ collect_pattern_names(pattern, names);
325
+ }
326
+ return names;
327
+ }
328
+
329
+ /**
330
+ * @param {any} child
331
+ * @returns {any}
332
+ */
333
+ function jsx_child_to_expression(child) {
334
+ return child?.type === 'JSXExpressionContainer' ? child.expression : child;
335
+ }
336
+
337
+ /**
338
+ * @param {any} content
339
+ * @param {any} fallback_fn
340
+ * @returns {any}
341
+ */
342
+ function create_vapor_error_boundary(content, fallback_fn) {
343
+ return builders.jsx_element_fresh(
344
+ builders.jsx_opening_element(
345
+ builders.jsx_id('TsrxErrorBoundary'),
346
+ [
347
+ builders.jsx_attribute(
348
+ builders.jsx_id('fallback'),
349
+ to_jsx_expression_container(fallback_fn),
350
+ ),
351
+ builders.jsx_attribute(
352
+ builders.jsx_id('content'),
353
+ to_jsx_expression_container(builders.arrow([], jsx_child_to_expression(content))),
354
+ ),
355
+ ],
356
+ true,
357
+ ),
358
+ null,
359
+ [],
360
+ );
361
+ }
362
+
119
363
  /**
120
364
  * Vue's `VNodeRef` type is wider than TSRX host refs because it also supports
121
365
  * component instances and null teardown values. In editor-only TSX, keep the ref
@@ -158,14 +402,13 @@ function component_to_vapor_component_declaration(component, transform_context,
158
402
  function_declaration_to_expression(fn),
159
403
  generated_helpers,
160
404
  generated_statics,
161
- component,
162
405
  );
163
406
 
164
407
  if (component.default || !component.id) {
165
408
  return call;
166
409
  }
167
410
 
168
- const component_id = clone_identifier(component.id);
411
+ const component_id = create_generated_identifier(component.id.name);
169
412
  const fn_id = fn.type === 'FunctionDeclaration' ? fn.id : null;
170
413
  component_id.metadata = {
171
414
  ...component_id.metadata,
@@ -179,7 +422,7 @@ function component_to_vapor_component_declaration(component, transform_context,
179
422
  generated_helpers,
180
423
  generated_statics,
181
424
  });
182
- return setLocation(/** @type {any} */ (declaration), component);
425
+ return declaration;
183
426
  }
184
427
 
185
428
  /**
@@ -193,12 +436,7 @@ function wrap_helper_component(helper_fn, helper_id, source_node) {
193
436
  builders.declaration('const', [
194
437
  builders.declarator(
195
438
  clone_identifier(helper_id),
196
- create_define_vapor_component_call(
197
- function_declaration_to_expression(helper_fn),
198
- [],
199
- [],
200
- source_node,
201
- ),
439
+ create_define_vapor_component_call(function_declaration_to_expression(helper_fn), [], []),
202
440
  ),
203
441
  ]),
204
442
  source_node,
@@ -209,21 +447,15 @@ function wrap_helper_component(helper_fn, helper_id, source_node) {
209
447
  * @param {any} fn_expression
210
448
  * @param {any[]} generated_helpers
211
449
  * @param {any[]} generated_statics
212
- * @param {any} source_node
213
450
  * @returns {any}
214
451
  */
215
- function create_define_vapor_component_call(
216
- fn_expression,
217
- generated_helpers,
218
- generated_statics,
219
- source_node,
220
- ) {
452
+ function create_define_vapor_component_call(fn_expression, generated_helpers, generated_statics) {
221
453
  const call = builders.call('defineVaporComponent', fn_expression);
222
454
  Object.assign(/** @type {any} */ (call.metadata), {
223
455
  generated_helpers,
224
456
  generated_statics,
225
457
  });
226
- return setLocation(call, source_node);
458
+ return call;
227
459
  }
228
460
 
229
461
  /**
@@ -0,0 +1 @@
1
+ export function addVaporInteropToCreateVaporApp(source: string): string;