@tsrx/core 0.1.27 → 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.27",
6
+ "version": "0.1.28",
7
7
  "type": "module",
8
8
  "repository": {
9
9
  "type": "git",
package/src/plugin.js CHANGED
@@ -547,7 +547,7 @@ export function TSRXPlugin(config) {
547
547
  let index = start;
548
548
  let value = '';
549
549
  while (index < this.input.length) {
550
- if (this.#isTemplateLineCommentStart(index)) {
550
+ if (this.#isTemplateLineCommentStart(index, start)) {
551
551
  const comment_start = index;
552
552
  const comment_start_loc = acorn.getLineInfo(this.input, comment_start);
553
553
  index += 2;
@@ -600,6 +600,7 @@ export function TSRXPlugin(config) {
600
600
  ch === CharCode.lessThan ||
601
601
  ch === CharCode.openBrace ||
602
602
  ch === CharCode.closeBrace ||
603
+ this.#isCodeBlockStart(index) ||
603
604
  this.#isJSXControlFlowDirectiveAt(index)
604
605
  ) {
605
606
  break;
@@ -961,14 +962,29 @@ export function TSRXPlugin(config) {
961
962
  }
962
963
 
963
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.
964
970
  * @param {number} index
971
+ * @param {number} [run_start]
965
972
  */
966
- #isTemplateLineCommentStart(index) {
967
- return (
968
- this.input.charCodeAt(index) === CharCode.slash &&
969
- this.input.charCodeAt(index + 1) === CharCode.slash &&
970
- this.#isLineStartPosition(index)
971
- );
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;
972
988
  }
973
989
 
974
990
  /**
@@ -996,7 +1012,7 @@ export function TSRXPlugin(config) {
996
1012
  ch === CharCode.openBrace ||
997
1013
  ch === CharCode.closeBrace ||
998
1014
  this.#isJSXControlFlowDirectiveAt(index) ||
999
- this.#isTemplateLineCommentStart(index) ||
1015
+ this.#isTemplateLineCommentStart(index, start) ||
1000
1016
  this.#isTemplateBlockCommentStart(index)
1001
1017
  ) {
1002
1018
  break;
@@ -2714,7 +2730,10 @@ export function TSRXPlugin(config) {
2714
2730
  this.pos++;
2715
2731
  return this.finishToken(tt.arrow);
2716
2732
  }
2717
- 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>`).
2718
2737
  const next = this.input.charCodeAt(this.pos + 1);
2719
2738
  if (
2720
2739
  next !== CharCode.slash &&
@@ -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
 
@@ -323,7 +334,14 @@ declare module 'estree' {
323
334
  closingElement?: ESTreeJSX.JSXClosingFragment | null;
324
335
  selfClosing?: boolean;
325
336
  attributes?: Array<Attribute | SpreadAttribute>;
326
- 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
+ };
327
345
  start: number;
328
346
  end: number;
329
347
  }