@tsrx/core 0.0.24 → 0.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Core compiler infrastructure for TSRX syntax",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.0.24",
6
+ "version": "0.0.26",
7
7
  "type": "module",
8
8
  "repository": {
9
9
  "type": "git",
package/src/index.js CHANGED
@@ -133,8 +133,11 @@ export {
133
133
  // Sanitize
134
134
  export { sanitize_template_string as sanitizeTemplateString } from './utils/sanitize_template_string.js';
135
135
 
136
+ // CSS Property Name
137
+ export { normalize_css_property_name as normalizeCssPropertyName } from './utils/normalize_css_property_name.js';
138
+
136
139
  // Escaping
137
- export { escape } from './utils/escaping.js';
140
+ export { escape, escape_script as escapeScript } from './utils/escaping.js';
138
141
 
139
142
  // Transform
140
143
  export {
package/src/plugin.js CHANGED
@@ -313,6 +313,12 @@ export function TSRXPlugin(config) {
313
313
  }
314
314
  }
315
315
 
316
+ #popTemplateLiteralTokenContext() {
317
+ while (this.curContext()?.token === '`') {
318
+ this.context.pop();
319
+ }
320
+ }
321
+
316
322
  #isDoubleQuotedTextChildStart() {
317
323
  if (this.#path.findLast((n) => n.type === 'TsxCompat' || n.type === 'Tsx')) {
318
324
  return false;
@@ -1097,6 +1103,19 @@ export function TSRXPlugin(config) {
1097
1103
  this.parseFunctionParams(node);
1098
1104
  this.checkComponentParams(node.params);
1099
1105
 
1106
+ const is_arrow_component = this.type === tt.arrow;
1107
+ if (is_arrow_component) {
1108
+ if (node.id || requireName || skipName) {
1109
+ this.raise(
1110
+ this.start,
1111
+ 'Arrow component syntax is only supported for anonymous component expressions.',
1112
+ );
1113
+ }
1114
+ node.metadata ??= { path: [] };
1115
+ node.metadata.arrow = true;
1116
+ this.next();
1117
+ }
1118
+
1100
1119
  // Reset before `eat(braceL)` so the lookahead `next()` it triggers reads
1101
1120
  // the component body's first token as if we'd entered fresh — no
1102
1121
  // surrounding function body should affect our parseStatement/parseBlock
@@ -2370,7 +2389,7 @@ export function TSRXPlugin(config) {
2370
2389
  body.push(node);
2371
2390
  } else if (this.type === tstt.jsxTagStart) {
2372
2391
  // Parse JSX element
2373
- const node = super.parseExpression();
2392
+ const node = super.jsx_parseElement();
2374
2393
  body.push(node);
2375
2394
  } else {
2376
2395
  const start = this.start;
@@ -2401,6 +2420,7 @@ export function TSRXPlugin(config) {
2401
2420
  body.push(node);
2402
2421
  }
2403
2422
 
2423
+ this.#popTemplateLiteralTokenContext();
2404
2424
  // Always call next() to ensure parser makes progress
2405
2425
  this.next();
2406
2426
  }
@@ -2433,7 +2453,7 @@ export function TSRXPlugin(config) {
2433
2453
  body.push(node);
2434
2454
  } else if (this.type === tstt.jsxTagStart) {
2435
2455
  // Parse JSX element
2436
- const node = super.parseExpression();
2456
+ const node = super.jsx_parseElement();
2437
2457
  body.push(node);
2438
2458
  } else {
2439
2459
  const start = this.start;
@@ -2464,6 +2484,7 @@ export function TSRXPlugin(config) {
2464
2484
  body.push(node);
2465
2485
  }
2466
2486
 
2487
+ this.#popTemplateLiteralTokenContext();
2467
2488
  this.next();
2468
2489
  }
2469
2490
  }
@@ -2748,6 +2769,19 @@ export function TSRXPlugin(config) {
2748
2769
  return node;
2749
2770
  }
2750
2771
 
2772
+ if (
2773
+ this.#functionBodyDepth === 0 &&
2774
+ this.type === tt.string &&
2775
+ this.input.charCodeAt(this.start) === 34 &&
2776
+ (this.#path.at(-1)?.type === 'Component' || this.#path.at(-1)?.type === 'Element')
2777
+ ) {
2778
+ this.pos = this.start;
2779
+ this.#readDoubleQuotedTextChildToken();
2780
+ const node = this.parseDoubleQuotedTextChild();
2781
+ this.semicolon();
2782
+ return node;
2783
+ }
2784
+
2751
2785
  // &[ or &{ at statement level — lazy destructuring assignment
2752
2786
  // e.g., &[data] = track(0); or &{x, y} = obj;
2753
2787
  if (this.type === tt.bitwiseAND) {
@@ -487,7 +487,7 @@ function has_use_server_directive(program) {
487
487
  * @param {any} component
488
488
  * @param {TransformContext} transform_context
489
489
  * @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }} [walk_helper_state]
490
- * @returns {AST.FunctionDeclaration}
490
+ * @returns {AST.FunctionDeclaration | AST.FunctionExpression | AST.ArrowFunctionExpression}
491
491
  */
492
492
  export function component_to_function_declaration(component, transform_context, walk_helper_state) {
493
493
  const helper_state = walk_helper_state || create_helper_state(component.id?.name || 'Component');
@@ -527,28 +527,62 @@ export function component_to_function_declaration(component, transform_context,
527
527
  const final_body =
528
528
  lazy_bindings.size > 0 ? apply_lazy_transforms(body_block, lazy_bindings) : body_block;
529
529
 
530
- const fn = /** @type {any} */ ({
531
- type: 'FunctionDeclaration',
532
- id: component.id,
533
- typeParameters: component.typeParameters,
534
- params: final_params,
535
- body: final_body,
536
- async: is_async_component,
537
- generator: false,
538
- metadata: {
539
- path: [],
540
- is_component: true,
541
- },
542
- });
530
+ /** @type {AST.FunctionDeclaration | AST.FunctionExpression | AST.ArrowFunctionExpression} */
531
+ let fn;
532
+
533
+ if (component.id) {
534
+ fn = /** @type {any} */ ({
535
+ type: 'FunctionDeclaration',
536
+ id: component.id,
537
+ typeParameters: component.typeParameters,
538
+ params: final_params,
539
+ body: final_body,
540
+ async: is_async_component,
541
+ generator: false,
542
+ metadata: {
543
+ path: [],
544
+ is_component: true,
545
+ },
546
+ });
547
+ } else if (component.metadata?.arrow) {
548
+ fn = /** @type {any} */ ({
549
+ type: 'ArrowFunctionExpression',
550
+ typeParameters: component.typeParameters,
551
+ params: final_params,
552
+ body: final_body,
553
+ async: is_async_component,
554
+ generator: false,
555
+ expression: false,
556
+ metadata: {
557
+ path: [],
558
+ is_component: true,
559
+ },
560
+ });
561
+ } else {
562
+ fn = /** @type {any} */ ({
563
+ type: 'FunctionExpression',
564
+ id: null,
565
+ typeParameters: component.typeParameters,
566
+ params: final_params,
567
+ body: final_body,
568
+ async: is_async_component,
569
+ generator: false,
570
+ metadata: {
571
+ path: [],
572
+ is_component: true,
573
+ },
574
+ });
575
+ }
543
576
 
544
577
  // Restore context
545
578
  transform_context.helper_state = saved_helper_state;
546
579
  transform_context.available_bindings = saved_bindings;
547
580
 
548
- fn.metadata.generated_helpers = helper_state.helpers;
549
- fn.metadata.generated_statics = helper_state.statics;
581
+ const fn_metadata = /** @type {any} */ (fn.metadata);
582
+ fn_metadata.generated_helpers = helper_state.helpers;
583
+ fn_metadata.generated_statics = helper_state.statics;
550
584
 
551
- if (fn.id) {
585
+ if (fn.type === 'FunctionDeclaration' && fn.id) {
552
586
  fn.id.metadata = /** @type {AST.Identifier['metadata']} */ ({
553
587
  ...fn.id.metadata,
554
588
  is_component: true,
@@ -590,6 +624,10 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
590
624
  // any JSX is constructed, and every JSX child would observe the final
591
625
  // state of mutable variables.
592
626
  const interleaved = is_interleaved_body(body_nodes);
627
+ const capture_static_early_return_nodes =
628
+ !interleaved &&
629
+ !transform_context.platform.hooks?.isTopLevelSetupCall &&
630
+ body_nodes.filter(is_returning_if_statement).length > 1;
593
631
  let capture_index = 0;
594
632
 
595
633
  for (let i = 0; i < body_nodes.length; i += 1) {
@@ -614,6 +652,15 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
614
652
  true,
615
653
  );
616
654
 
655
+ if (capture_static_early_return_nodes) {
656
+ capture_index = capture_static_early_return_render_nodes(
657
+ render_nodes,
658
+ statements,
659
+ capture_index,
660
+ transform_context,
661
+ );
662
+ }
663
+
617
664
  if (branch_has_hooks || continuation_has_hooks) {
618
665
  if (transform_context.platform.hooks?.isTopLevelSetupCall) {
619
666
  statements.push(
@@ -1291,15 +1338,68 @@ function hoist_static_render_nodes(render_nodes, transform_context) {
1291
1338
  }
1292
1339
  }
1293
1340
 
1341
+ /**
1342
+ * Static JSX that appears before multiple early-return guards is otherwise
1343
+ * cloned into every generated return. Capture it once at its source position
1344
+ * and reuse the reference, matching the interleaved-statement capture path
1345
+ * without moving dynamic render-time expressions across guards.
1346
+ *
1347
+ * @param {any[]} render_nodes
1348
+ * @param {any[]} statements
1349
+ * @param {number} capture_index
1350
+ * @param {TransformContext} transform_context
1351
+ * @returns {number}
1352
+ */
1353
+ function capture_static_early_return_render_nodes(
1354
+ render_nodes,
1355
+ statements,
1356
+ capture_index,
1357
+ transform_context,
1358
+ ) {
1359
+ for (let i = 0; i < render_nodes.length; i += 1) {
1360
+ const node = render_nodes[i];
1361
+ if (!is_static_early_return_capture_node(node, transform_context)) {
1362
+ continue;
1363
+ }
1364
+
1365
+ const { declaration, reference } = captureJsxChild(node, capture_index++);
1366
+ statements.push(declaration);
1367
+ render_nodes[i] = reference;
1368
+ }
1369
+
1370
+ return capture_index;
1371
+ }
1372
+
1373
+ /**
1374
+ * @param {any} node
1375
+ * @param {TransformContext} transform_context
1376
+ * @returns {boolean}
1377
+ */
1378
+ function is_static_early_return_capture_node(node, transform_context) {
1379
+ if (node?.type !== 'JSXElement' && node?.type !== 'JSXFragment') {
1380
+ return false;
1381
+ }
1382
+ if (!is_hoist_safe_jsx_node(node)) {
1383
+ return false;
1384
+ }
1385
+ if (
1386
+ transform_context.platform.hooks?.canHoistStaticNode &&
1387
+ !transform_context.platform.hooks.canHoistStaticNode(node, transform_context)
1388
+ ) {
1389
+ return false;
1390
+ }
1391
+ return !references_scope_bindings(node, transform_context.available_bindings);
1392
+ }
1393
+
1294
1394
  /**
1295
1395
  * @param {AST.Program} program
1296
1396
  * @returns {AST.Program}
1297
1397
  */
1298
1398
  function expand_component_helpers(program) {
1299
1399
  program.body = program.body.flatMap((statement) => {
1300
- const meta = get_generated_component_metadata(statement);
1301
- const statics = meta?.generated_statics || [];
1302
- const helpers = meta?.generated_helpers || [];
1400
+ const metas = get_generated_component_metadata_list(statement);
1401
+ const statics = metas.flatMap((meta) => meta.generated_statics || []);
1402
+ const helpers = metas.flatMap((meta) => meta.generated_helpers || []);
1303
1403
  if (statics.length || helpers.length) {
1304
1404
  return [...statics, ...helpers, statement];
1305
1405
  }
@@ -1312,30 +1412,63 @@ function expand_component_helpers(program) {
1312
1412
 
1313
1413
  /**
1314
1414
  * Component hooks may replace a `Component` node with a function declaration,
1315
- * variable declaration, or export-safe expression. Generated helper/statics
1316
- * metadata is carried on whichever replacement node the hook returns, so
1317
- * helper expansion must read metadata from that broader set.
1415
+ * variable declaration, object literal member, or export-safe expression.
1416
+ * Generated helper/statics metadata is carried on whichever replacement node
1417
+ * the hook returns, so helper expansion must read metadata from that broader
1418
+ * set.
1318
1419
  *
1319
1420
  * @param {any} node
1320
- * @returns {{ generated_helpers?: any[], generated_statics?: any[] } | null}
1421
+ * @returns {{ generated_helpers?: any[], generated_statics?: any[] }[]}
1321
1422
  */
1322
- function get_generated_component_metadata(node) {
1323
- if (!node || typeof node !== 'object') {
1324
- return null;
1325
- }
1423
+ function get_generated_component_metadata_list(node) {
1424
+ /** @type {{ generated_helpers?: any[], generated_statics?: any[] }[]} */
1425
+ const metas = [];
1426
+ const seen_nodes = new Set();
1427
+ const seen_metas = new Set();
1428
+
1429
+ /** @param {any} current */
1430
+ const visit = (current) => {
1431
+ if (!current || typeof current !== 'object' || seen_nodes.has(current)) {
1432
+ return;
1433
+ }
1326
1434
 
1327
- if (node.metadata?.generated_helpers || node.metadata?.generated_statics) {
1328
- return node.metadata;
1329
- }
1435
+ seen_nodes.add(current);
1330
1436
 
1331
- if (
1332
- (node.type === 'ExportNamedDeclaration' || node.type === 'ExportDefaultDeclaration') &&
1333
- node.declaration?.metadata
1334
- ) {
1335
- return node.declaration.metadata;
1336
- }
1437
+ if (current.metadata?.generated_helpers || current.metadata?.generated_statics) {
1438
+ if (!seen_metas.has(current.metadata)) {
1439
+ seen_metas.add(current.metadata);
1440
+ metas.push(current.metadata);
1441
+ }
1442
+ return;
1443
+ }
1444
+
1445
+ if (
1446
+ current.type === 'FunctionDeclaration' ||
1447
+ current.type === 'FunctionExpression' ||
1448
+ current.type === 'ArrowFunctionExpression'
1449
+ ) {
1450
+ return;
1451
+ }
1452
+
1453
+ for (const key of Object.keys(current)) {
1454
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
1455
+ continue;
1456
+ }
1457
+
1458
+ const value = current[key];
1459
+ if (Array.isArray(value)) {
1460
+ for (const child of value) {
1461
+ visit(child);
1462
+ }
1463
+ } else {
1464
+ visit(value);
1465
+ }
1466
+ }
1467
+ };
1468
+
1469
+ visit(node);
1337
1470
 
1338
- return null;
1471
+ return metas;
1339
1472
  }
1340
1473
 
1341
1474
  /**
@@ -57,18 +57,21 @@ export function assignment_pattern(left, right) {
57
57
  /**
58
58
  * @param {Array<AST.Pattern>} params
59
59
  * @param {AST.BlockStatement | AST.Expression} body
60
+ * @param {AST.NodeWithLocation} [loc_info]
60
61
  * @returns {AST.ArrowFunctionExpression}
61
62
  */
62
- export function arrow(params, body, async = false) {
63
- return {
63
+ export function arrow(params, body, async = false, loc_info) {
64
+ const node = /** @type {AST.ArrowFunctionExpression} */ ({
64
65
  type: 'ArrowFunctionExpression',
65
66
  params,
66
67
  body,
67
68
  expression: body.type !== 'BlockStatement',
68
69
  generator: false,
69
70
  async,
70
- metadata: /** @type {any} */ (null), // should not be used by codegen
71
- };
71
+ metadata: { path: [] },
72
+ });
73
+
74
+ return set_location(node, loc_info);
72
75
  }
73
76
 
74
77
  /**
@@ -1,5 +1,7 @@
1
1
  const ATTR_REGEX = /[&"<]/g;
2
2
  const CONTENT_REGEX = /[&<]/g;
3
+ const OPEN_TAG_REGEX = /</g;
4
+ const CLOSE_TAG_REGEX = />/g;
3
5
 
4
6
  /**
5
7
  * @template V
@@ -24,3 +26,12 @@ export function escape(value, is_attr) {
24
26
 
25
27
  return escaped + str.substring(last);
26
28
  }
29
+
30
+ /**
31
+ * Escapes characters that can prematurely terminate inline script tags.
32
+ * @param {string} str
33
+ * @returns {string}
34
+ */
35
+ export function escape_script(str) {
36
+ return str.replace(OPEN_TAG_REGEX, '\\u003c').replace(CLOSE_TAG_REGEX, '\\u003e');
37
+ }
@@ -1,4 +1,4 @@
1
- /** @import { AddEventObject } from '../../types/index'*/
1
+ /** @import { AddEventObject } from '../../types/index' */
2
2
 
3
3
  const NON_DELEGATED_EVENTS = new Set([
4
4
  'abort',
package/types/index.d.ts CHANGED
@@ -320,12 +320,13 @@ declare module 'estree' {
320
320
  */
321
321
  interface Component extends AST.BaseNode {
322
322
  type: 'Component';
323
- // null is for anonymous components {component: () => {}}
323
+ // null is for anonymous components, e.g. `component(props) => {}`
324
324
  id: AST.Identifier | null;
325
325
  params: AST.Pattern[];
326
326
  body: AST.Node[];
327
327
  css: CSS.StyleSheet | null;
328
328
  metadata: BaseNodeMetaData & {
329
+ arrow?: boolean;
329
330
  topScopedClasses?: TopScopedClasses;
330
331
  styleClasses?: StyleClasses;
331
332
  };
@@ -1499,15 +1500,20 @@ export type StyleClasses = Map<string, AST.MemberExpression['property']>;
1499
1500
  /**
1500
1501
  * Event handling types
1501
1502
  */
1502
- export interface AddEventObject {
1503
+ export interface AddEventOptions extends ExtendedEventOptions {
1503
1504
  customName?: string;
1504
- // from AddEventListenerOptions
1505
+ }
1506
+
1507
+ export interface AddEventObject extends AddEventOptions {
1508
+ handleEvent(object: Event): void;
1509
+ }
1510
+
1511
+ export interface ExtendedEventOptions {
1512
+ capture?: boolean;
1505
1513
  once?: boolean;
1506
1514
  passive?: boolean;
1507
1515
  signal?: AbortSignal;
1508
- capture?: boolean;
1509
- // from EventListenerObject
1510
- handleEvent?(object: Event): void;
1516
+ delegated?: boolean;
1511
1517
  }
1512
1518
 
1513
1519
  /**
@@ -351,4 +351,4 @@ export function componentToFunctionDeclaration(
351
351
  component: any,
352
352
  ctx: any,
353
353
  helperState?: any,
354
- ): AST.FunctionDeclaration;
354
+ ): AST.FunctionDeclaration | AST.FunctionExpression | AST.ArrowFunctionExpression;