@tsrx/core 0.1.33 → 0.1.35

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.1.33",
6
+ "version": "0.1.35",
7
7
  "type": "module",
8
8
  "repository": {
9
9
  "type": "git",
package/src/plugin.js CHANGED
@@ -259,6 +259,13 @@ export function TSRXPlugin(config) {
259
259
  // `#filterTemplateScriptContexts`.
260
260
  /** @type {number[]} */
261
261
  #expressionContainerContextBaselines = [];
262
+ // `#path` length at the start of each open `{ … }` expression container.
263
+ // Raw template text inside a container belongs only to an element opened
264
+ // inside it (`{<div> a</div>}`); at the container's own expression level
265
+ // (`{cond ? (<Outer>…</Outer>) : null}` after the `)`) the next characters
266
+ // are JS, and reading them as raw text would swallow tokens like `: null`.
267
+ /** @type {number[]} */
268
+ #expressionContainerPathBaselines = [];
262
269
  #consumeContainerBraceAfterScope = false;
263
270
  #scriptJSXElementDepth = 0;
264
271
  #forceScriptJSXElementDepth = 0;
@@ -936,6 +943,26 @@ export function TSRXPlugin(config) {
936
943
  if (!current_template_node || this.#isJSXControlFlowDirectiveAt(this.pos)) {
937
944
  return false;
938
945
  }
946
+ // Inside an expression container (only reachable with
947
+ // `allow_inside_expression_container`), raw text belongs to an element
948
+ // opened inside the container. When the innermost native template element
949
+ // sits below the container's path baseline we are at the container's own
950
+ // expression level — e.g. after `(<Outer>…</Outer>)` in
951
+ // `{cond ? (<Outer>…</Outer>) : null}` — and the following characters are
952
+ // JS tokens, not template text.
953
+ if (this.#jsxExpressionContainerDepth > 0 && !this.#openingNativeTemplateNode) {
954
+ const path_baseline = this.#expressionContainerPathBaselines.at(-1) ?? 0;
955
+ let inside_container = false;
956
+ for (let i = this.#path.length - 1; i >= path_baseline; i--) {
957
+ if (this.#isNativeTemplateNode(this.#path[i])) {
958
+ inside_container = true;
959
+ break;
960
+ }
961
+ }
962
+ if (!inside_container) {
963
+ return false;
964
+ }
965
+ }
939
966
  if (this.#isTemplateLineCommentStart(this.pos)) {
940
967
  return false;
941
968
  }
@@ -3377,6 +3404,7 @@ export function TSRXPlugin(config) {
3377
3404
  // container must not strip anything below this floor (see
3378
3405
  // `#filterTemplateScriptContexts`).
3379
3406
  this.#expressionContainerContextBaselines.push(this.context.length);
3407
+ this.#expressionContainerPathBaselines.push(this.#path.length);
3380
3408
  pushed_context_baseline = true;
3381
3409
 
3382
3410
  node.expression =
@@ -3398,6 +3426,7 @@ export function TSRXPlugin(config) {
3398
3426
  this.#jsxExpressionContainerDepth--;
3399
3427
  if (pushed_context_baseline) {
3400
3428
  this.#expressionContainerContextBaselines.pop();
3429
+ this.#expressionContainerPathBaselines.pop();
3401
3430
  }
3402
3431
  }
3403
3432
 
@@ -50,6 +50,17 @@ export const mapping_data_verify_complete = {
50
50
  completion: true,
51
51
  };
52
52
 
53
+ /**
54
+ * Completion only — no verification/hover/navigation. Used for positions that
55
+ * should surface completions but must not be type-checked, e.g. a `@`-leading text
56
+ * node that is an in-progress template directive (verifying it as code would raise
57
+ * spurious diagnostics).
58
+ * @type {Partial<VolarCodeMapping['data']>}
59
+ */
60
+ export const mapping_data_completion_only = {
61
+ completion: true,
62
+ };
63
+
53
64
  /**
54
65
  * Convert byte offset to line/column
55
66
  * @param {number} offset
@@ -1202,7 +1202,9 @@ function build_render_statements(
1202
1202
  const child = body_nodes[i];
1203
1203
 
1204
1204
  if (is_loop_skip_return_statement(child)) {
1205
- statements.push(create_component_return_statement(render_nodes, child));
1205
+ statements.push(
1206
+ create_component_return_statement(render_nodes, child, true, transform_context.typeOnly),
1207
+ );
1206
1208
  render_nodes.length = 0;
1207
1209
  has_terminal_return = true;
1208
1210
  continue;
@@ -1314,7 +1316,7 @@ function build_render_statements(
1314
1316
  hoist_static_render_nodes(render_nodes, transform_context);
1315
1317
  }
1316
1318
 
1317
- let return_arg = build_return_expression(render_nodes);
1319
+ let return_arg = build_return_expression(render_nodes, false, transform_context.typeOnly);
1318
1320
  // Keep an authored `<> … </>` render output verbatim instead of collapsing it:
1319
1321
  // an empty `<></>` stays `<></>` (not `null`), and a single child stays wrapped
1320
1322
  // (not its bare value). The `!== 'JSXFragment'` guard avoids double-wrapping a
@@ -3142,18 +3144,23 @@ function get_generated_component_metadata_list(node) {
3142
3144
  * @param {any[]} render_nodes
3143
3145
  * @param {any} source_node
3144
3146
  * @param {boolean} [map_render_node_locations]
3147
+ * @param {boolean} [type_only]
3145
3148
  * @returns {any}
3146
3149
  */
3147
3150
  function create_component_return_statement(
3148
3151
  render_nodes,
3149
3152
  source_node,
3150
3153
  map_render_node_locations = true,
3154
+ type_only = false,
3151
3155
  ) {
3152
3156
  const cloned = render_nodes.map((node) =>
3153
3157
  map_render_node_locations ? clone_expression_node(node) : clone_expression_node(node, false),
3154
3158
  );
3155
3159
 
3156
- return set_loc(b.return(build_return_expression(cloned) || create_null_literal()), source_node);
3160
+ return set_loc(
3161
+ b.return(build_return_expression(cloned, false, type_only) || create_null_literal()),
3162
+ source_node,
3163
+ );
3157
3164
  }
3158
3165
 
3159
3166
  /**
@@ -3196,7 +3203,11 @@ function get_loop_skip_if_consequent_body(node) {
3196
3203
  function create_component_loop_skip_if_statement(node, render_nodes, transform_context) {
3197
3204
  const consequent_body = /** @type {any[]} */ (get_loop_skip_if_consequent_body(node));
3198
3205
  const branch_statements = build_render_statements(consequent_body, true, transform_context);
3199
- prepend_render_nodes_to_return_statements(branch_statements, render_nodes);
3206
+ prepend_render_nodes_to_return_statements(
3207
+ branch_statements,
3208
+ render_nodes,
3209
+ transform_context.typeOnly,
3210
+ );
3200
3211
 
3201
3212
  const statement = set_loc(
3202
3213
  b.if(node.test, set_loc(b.block(branch_statements), node.consequent), null),
@@ -3212,15 +3223,16 @@ function create_component_loop_skip_if_statement(node, render_nodes, transform_c
3212
3223
  /**
3213
3224
  * @param {any[]} statements
3214
3225
  * @param {any[]} render_nodes
3226
+ * @param {boolean} [type_only]
3215
3227
  * @returns {void}
3216
3228
  */
3217
- function prepend_render_nodes_to_return_statements(statements, render_nodes) {
3229
+ function prepend_render_nodes_to_return_statements(statements, render_nodes, type_only = false) {
3218
3230
  if (render_nodes.length === 0) {
3219
3231
  return;
3220
3232
  }
3221
3233
 
3222
3234
  for (const statement of statements) {
3223
- prepend_render_nodes_to_return_statement(statement, render_nodes, false);
3235
+ prepend_render_nodes_to_return_statement(statement, render_nodes, false, type_only);
3224
3236
  }
3225
3237
  }
3226
3238
 
@@ -3228,9 +3240,15 @@ function prepend_render_nodes_to_return_statements(statements, render_nodes) {
3228
3240
  * @param {any} node
3229
3241
  * @param {any[]} render_nodes
3230
3242
  * @param {boolean} inside_nested_function
3243
+ * @param {boolean} [type_only]
3231
3244
  * @returns {void}
3232
3245
  */
3233
- function prepend_render_nodes_to_return_statement(node, render_nodes, inside_nested_function) {
3246
+ function prepend_render_nodes_to_return_statement(
3247
+ node,
3248
+ render_nodes,
3249
+ inside_nested_function,
3250
+ type_only = false,
3251
+ ) {
3234
3252
  if (!node || typeof node !== 'object') {
3235
3253
  return;
3236
3254
  }
@@ -3244,13 +3262,18 @@ function prepend_render_nodes_to_return_statement(node, render_nodes, inside_nes
3244
3262
  }
3245
3263
 
3246
3264
  if (!inside_nested_function && node.type === 'ReturnStatement') {
3247
- node.argument = combine_render_return_argument(render_nodes, node.argument);
3265
+ node.argument = combine_render_return_argument(render_nodes, node.argument, type_only);
3248
3266
  return;
3249
3267
  }
3250
3268
 
3251
3269
  if (Array.isArray(node)) {
3252
3270
  for (const child of node) {
3253
- prepend_render_nodes_to_return_statement(child, render_nodes, inside_nested_function);
3271
+ prepend_render_nodes_to_return_statement(
3272
+ child,
3273
+ render_nodes,
3274
+ inside_nested_function,
3275
+ type_only,
3276
+ );
3254
3277
  }
3255
3278
  return;
3256
3279
  }
@@ -3259,23 +3282,29 @@ function prepend_render_nodes_to_return_statement(node, render_nodes, inside_nes
3259
3282
  if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
3260
3283
  continue;
3261
3284
  }
3262
- prepend_render_nodes_to_return_statement(node[key], render_nodes, inside_nested_function);
3285
+ prepend_render_nodes_to_return_statement(
3286
+ node[key],
3287
+ render_nodes,
3288
+ inside_nested_function,
3289
+ type_only,
3290
+ );
3263
3291
  }
3264
3292
  }
3265
3293
 
3266
3294
  /**
3267
3295
  * @param {any[]} render_nodes
3268
3296
  * @param {any} return_argument
3297
+ * @param {boolean} [type_only]
3269
3298
  * @returns {any}
3270
3299
  */
3271
- function combine_render_return_argument(render_nodes, return_argument) {
3300
+ function combine_render_return_argument(render_nodes, return_argument, type_only = false) {
3272
3301
  const combined = render_nodes.map((node) => clone_expression_node(node, false));
3273
3302
 
3274
3303
  if (return_argument != null && !is_null_literal(return_argument)) {
3275
3304
  combined.push(return_argument_to_render_node(return_argument));
3276
3305
  }
3277
3306
 
3278
- return build_return_expression(combined) || create_null_literal();
3307
+ return build_return_expression(combined, false, type_only) || create_null_literal();
3279
3308
  }
3280
3309
 
3281
3310
  /**
@@ -4284,7 +4313,9 @@ export function wrap_edge_whitespace(nodes) {
4284
4313
  }
4285
4314
  }
4286
4315
  if (value !== '') {
4287
- out.push(b.jsx_text(value, value));
4316
+ // keep the location as we need it for @ autocomplete
4317
+ // and perhaps other things in the future
4318
+ out.push(b.jsx_text(value, value, node));
4288
4319
  }
4289
4320
  if (trailing) {
4290
4321
  out.push(trailing);
@@ -4412,7 +4443,9 @@ function tsrx_node_to_jsx_expression(node, transform_context, in_jsx_child = fal
4412
4443
  const render_nodes = wrap_edge_whitespace(
4413
4444
  children.map((/** @type {any} */ child) => to_jsx_child(child, transform_context)),
4414
4445
  );
4415
- expression = build_return_expression(render_nodes, in_jsx_child) || create_null_literal();
4446
+ expression =
4447
+ build_return_expression(render_nodes, in_jsx_child, transform_context.typeOnly) ||
4448
+ create_null_literal();
4416
4449
  } finally {
4417
4450
  transform_context.inside_element_child = saved_inside_element_child;
4418
4451
  }
@@ -5605,7 +5638,12 @@ function build_switch_with_lift(switch_node, transform_context) {
5605
5638
  if (helper) {
5606
5639
  return set_loc(
5607
5640
  b.switch_case(original_case.test, [
5608
- create_component_return_statement([helper.component_element], original_case),
5641
+ create_component_return_statement(
5642
+ [helper.component_element],
5643
+ original_case,
5644
+ true,
5645
+ transform_context.typeOnly,
5646
+ ),
5609
5647
  ]),
5610
5648
  original_case,
5611
5649
  );
@@ -5626,7 +5664,14 @@ function build_switch_with_lift(switch_node, transform_context) {
5626
5664
 
5627
5665
  for (const child of own_body) {
5628
5666
  if (is_loop_skip_return_statement(child)) {
5629
- case_body.push(create_component_return_statement(render_nodes, child));
5667
+ case_body.push(
5668
+ create_component_return_statement(
5669
+ render_nodes,
5670
+ child,
5671
+ true,
5672
+ transform_context.typeOnly,
5673
+ ),
5674
+ );
5630
5675
  has_terminal = true;
5631
5676
  break;
5632
5677
  }
@@ -5646,7 +5691,14 @@ function build_switch_with_lift(switch_node, transform_context) {
5646
5691
 
5647
5692
  if (!has_terminal) {
5648
5693
  if (render_nodes.length > 0) {
5649
- case_body.push(create_component_return_statement(render_nodes, original_case));
5694
+ case_body.push(
5695
+ create_component_return_statement(
5696
+ render_nodes,
5697
+ original_case,
5698
+ true,
5699
+ transform_context.typeOnly,
5700
+ ),
5701
+ );
5650
5702
  } else if (case_body.length > 0) {
5651
5703
  case_body.push(create_null_return_statement());
5652
5704
  } else if (has_terminator) {
@@ -6199,7 +6251,7 @@ function value_has_unmappable_jsx_loc(value) {
6199
6251
  * @param {boolean} [in_jsx_child]
6200
6252
  * @returns {any}
6201
6253
  */
6202
- export function build_return_expression(render_nodes, in_jsx_child = false) {
6254
+ export function build_return_expression(render_nodes, in_jsx_child = false, type_only = false) {
6203
6255
  if (render_nodes.length === 0) return null;
6204
6256
  if (render_nodes.length === 1) {
6205
6257
  const only = render_nodes[0];
@@ -6213,11 +6265,15 @@ export function build_return_expression(render_nodes, in_jsx_child = false) {
6213
6265
  return only.expression;
6214
6266
  }
6215
6267
  if (only.type === 'JSXText') {
6216
- if (in_jsx_child) {
6217
- return set_loc(b.jsx_fragment([only]), only.loc ? only : undefined);
6268
+ // Keep a single text child faithful to the source (e.g. `<>@</>`) — never
6269
+ // promote it to a `{'text'}` string-literal expression, in either the
6270
+ // type-only editor view or runtime codegen. At runtime we additionally drop a
6271
+ // nullish/whitespace-only child so it renders nothing instead of emitting
6272
+ // empty output.
6273
+ if (!type_only && !in_jsx_child && (only.value ?? '').trim() === '') {
6274
+ return null;
6218
6275
  }
6219
- const value = (only.value ?? '').trim();
6220
- return b.literal(value, JSON.stringify(value), only);
6276
+ return set_loc(b.jsx_fragment([only]), only.loc ? only : undefined);
6221
6277
  }
6222
6278
  return only;
6223
6279
  }
@@ -53,6 +53,7 @@ import {
53
53
  mapping_data,
54
54
  mapping_data_verify_only,
55
55
  mapping_data_verify_complete,
56
+ mapping_data_completion_only,
56
57
  build_line_offsets,
57
58
  get_mapping_from_node,
58
59
  } from '../source-map-utils.js';
@@ -806,7 +807,27 @@ export function convert_source_map_to_mappings(
806
807
  }
807
808
  return;
808
809
  } else if (node.type === 'JSXText') {
809
- // Text content, no tokens to collect
810
+ // A text node whose first non-whitespace char is `@` is an in-progress template
811
+ // directive (`@`, `@i`, `@if …`) the parser recovered as text; emit a completion-only
812
+ // mapping so the editor can still offer `@if`/`@for`/`@switch`/`@try` completions there.
813
+ //
814
+ // Use a token (resolved by matching generated CONTENT) rather than get_mapping_from_node.
815
+ // At a control-flow boundary the text node's source start maps to several generated
816
+ // positions — e.g. a preceding `@switch` value-IIFE's `return null; })()` tail AND the
817
+ // text itself — and get_mapping_from_node just takes the first, so its generated length
818
+ // spans the wrong region and the editor can't map a completion's edit back to source
819
+ // (it then drops the item). The token resolves to the position whose generated text
820
+ // matches the node's value, giving a well-formed same-length mapping. Ripple keeps text
821
+ // verbatim in to_ts, so `source` and `generated` are identical. Other text stays unmapped.
822
+ if (node.loc && typeof node.value === 'string' && node.value.trimStart().startsWith('@')) {
823
+ tokens.push({
824
+ source: node.value,
825
+ generated: node.value,
826
+ loc: node.loc,
827
+ metadata: {},
828
+ mappingData: mapping_data_completion_only,
829
+ });
830
+ }
810
831
  return;
811
832
  } else if (node.type === 'JSXCodeBlock') {
812
833
  for (const statement of node.body) {
@@ -1369,15 +1369,18 @@ export function jsx_spread_attribute(argument, loc_info) {
1369
1369
  /**
1370
1370
  * @param {string} value
1371
1371
  * @param {string} raw
1372
+ * @param {AST.NodeWithLocation} [loc_info]
1372
1373
  * @returns {ESTreeJSX.JSXText}
1373
1374
  */
1374
- export function jsx_text(value, raw) {
1375
- return {
1375
+ export function jsx_text(value, raw, loc_info) {
1376
+ const node = /** @type {ESTreeJSX.JSXText} */ ({
1376
1377
  type: 'JSXText',
1377
1378
  value,
1378
1379
  raw,
1379
1380
  metadata: { path: [] },
1380
- };
1381
+ });
1382
+
1383
+ return set_location(node, loc_info);
1381
1384
  }
1382
1385
 
1383
1386
  /**
package/types/index.d.ts CHANGED
@@ -1480,6 +1480,13 @@ export interface TransformServerState extends BaseState {
1480
1480
  dev?: boolean;
1481
1481
  return_flags?: Map<AST.ReturnStatement, { name: string; tracked: boolean }>;
1482
1482
  template_child?: boolean;
1483
+ /**
1484
+ * True while transforming the direct body of a control-flow branch
1485
+ * (`@if`/`@else`/`@for`/`@switch`/`@try`). A `<>…</>` in this position is
1486
+ * bracketed with hydration block markers so the client's fragment
1487
+ * `expression()` finds a matching boundary during hydration.
1488
+ */
1489
+ control_flow_branch_body?: boolean;
1483
1490
  skip_regular_blocks?: boolean;
1484
1491
  in_regular_block?: boolean;
1485
1492
  is_tsrx_element?: boolean;
@@ -1564,7 +1571,10 @@ export type TransformClientContext = Context<AST.Node, TransformClientState>;
1564
1571
  export type TransformServerContext = Context<AST.Node, TransformServerState>;
1565
1572
  export type AnalysisContext = Context<AST.Node, AnalysisState>;
1566
1573
  export type CommonContext = TransformClientContext | TransformServerContext | AnalysisContext;
1567
- export type VisitorClientContext = TransformClientContext & { root?: boolean };
1574
+ export type VisitorClientContext = TransformClientContext & {
1575
+ root?: boolean;
1576
+ value_position?: boolean;
1577
+ };
1568
1578
 
1569
1579
  /**
1570
1580
  * Delegated event result