@tsrx/core 0.1.26 → 0.1.28

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.26",
6
+ "version": "0.1.28",
7
7
  "type": "module",
8
8
  "repository": {
9
9
  "type": "git",
package/src/plugin.js CHANGED
@@ -55,27 +55,6 @@ const CharCode = Object.freeze({
55
55
  closeBrace: 125,
56
56
  });
57
57
 
58
- /**
59
- * Keywords after which a `/` begins a regex literal rather than division, used
60
- * by the look-ahead scanners to track expression position in script content.
61
- */
62
- const REGEX_PRECEDING_KEYWORDS = new Set([
63
- 'return',
64
- 'typeof',
65
- 'instanceof',
66
- 'in',
67
- 'of',
68
- 'new',
69
- 'delete',
70
- 'void',
71
- 'do',
72
- 'else',
73
- 'yield',
74
- 'await',
75
- 'case',
76
- 'throw',
77
- ]);
78
-
79
58
  // Transparent wrappers to look through when validating a dynamic tag
80
59
  // expression (`<{expr}>`), and syntax that disqualifies one outright.
81
60
  const DYNAMIC_TAG_WRAPPER_TYPES = new Set([
@@ -568,7 +547,7 @@ export function TSRXPlugin(config) {
568
547
  let index = start;
569
548
  let value = '';
570
549
  while (index < this.input.length) {
571
- if (this.#isTemplateLineCommentStart(index)) {
550
+ if (this.#isTemplateLineCommentStart(index, start)) {
572
551
  const comment_start = index;
573
552
  const comment_start_loc = acorn.getLineInfo(this.input, comment_start);
574
553
  index += 2;
@@ -596,11 +575,32 @@ export function TSRXPlugin(config) {
596
575
  }
597
576
  continue;
598
577
  }
578
+ if (this.#isTemplateBlockCommentStart(index)) {
579
+ const comment_start = index;
580
+ const comment_start_loc = acorn.getLineInfo(this.input, comment_start);
581
+ const close = this.input.indexOf('*/', index + 2);
582
+ const value_end = close === -1 ? this.input.length : close;
583
+ index = close === -1 ? this.input.length : close + 2;
584
+ if (this.options.onComment && comment_start >= token_end) {
585
+ const comment_end_loc = acorn.getLineInfo(this.input, index);
586
+ this.options.onComment(
587
+ true,
588
+ this.input.slice(comment_start + 2, value_end),
589
+ comment_start,
590
+ index,
591
+ new acorn.Position(comment_start_loc.line, comment_start_loc.column),
592
+ new acorn.Position(comment_end_loc.line, comment_end_loc.column),
593
+ /** @type {any} */ (null),
594
+ );
595
+ }
596
+ continue;
597
+ }
599
598
  const ch = this.input.charCodeAt(index);
600
599
  if (
601
600
  ch === CharCode.lessThan ||
602
601
  ch === CharCode.openBrace ||
603
602
  ch === CharCode.closeBrace ||
603
+ this.#isCodeBlockStart(index) ||
604
604
  this.#isJSXControlFlowDirectiveAt(index)
605
605
  ) {
606
606
  break;
@@ -962,13 +962,41 @@ export function TSRXPlugin(config) {
962
962
  }
963
963
 
964
964
  /**
965
+ * A `//` is a comment only when nothing but whitespace precedes it on its
966
+ * line, or — given `run_start`, the position where the current text run
967
+ * began (right after a sibling element, code block, or expression
968
+ * container) — since that boundary. Once real text has begun, `//` is
969
+ * literal so inline text like `https://…` stays text.
965
970
  * @param {number} index
971
+ * @param {number} [run_start]
966
972
  */
967
- #isTemplateLineCommentStart(index) {
973
+ #isTemplateLineCommentStart(index, run_start = -1) {
974
+ if (
975
+ this.input.charCodeAt(index) !== CharCode.slash ||
976
+ this.input.charCodeAt(index + 1) !== CharCode.slash
977
+ ) {
978
+ return false;
979
+ }
980
+ if (this.#isLineStartPosition(index)) return true;
981
+ if (run_start < 0) return false;
982
+ for (let i = index - 1; i >= run_start; i--) {
983
+ const ch = this.input.charCodeAt(i);
984
+ if (ch === CharCode.lineFeed || ch === CharCode.carriageReturn) return false;
985
+ if (ch !== CharCode.space && ch !== CharCode.tab) return false;
986
+ }
987
+ return true;
988
+ }
989
+
990
+ /**
991
+ * Unlike `//` (which is only a comment at line-start so inline text like
992
+ * `https://…` stays text), `/*` starts a comment anywhere in template
993
+ * text, matching `jsx_readToken`.
994
+ * @param {number} index
995
+ */
996
+ #isTemplateBlockCommentStart(index) {
968
997
  return (
969
998
  this.input.charCodeAt(index) === CharCode.slash &&
970
- this.input.charCodeAt(index + 1) === CharCode.slash &&
971
- this.#isLineStartPosition(index)
999
+ this.input.charCodeAt(index + 1) === CharCode.asterisk
972
1000
  );
973
1001
  }
974
1002
 
@@ -984,7 +1012,8 @@ export function TSRXPlugin(config) {
984
1012
  ch === CharCode.openBrace ||
985
1013
  ch === CharCode.closeBrace ||
986
1014
  this.#isJSXControlFlowDirectiveAt(index) ||
987
- this.#isTemplateLineCommentStart(index)
1015
+ this.#isTemplateLineCommentStart(index, start) ||
1016
+ this.#isTemplateBlockCommentStart(index)
988
1017
  ) {
989
1018
  break;
990
1019
  }
@@ -2005,9 +2034,10 @@ export function TSRXPlugin(config) {
2005
2034
  );
2006
2035
  const closingEnd = closingStart + '</style>'.length;
2007
2036
  const closingEndInfo = acorn.getLineInfo(this.input, closingEnd);
2008
- const closingElement = /** @type {ESTreeJSX.JSXClosingElement & AST.NodeWithLocation} */ (
2009
- this.startNodeAt(closingStart, closingStartLoc)
2010
- );
2037
+ const closingElement =
2038
+ /** @type {ESTreeJSX.TSRXJSXClosingElement & AST.NodeWithLocation} */ (
2039
+ this.startNodeAt(closingStart, closingStartLoc)
2040
+ );
2011
2041
  closingElement.name = name;
2012
2042
  this.finishNodeAt(
2013
2043
  closingElement,
@@ -2700,7 +2730,10 @@ export function TSRXPlugin(config) {
2700
2730
  this.pos++;
2701
2731
  return this.finishToken(tt.arrow);
2702
2732
  }
2703
- if (code === CharCode.lessThan) {
2733
+ if (code === CharCode.lessThan && this.type !== tstt.jsxText) {
2734
+ // After a JSX text token a `<` can only open a tag; without this guard
2735
+ // text ending in an identifier character (`hello<div>`) would read as
2736
+ // the start of a type argument list (`hello<T>`).
2704
2737
  const next = this.input.charCodeAt(this.pos + 1);
2705
2738
  if (
2706
2739
  next !== CharCode.slash &&
@@ -4208,8 +4241,10 @@ export function TSRXPlugin(config) {
4208
4241
  } else {
4209
4242
  if (is_style) {
4210
4243
  /** @type {AST.JSXStyleElement} */ (node).type = 'JSXStyleElement';
4211
- /** @type {AST.JSXStyleElement} */ (node).openingElement = open;
4212
- /** @type {AST.JSXStyleElement} */ (node).closingElement = null;
4244
+ /** @type {AST.JSXStyleElement} */ (node).openingElement =
4245
+ /** @type {AST.JSXStyleElement['openingElement']} */ (open);
4246
+ /** @type {AST.JSXStyleElement} */ (node).closingElement =
4247
+ /** @type {AST.JSXStyleElement['closingElement']} */ (null);
4213
4248
  } else {
4214
4249
  /** @type {ESTreeJSX.JSXElement} */ (node).type = 'JSXElement';
4215
4250
  /** @type {ESTreeJSX.JSXElement} */ (node).openingElement = open;
@@ -4,20 +4,19 @@
4
4
  import tsx from 'esrap/languages/tsx';
5
5
 
6
6
  /**
7
- * Zimmerframe provides `path` as the ancestor chain. A native template node whose
8
- * parent is another native template node renders as a JSX child; anywhere else it
9
- * renders as a standalone expression (e.g. a return value).
7
+ * Zimmerframe provides `path` as the ancestor chain. A native template node in
8
+ * the children list of any JSX element/fragment renders as a JSX child;
9
+ * anywhere else it renders as a standalone expression (e.g. a return value).
10
+ * The parent may be a parsed native template node or a synthetic fragment the
11
+ * transform built around render children — either way a bare expression in a
12
+ * child slot would print as JSX text.
10
13
  *
11
14
  * @param {any[]} path
12
15
  * @returns {boolean}
13
16
  */
14
17
  export function in_jsx_child_context(path) {
15
18
  const parent = path[path.length - 1];
16
- return (
17
- !!parent &&
18
- (parent.type === 'JSXElement' || parent.type === 'JSXFragment') &&
19
- parent.metadata?.native_tsrx
20
- );
19
+ return !!parent && (parent.type === 'JSXElement' || parent.type === 'JSXFragment');
21
20
  }
22
21
 
23
22
  /**
@@ -146,10 +146,57 @@ function mark_nested_function_return_jsx(node, inside_function = false, seen = n
146
146
  }
147
147
 
148
148
  /**
149
- * Flatten a `@{ … }` code block that appears as an element/fragment child into
150
- * the element's children list: its setup statements followed by its single
151
- * render output. The render pipeline already handles interleaved setup
152
- * statements and JSX children. This is the element-scoped equivalent of
149
+ * Lower a `@{ … }` code block that appears as an element/fragment child,
150
+ * paying only for what the block uses while keeping each block its own
151
+ * lexical scope:
152
+ *
153
+ * - no setup code: the scope is unobservable, so the render output merges
154
+ * directly into the children list (template-only chains collapse to the
155
+ * innermost output, empty chains to nothing);
156
+ * - code-only: a plain `{ … }` statement block — statements run in source
157
+ * order, scoped, and render nothing (the render pipeline already handles
158
+ * statements interleaved with JSX children);
159
+ * - setup code + render output: kept as a `JSXCodeBlock` (with any nested
160
+ * chain simplified) for the context-aware lowering into a scoped IIFE
161
+ * (`transform_jsx_code_block` / `build_render_statements`).
162
+ *
163
+ * Always returns zero or one node.
164
+ * @param {any} block
165
+ * @returns {any[]}
166
+ */
167
+ function lower_code_block_child(block) {
168
+ const body = block.body || [];
169
+ const render = block.render ?? null;
170
+
171
+ if (body.length === 0) {
172
+ if (render == null) return [];
173
+ if (render.type === 'JSXCodeBlock') return lower_code_block_child(render);
174
+ return [render];
175
+ }
176
+
177
+ if (render?.type === 'JSXCodeBlock') {
178
+ const inner = lower_code_block_child(render);
179
+ if (inner.length === 0) {
180
+ return [b.block(body, block)];
181
+ }
182
+ if (inner[0].type === 'BlockStatement') {
183
+ return [b.block([...body, inner[0]], block)];
184
+ }
185
+ // The chain still renders — simplify the render to the lowered inner
186
+ // node and leave the block for the context-aware lowering.
187
+ return [{ ...block, render: inner[0] }];
188
+ }
189
+
190
+ if (render == null) {
191
+ return [b.block(body, block)];
192
+ }
193
+
194
+ return [block];
195
+ }
196
+
197
+ /**
198
+ * Lower `@{ … }` code blocks that appear as element/fragment children (see
199
+ * `lower_code_block_child`). This is the element-scoped equivalent of
153
200
  * `transform_function`'s body lowering — function and arrow bodies are never
154
201
  * element children, so they are untouched here.
155
202
  * @param {any} node
@@ -170,9 +217,7 @@ function expand_child_code_blocks(node, seen = new Set()) {
170
217
  node.children.some((/** @type {any} */ c) => c?.type === 'JSXCodeBlock')
171
218
  ) {
172
219
  node.children = node.children.flatMap((/** @type {any} */ child) =>
173
- child?.type === 'JSXCodeBlock'
174
- ? [...child.body, ...(child.render != null ? [child.render] : [])]
175
- : [child],
220
+ child?.type === 'JSXCodeBlock' ? lower_code_block_child(child) : [child],
176
221
  );
177
222
  }
178
223
 
@@ -801,6 +846,79 @@ function build_component_statements(body_nodes, transform_context) {
801
846
  return build_render_statements(body_nodes, false, transform_context);
802
847
  }
803
848
 
849
+ /**
850
+ * Statements for one `@{ … }` scope level: the setup statements followed by
851
+ * the lowered chain continuation. A nested level that declares anything is
852
+ * kept in a nested plain `{ … }` block, so a whole chain shares a single
853
+ * closure while still scoping each level; the generated `return` exits that
854
+ * closure.
855
+ * @param {any} block
856
+ * @param {TransformContext} transform_context
857
+ * @returns {{ statements: any[], has_render: boolean }}
858
+ */
859
+ function code_block_scope_statements(block, transform_context) {
860
+ const statements = [...(block.body || [])];
861
+ const render = block.render ?? null;
862
+
863
+ if (render == null) {
864
+ return { statements, has_render: false };
865
+ }
866
+
867
+ if (render.type === 'JSXCodeBlock') {
868
+ const inner = code_block_scope_statements(render, transform_context);
869
+ if (inner.statements.length > 0) {
870
+ if ((render.body || []).length > 0) {
871
+ statements.push(b.block(inner.statements, render));
872
+ } else {
873
+ statements.push(...inner.statements);
874
+ }
875
+ }
876
+ return { statements, has_render: inner.has_render };
877
+ }
878
+
879
+ return {
880
+ statements: [...statements, ...build_render_statements([render], true, transform_context)],
881
+ has_render: true,
882
+ };
883
+ }
884
+
885
+ /**
886
+ * Lower a `@{ … }` code block that appears in a component/IIFE statement
887
+ * stream, keeping each block its own lexical scope:
888
+ *
889
+ * - no setup code: the scope is unobservable, so the render output (if any)
890
+ * merges directly into the stream;
891
+ * - code-only: a plain `{ … }` statement block;
892
+ * - setup code + render output: a scoped IIFE expression child whose value is
893
+ * the render output, with nested chains folded into the one closure.
894
+ *
895
+ * Always returns zero or one node.
896
+ * @param {any} block
897
+ * @param {TransformContext} transform_context
898
+ * @returns {any[]}
899
+ */
900
+ function lower_code_block_stream_node(block, transform_context) {
901
+ const body = block.body || [];
902
+ const render = block.render ?? null;
903
+
904
+ if (body.length === 0) {
905
+ if (render == null) return [];
906
+ if (render.type === 'JSXCodeBlock') {
907
+ return lower_code_block_stream_node(render, transform_context);
908
+ }
909
+ return [render];
910
+ }
911
+
912
+ const { statements, has_render } = code_block_scope_statements(block, transform_context);
913
+
914
+ if (!has_render) {
915
+ return [b.block(statements, block)];
916
+ }
917
+
918
+ const iife = b.call(b.arrow([], b.block(statements, block)));
919
+ return [to_jsx_expression_container(iife, block)];
920
+ }
921
+
804
922
  /**
805
923
  * @param {any[]} body_nodes
806
924
  * @param {boolean} return_null_when_empty
@@ -809,9 +927,7 @@ function build_component_statements(body_nodes, transform_context) {
809
927
  */
810
928
  function build_render_statements(body_nodes, return_null_when_empty, transform_context) {
811
929
  body_nodes = body_nodes.flatMap((node) =>
812
- node?.type === 'JSXCodeBlock'
813
- ? [...node.body, ...(node.render != null ? [node.render] : [])]
814
- : [node],
930
+ node?.type === 'JSXCodeBlock' ? lower_code_block_stream_node(node, transform_context) : [node],
815
931
  );
816
932
 
817
933
  const statements = [];
@@ -1249,6 +1249,44 @@ export function jsx_fragment(children = [], attributes = []) {
1249
1249
  };
1250
1250
  }
1251
1251
 
1252
+ /**
1253
+ * Ripple's internal fragment template node (the normalized form of a
1254
+ * `JSXFragment`).
1255
+ * @param {AST.Node[]} [children]
1256
+ * @param {AST.NodeWithLocation} [loc_info]
1257
+ * @returns {AST.TsrxFragment}
1258
+ */
1259
+ export function tsrx_fragment(children = [], loc_info) {
1260
+ const node = /** @type {AST.TsrxFragment} */ (
1261
+ /** @type {unknown} */ ({
1262
+ type: 'TsrxFragment',
1263
+ children,
1264
+ attributes: [],
1265
+ selfClosing: false,
1266
+ metadata: { path: [] },
1267
+ })
1268
+ );
1269
+
1270
+ return set_location(node, loc_info);
1271
+ }
1272
+
1273
+ /**
1274
+ * Ripple's internal expression template child (the normalized form of a
1275
+ * `JSXExpressionContainer` child).
1276
+ * @param {AST.Expression} expression
1277
+ * @param {AST.NodeWithLocation} [loc_info]
1278
+ * @returns {AST.TSRXExpression}
1279
+ */
1280
+ export function tsrx_expression(expression, loc_info) {
1281
+ const node = /** @type {AST.TSRXExpression} */ ({
1282
+ type: 'TSRXExpression',
1283
+ expression,
1284
+ metadata: { path: [] },
1285
+ });
1286
+
1287
+ return set_location(node, loc_info);
1288
+ }
1289
+
1252
1290
  /**
1253
1291
  * @param {AST.Expression | ESTreeJSX.JSXEmptyExpression} expression
1254
1292
  * @param {AST.NodeWithLocation} [loc_info]
package/types/index.d.ts CHANGED
@@ -170,6 +170,17 @@ declare module 'estree' {
170
170
  interface SimpleCallExpression {
171
171
  metadata: BaseNodeMetaData & {
172
172
  hash?: string;
173
+ /**
174
+ * A generated `(() => @{ … })()` inline-component IIFE for a code
175
+ * block; collapsible once the block's statements lower into the
176
+ * component callback.
177
+ */
178
+ tsrx_code_block_component?: boolean;
179
+ /**
180
+ * A generated zero-argument scope IIFE for a `@{ … }` code-block
181
+ * chain level; runs synchronously inside its `with_scope` wrapper.
182
+ */
183
+ tsrx_code_block_scope?: boolean;
173
184
  };
174
185
  }
175
186
 
@@ -253,6 +264,9 @@ declare module 'estree' {
253
264
  TsrxFragment: TsrxFragment;
254
265
  Text: Text;
255
266
  TSRXJSXElement: TSRXJSXElement;
267
+ TSRXJSXFragment: TSRXJSXFragment;
268
+ TSRXJSXOpeningElement: ESTreeJSX.TSRXJSXOpeningElement;
269
+ TSRXJSXClosingElement: ESTreeJSX.TSRXJSXClosingElement;
256
270
  TSRXExpression: TSRXExpression;
257
271
  Attribute: Attribute;
258
272
  SpreadAttribute: SpreadAttribute;
@@ -320,7 +334,14 @@ declare module 'estree' {
320
334
  closingElement?: ESTreeJSX.JSXClosingFragment | null;
321
335
  selfClosing?: boolean;
322
336
  attributes?: Array<Attribute | SpreadAttribute>;
323
- metadata: BaseNodeMetaData;
337
+ metadata: BaseNodeMetaData & {
338
+ /**
339
+ * A synthetic wrapper for a nested code-block render chain
340
+ * (`@{ @{ … } }`), so render-slot consumers see a template node;
341
+ * template-children lowering unwraps it.
342
+ */
343
+ tsrx_code_block_chain?: boolean;
344
+ };
324
345
  start: number;
325
346
  end: number;
326
347
  }
@@ -346,7 +367,11 @@ declare module 'estree' {
346
367
  | AST.JSXCodeBlock;
347
368
 
348
369
  interface TSRXJSXElement
349
- extends Omit<ESTreeJSX.JSXElement, 'children'>, AST.NodeWithMaybeComments {
370
+ extends
371
+ Omit<ESTreeJSX.JSXElement, 'children' | 'openingElement' | 'closingElement'>,
372
+ AST.NodeWithMaybeComments {
373
+ openingElement: ESTreeJSX.TSRXJSXOpeningElement;
374
+ closingElement: ESTreeJSX.TSRXJSXClosingElement | null;
350
375
  children: TSRXJSXChild[];
351
376
  metadata: BaseNodeMetaData & {
352
377
  ts_name?: string;
@@ -366,13 +391,10 @@ declare module 'estree' {
366
391
  innerComments?: AST.Comment[] | undefined;
367
392
  }
368
393
 
369
- interface JSXStyleElement extends AST.BaseExpression {
394
+ interface JSXStyleElement extends Omit<AST.TSRXJSXElement, 'type' | 'children'> {
370
395
  type: 'JSXStyleElement';
371
- openingElement: ESTreeJSX.JSXOpeningElement;
372
- closingElement: ESTreeJSX.JSXClosingElement | null;
373
396
  children: AST.CSS.StyleSheet[];
374
397
  css?: string;
375
- metadata: BaseNodeMetaData;
376
398
  unclosed?: boolean;
377
399
  }
378
400
 
@@ -520,7 +542,7 @@ declare module 'estree' {
520
542
 
521
543
  type TSRXStatement = AST.Statement | TSESTree.Statement;
522
544
 
523
- type NodeWithChildren = TSRXJSXElement | TSRXJSXFragment | JSXStyleElement;
545
+ type NodeWithChildren = TSRXJSXElement | TSRXJSXFragment | JSXStyleElement | ESTreeJSX.JSXElement;
524
546
 
525
547
  export namespace CSS {
526
548
  export interface BaseNode extends AST.NodeWithMaybeComments {
@@ -736,11 +758,11 @@ declare module 'estree-jsx' {
736
758
  }
737
759
 
738
760
  interface TSRXJSXOpeningElement extends Omit<JSXOpeningElement, 'name'> {
739
- name: AST.MemberExpression | JSXIdentifier | JSXNamespacedName;
761
+ name: AST.MemberExpression | JSXIdentifier | JSXNamespacedName | JSXExpressionContainer;
740
762
  }
741
763
 
742
764
  interface TSRXJSXClosingElement extends Omit<JSXClosingElement, 'name'> {
743
- name: AST.MemberExpression | JSXIdentifier | JSXNamespacedName;
765
+ name: AST.MemberExpression | JSXIdentifier | JSXNamespacedName | JSXExpressionContainer;
744
766
  }
745
767
 
746
768
  interface ExpressionMap {