@tsrx/core 0.0.22 → 0.0.23

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/README.md CHANGED
@@ -42,7 +42,7 @@ The TSRX website is the canonical source for language documentation:
42
42
  - [Getting Started](https://tsrx.dev/getting-started) — install TSRX for React,
43
43
  Preact, Solid, Vue, or Ripple and configure editor/AI tooling.
44
44
  - [Features](https://tsrx.dev/features) — examples of components, statement
45
- templates, control flow, scoped styles, server blocks, and lazy destructuring.
45
+ templates, control flow, scoped styles, submodules, and lazy destructuring.
46
46
  - [Specification](https://tsrx.dev/specification) — the current grammar and
47
47
  parser-level semantics.
48
48
 
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.22",
6
+ "version": "0.0.23",
7
7
  "type": "module",
8
8
  "repository": {
9
9
  "type": "git",
@@ -1,6 +1,5 @@
1
1
  export const IDENTIFIER_OBFUSCATION_PREFIX = '_$_';
2
- export const STYLE_IDENTIFIER = IDENTIFIER_OBFUSCATION_PREFIX + encode_utf16_char('#') + 'style';
3
- export const SERVER_IDENTIFIER = IDENTIFIER_OBFUSCATION_PREFIX + encode_utf16_char('#') + 'server';
2
+ export const SERVER_IDENTIFIER = IDENTIFIER_OBFUSCATION_PREFIX + 'server_$_';
4
3
  export const CSS_HASH_IDENTIFIER = IDENTIFIER_OBFUSCATION_PREFIX + 'hash';
5
4
 
6
5
  const DECODE_UTF16_REGEX = /_u([0-9a-fA-F]{4})_/g;
package/src/index.js CHANGED
@@ -50,7 +50,6 @@ export {
50
50
  // Identifier utils
51
51
  export {
52
52
  IDENTIFIER_OBFUSCATION_PREFIX,
53
- STYLE_IDENTIFIER,
54
53
  SERVER_IDENTIFIER,
55
54
  CSS_HASH_IDENTIFIER,
56
55
  obfuscate_identifier as obfuscateIdentifier,
package/src/plugin.js CHANGED
@@ -189,7 +189,7 @@ function previous_word_before(input, pos) {
189
189
  /**
190
190
  * Acorn parser plugin for Ripple syntax extensions.
191
191
  * Adds support for: component declarations, &[]/&{} lazy destructuring,
192
- * #server blocks, #style identifiers, and enhanced JSX handling.
192
+ * submodule imports, TSRX directives, and enhanced JSX handling.
193
193
  *
194
194
  * @param {import('../types/index').TSRXPluginConfig} [config] - Plugin configuration
195
195
  * @returns {(Parser: Parse.ParserConstructor) => Parse.ParserConstructor} Parser extension function
@@ -221,6 +221,22 @@ export function TSRXPlugin(config) {
221
221
  #filename = null;
222
222
  #functionBodyDepth = 0;
223
223
 
224
+ /**
225
+ * @type {Parse.Parser['finishNode']}
226
+ */
227
+ finishNode(node, type) {
228
+ const finished = super.finishNode(node, type);
229
+ if (type === 'TSModuleDeclaration') {
230
+ const start = /** @type {number} */ (finished.start);
231
+ const source = this.input.slice(start, start + 'namespace'.length);
232
+ finished.metadata ??= { path: [] };
233
+ finished.metadata.module_keyword = source.startsWith('namespace')
234
+ ? 'namespace'
235
+ : 'module';
236
+ }
237
+ return finished;
238
+ }
239
+
224
240
  /**
225
241
  * @param {Parse.Options} options
226
242
  * @param {string} input
@@ -234,6 +250,13 @@ export function TSRXPlugin(config) {
234
250
  this.#filename = tsrx_options?.filename || null;
235
251
  }
236
252
 
253
+ #resetTokenStartToCurrentPosition() {
254
+ if (this.start !== this.pos) {
255
+ this.start = this.pos;
256
+ this.startLoc = this.curPosition();
257
+ }
258
+ }
259
+
237
260
  #previousNonWhitespaceChar() {
238
261
  let index = this.pos - 1;
239
262
  while (index >= 0) {
@@ -820,44 +843,6 @@ export function TSRXPlugin(config) {
820
843
  }
821
844
  }
822
845
 
823
- if (code === 35) {
824
- // # character
825
- if (this.pos + 1 < this.input.length) {
826
- /** @param {string} value */
827
- const startsWith = (value) =>
828
- this.input.slice(this.pos, this.pos + value.length) === value;
829
- /** @param {number} length */
830
- const char_after = (length) =>
831
- this.pos + length < this.input.length ? this.input.charCodeAt(this.pos + length) : -1;
832
- /** @param {number} ch */
833
- const is_ripple_delimiter = (ch) =>
834
- ch === 40 || // (
835
- ch === 41 || // )
836
- ch === 60 || // <
837
- ch === 46 || // .
838
- ch === 44 || // ,
839
- ch === 59 || // ;
840
- ch === 91 || // [
841
- ch === 93 || // ]
842
- ch === 123 || // {
843
- ch === 125 || // }
844
- ch === 32 || // space
845
- ch === 9 || // tab
846
- ch === 10 || // newline
847
- ch === 13 || // carriage return
848
- ch === -1; // EOF
849
-
850
- if (startsWith('#server') && is_ripple_delimiter(char_after(7))) {
851
- this.pos += 7;
852
- return this.finishToken(tt.name, '#server');
853
- }
854
-
855
- if (startsWith('#style') && is_ripple_delimiter(char_after(6))) {
856
- this.pos += 6;
857
- return this.finishToken(tt.name, '#style');
858
- }
859
- }
860
- }
861
846
  this.#allowTagStartAfterDoubleQuotedText = false;
862
847
  return super.getTokenFromCode(code);
863
848
  }
@@ -970,22 +955,6 @@ export function TSRXPlugin(config) {
970
955
  * @type {Parse.Parser['parseExprAtom']}
971
956
  */
972
957
  parseExprAtom(refDestructuringErrors, forNew, forInit) {
973
- const lookahead_type = this.lookahead().type;
974
- const is_next_call_token = lookahead_type === tt.parenL || lookahead_type === tt.relational;
975
-
976
- // Check if this is #server identifier for server function calls
977
- if (this.type === tt.name && this.value === '#server') {
978
- const node = this.startNode();
979
- this.next();
980
- return /** @type {AST.ServerIdentifier} */ (this.finishNode(node, 'ServerIdentifier'));
981
- }
982
-
983
- if (this.type === tt.name && this.value === '#style') {
984
- const node = this.startNode();
985
- this.next();
986
- return /** @type {AST.StyleIdentifier} */ (this.finishNode(node, 'StyleIdentifier'));
987
- }
988
-
989
958
  // Check if this is a component expression (e.g., in object literal values)
990
959
  if (this.type === tt.name && this.value === 'component') {
991
960
  return this.parseComponent();
@@ -1015,9 +984,9 @@ export function TSRXPlugin(config) {
1015
984
 
1016
985
  /**
1017
986
  * Override checkLocalExport to check all scopes in the scope stack.
1018
- * This is needed because server blocks create nested scopes, but exports
1019
- * from within server blocks should still be valid if the identifier is
1020
- * declared in the server block's scope (not just the top-level module scope).
987
+ * This is needed because submodules create nested scopes, but exports
988
+ * from within submodules should still be valid if the identifier is
989
+ * declared in the submodule scope (not just the top-level module scope).
1021
990
  * @type {Parse.Parser['checkLocalExport']}
1022
991
  */
1023
992
  checkLocalExport(id) {
@@ -1036,31 +1005,6 @@ export function TSRXPlugin(config) {
1036
1005
  this.undefinedExports[name] = id;
1037
1006
  }
1038
1007
 
1039
- /**
1040
- * @type {Parse.Parser['parseServerBlock']}
1041
- */
1042
- parseServerBlock() {
1043
- const node = /** @type {AST.ServerBlock} */ (this.startNode());
1044
- this.next();
1045
-
1046
- const body = /** @type {AST.ServerBlockStatement} */ (this.startNode());
1047
- node.body = body;
1048
- body.body = [];
1049
-
1050
- this.expect(tt.braceL);
1051
- this.enterScope(0);
1052
- while (this.type !== tt.braceR) {
1053
- const stmt = /** @type {AST.Statement} */ (this.parseStatement(null, true));
1054
- body.body.push(stmt);
1055
- }
1056
- this.next();
1057
- this.exitScope();
1058
- this.finishNode(body, 'BlockStatement');
1059
-
1060
- this.awaitPos = 0;
1061
- return this.finishNode(node, 'ServerBlock');
1062
- }
1063
-
1064
1008
  /**
1065
1009
  * Parse a component - common implementation used by statements, expressions, and export defaults
1066
1010
  * @type {Parse.Parser['parseComponent']}
@@ -1415,10 +1359,26 @@ export function TSRXPlugin(config) {
1415
1359
  '"text" is a TSRX keyword and must be used in the form {text some_value}',
1416
1360
  );
1417
1361
  }
1362
+ } else if (
1363
+ this.type === tt.name &&
1364
+ this.value === 'style' &&
1365
+ this.lookahead().type === tt.string
1366
+ ) {
1367
+ node.style = true;
1368
+ this.next();
1418
1369
  }
1419
1370
 
1420
1371
  node.expression =
1421
1372
  this.type === tt.braceR ? this.jsx_parseEmptyExpression() : this.parseExpression();
1373
+ if (
1374
+ node.style &&
1375
+ (node.expression.type !== 'Literal' || typeof node.expression.value !== 'string')
1376
+ ) {
1377
+ this.raise(
1378
+ /** @type {number} */ (node.expression.start),
1379
+ '"style" is a TSRX keyword and must be used in the form {style "class_name"}',
1380
+ );
1381
+ }
1422
1382
  this.expect(tt.braceR);
1423
1383
 
1424
1384
  return this.finishNode(node, 'JSXExpressionContainer');
@@ -1812,6 +1772,7 @@ export function TSRXPlugin(config) {
1812
1772
  break;
1813
1773
  }
1814
1774
  // If not a comment, fall through to default case
1775
+ this.#resetTokenStartToCurrentPosition();
1815
1776
  this.context.push(b_stat);
1816
1777
  this.exprAllowed = true;
1817
1778
  return original.readToken.call(this, ch);
@@ -1831,6 +1792,7 @@ export function TSRXPlugin(config) {
1831
1792
  this.#path.at(-1)?.type === 'Component' ||
1832
1793
  this.#path.at(-1)?.type === 'Element')
1833
1794
  ) {
1795
+ this.#resetTokenStartToCurrentPosition();
1834
1796
  return original.readToken.call(this, ch);
1835
1797
  }
1836
1798
  this.raise(
@@ -1855,6 +1817,7 @@ export function TSRXPlugin(config) {
1855
1817
  } else if (ch === 32 || ch === 9) {
1856
1818
  ++this.pos;
1857
1819
  } else {
1820
+ this.#resetTokenStartToCurrentPosition();
1858
1821
  this.context.push(b_stat);
1859
1822
  this.exprAllowed = true;
1860
1823
  return original.readToken.call(this, ch);
@@ -1986,6 +1949,14 @@ export function TSRXPlugin(config) {
1986
1949
  if (attr.value !== null) {
1987
1950
  if (attr.value.type === 'JSXExpressionContainer') {
1988
1951
  const expression = attr.value.expression;
1952
+ if (attr.value.style) {
1953
+ /** @type {AST.Style} */ (/** @type {unknown} */ (attr.value)).type = 'Style';
1954
+ /** @type {AST.Style} */ (/** @type {unknown} */ (attr.value)).value =
1955
+ /** @type {AST.Literal} */ (expression);
1956
+ delete (/** @type {any} */ (attr.value).expression);
1957
+ delete (/** @type {any} */ (attr.value).style);
1958
+ continue;
1959
+ }
1989
1960
  if (expression.type === 'Literal') {
1990
1961
  expression.was_expression = true;
1991
1962
  }
@@ -2455,11 +2426,23 @@ export function TSRXPlugin(config) {
2455
2426
  // Keep JSXEmptyExpression as-is (for prettier to handle comments)
2456
2427
  // but convert other expressions to Html/TSRXExpression/Text nodes
2457
2428
  if (node.expression.type !== 'JSXEmptyExpression') {
2458
- /** @type {AST.TSRXExpression | AST.Html | AST.TextNode} */ (
2429
+ /** @type {AST.TSRXExpression | AST.Html | AST.TextNode | AST.Style} */ (
2459
2430
  /** @type {unknown} */ (node)
2460
- ).type = node.html ? 'Html' : node.text ? 'Text' : 'TSRXExpression';
2431
+ ).type = node.html
2432
+ ? 'Html'
2433
+ : node.text
2434
+ ? 'Text'
2435
+ : node.style
2436
+ ? 'Style'
2437
+ : 'TSRXExpression';
2438
+ if (node.style) {
2439
+ /** @type {AST.Style} */ (/** @type {unknown} */ (node)).value =
2440
+ /** @type {AST.Literal} */ (node.expression);
2441
+ delete (/** @type {any} */ (node).expression);
2442
+ }
2461
2443
  delete node.html;
2462
2444
  delete node.text;
2445
+ delete node.style;
2463
2446
  }
2464
2447
  body.push(node);
2465
2448
  } else if (this.type === tt.string && this.input.charCodeAt(this.start) === 34) {
@@ -2601,6 +2584,66 @@ export function TSRXPlugin(config) {
2601
2584
  this.parseTemplateBody(body);
2602
2585
  }
2603
2586
 
2587
+ /**
2588
+ * Parse proposal-style imports from an inline module declaration:
2589
+ * `import { foo } from server;`
2590
+ *
2591
+ * Acorn's import parser currently requires a string literal source. TSRX
2592
+ * extends only the source position; all specifier parsing stays delegated
2593
+ * to Acorn/@sveltejs/acorn-typescript.
2594
+ * @type {Parse.Parser['parseImport']}
2595
+ */
2596
+ parseImport(node) {
2597
+ const tokenIsIdentifier = /** @type {any} */ (Parser.acornTypeScript).tokenIsIdentifier;
2598
+ const parser = /** @type {any} */ (this);
2599
+ const import_node = /** @type {any} */ (node);
2600
+ let enterHead = parser.lookahead();
2601
+ import_node.importKind = 'value';
2602
+ parser.importOrExportOuterKind = 'value';
2603
+ if (tokenIsIdentifier(enterHead.type) || this.match(tt.star) || this.match(tt.braceL)) {
2604
+ let ahead = parser.lookahead(2);
2605
+ if (
2606
+ ahead.type !== tt.comma &&
2607
+ !parser.isContextualWithState('from', ahead) &&
2608
+ ahead.type !== tt.eq &&
2609
+ parser.ts_eatContextualWithState('type', 1, enterHead)
2610
+ ) {
2611
+ parser.importOrExportOuterKind = 'type';
2612
+ import_node.importKind = 'type';
2613
+ enterHead = parser.lookahead();
2614
+ ahead = parser.lookahead(2);
2615
+ }
2616
+ if (tokenIsIdentifier(enterHead.type) && ahead.type === tt.eq) {
2617
+ this.next();
2618
+ const importNode = parser.tsParseImportEqualsDeclaration(node);
2619
+ parser.importOrExportOuterKind = 'value';
2620
+ return importNode;
2621
+ }
2622
+ }
2623
+ this.next();
2624
+ if (this.type === tt.string) {
2625
+ import_node.specifiers = [];
2626
+ import_node.source = this.parseExprAtom();
2627
+ } else {
2628
+ import_node.specifiers = this.parseImportSpecifiers();
2629
+ this.expectContextual('from');
2630
+ if (this.type === tt.string) {
2631
+ import_node.source = this.parseExprAtom();
2632
+ } else if (tokenIsIdentifier(this.type)) {
2633
+ const source = this.parseIdent(false);
2634
+ source.metadata ??= { path: [] };
2635
+ import_node.source = source;
2636
+ } else {
2637
+ this.unexpected();
2638
+ }
2639
+ }
2640
+ parser.parseMaybeImportAttributes(node);
2641
+ this.semicolon();
2642
+ this.finishNode(node, 'ImportDeclaration');
2643
+ parser.importOrExportOuterKind = 'value';
2644
+ return import_node;
2645
+ }
2646
+
2604
2647
  /**
2605
2648
  * @type {Parse.Parser['parseStatement']}
2606
2649
  */
@@ -2616,11 +2659,23 @@ export function TSRXPlugin(config) {
2616
2659
  const node = this.jsx_parseExpressionContainer();
2617
2660
  // Keep JSXEmptyExpression as-is (don't convert to TSRXExpression/Text/Html)
2618
2661
  if (node.expression.type !== 'JSXEmptyExpression') {
2619
- /** @type {AST.TSRXExpression | AST.Html | AST.TextNode} */ (
2662
+ /** @type {AST.TSRXExpression | AST.Html | AST.TextNode | AST.Style} */ (
2620
2663
  /** @type {unknown} */ (node)
2621
- ).type = node.html ? 'Html' : node.text ? 'Text' : 'TSRXExpression';
2664
+ ).type = node.html
2665
+ ? 'Html'
2666
+ : node.text
2667
+ ? 'Text'
2668
+ : node.style
2669
+ ? 'Style'
2670
+ : 'TSRXExpression';
2671
+ if (node.style) {
2672
+ /** @type {AST.Style} */ (/** @type {unknown} */ (node)).value =
2673
+ /** @type {AST.Literal} */ (node.expression);
2674
+ delete (/** @type {any} */ (node).expression);
2675
+ }
2622
2676
  delete node.html;
2623
2677
  delete node.text;
2678
+ delete node.style;
2624
2679
  }
2625
2680
 
2626
2681
  return /** @type {ESTreeJSX.JSXEmptyExpression | AST.TSRXExpression | AST.Html | AST.TextNode | ESTreeJSX.JSXExpressionContainer} */ (
@@ -2628,18 +2683,6 @@ export function TSRXPlugin(config) {
2628
2683
  );
2629
2684
  }
2630
2685
 
2631
- if (this.value === '#server') {
2632
- // Peek ahead to see if this is a server block (#server { ... }) vs
2633
- // a server identifier expression (#server.fn(), #server.fn().then())
2634
- let peek_pos = this.end;
2635
- while (peek_pos < this.input.length && /\s/.test(this.input[peek_pos])) peek_pos++;
2636
- if (peek_pos < this.input.length && this.input.charCodeAt(peek_pos) === 123) {
2637
- // Next non-whitespace character is '{' — parse as server block
2638
- return this.parseServerBlock();
2639
- }
2640
- // Otherwise fall through to parse as expression statement (e.g., #server.fn().then(...))
2641
- }
2642
-
2643
2686
  if (this.value === 'component') {
2644
2687
  this.awaitPos = 0;
2645
2688
  return this.parseComponent({ requireName: true, declareName: true });
package/src/scope.js CHANGED
@@ -127,10 +127,18 @@ export function create_scopes(ast, root, parent, error_options) {
127
127
  next({ scope });
128
128
  },
129
129
 
130
- ServerBlock(node, { state, next }) {
130
+ TSModuleDeclaration(node, { state, next }) {
131
+ const is_submodule = node.metadata?.module_keyword === 'module';
132
+ if (is_submodule && node.id?.type === 'Identifier') {
133
+ state.scope.declare(node.id, 'normal', 'module', node);
134
+ }
135
+
131
136
  const scope = state.scope.child();
132
- scope.server_block = true;
137
+ scope.server_block = is_submodule;
133
138
  scopes.set(node, scope);
139
+ if (node.body) {
140
+ scopes.set(node.body, scope);
141
+ }
134
142
 
135
143
  next({ scope });
136
144
  },
@@ -296,7 +304,7 @@ export class Scope {
296
304
  tracing = null;
297
305
 
298
306
  /**
299
- * Is this scope a top-level server block scope
307
+ * Is this scope a submodule scope
300
308
  * @type {ScopeInterface['server_block']}
301
309
  */
302
310
  server_block = false;
@@ -325,7 +333,7 @@ export class Scope {
325
333
  return this.parent.declare(node, kind, declaration_kind);
326
334
  }
327
335
 
328
- if (declaration_kind === 'import' && !this.parent.server_block) {
336
+ if (declaration_kind === 'import' && !this.server_block && !this.parent.server_block) {
329
337
  return this.parent.declare(node, kind, declaration_kind, initial);
330
338
  }
331
339
  }
@@ -204,6 +204,12 @@ export function tsx_with_ts_locations() {
204
204
  }
205
205
  context.write('>');
206
206
  },
207
+ TSModuleDeclaration: (node, context) => {
208
+ context.write(node.metadata?.module_keyword ?? 'module');
209
+ context.write(' ');
210
+ context.visit(node.id);
211
+ context.visit(node.body);
212
+ },
207
213
  };
208
214
 
209
215
  // Be careful when duplicating visitors that are already defined
@@ -254,14 +254,13 @@ export function createJsxTransform(platform) {
254
254
  return /** @type {any} */ (to_jsx_expression_container(inner.expression, inner));
255
255
  },
256
256
 
257
- MemberExpression(node, { next, state }) {
258
- const as_any = /** @type {any} */ (node);
259
- if (as_any.object && as_any.object.type === 'StyleIdentifier' && state.current_css_hash) {
260
- const class_name = as_any.computed ? as_any.property.value : as_any.property.name;
261
- const value = `${state.current_css_hash} ${class_name}`;
262
- return /** @type {any} */ ({ type: 'Literal', value, raw: JSON.stringify(value) });
263
- }
264
- return next();
257
+ Style(node, { state, path }) {
258
+ validate_style_directive(node, state, path);
259
+ const class_name = typeof node.value.value === 'string' ? node.value.value : '';
260
+ const value = state.current_css_hash
261
+ ? `${state.current_css_hash} ${class_name}`
262
+ : class_name;
263
+ return /** @type {any} */ (b.literal(value, node));
265
264
  },
266
265
 
267
266
  // Default .metadata on every function-like node so downstream consumers
@@ -2771,6 +2770,108 @@ function get_body_source_node(body_nodes) {
2771
2770
  return first;
2772
2771
  }
2773
2772
 
2773
+ /**
2774
+ * @param {any} node
2775
+ * @param {TransformContext} transform_context
2776
+ * @param {any[]} path
2777
+ */
2778
+ function validate_style_directive(node, transform_context, path) {
2779
+ const { attribute, element } = get_style_attribute_context(node, path);
2780
+
2781
+ if (!attribute) {
2782
+ error(
2783
+ '`{style "class_name"}` can only be used as an element attribute value.',
2784
+ transform_context.filename,
2785
+ node,
2786
+ transform_context.errors,
2787
+ transform_context.comments,
2788
+ );
2789
+ }
2790
+
2791
+ if (element && is_dom_style_target(element)) {
2792
+ error(
2793
+ '`{style "class_name"}` cannot be used directly on DOM elements. Pass the class to a child component instead.',
2794
+ transform_context.filename,
2795
+ node,
2796
+ transform_context.errors,
2797
+ transform_context.comments,
2798
+ );
2799
+ }
2800
+
2801
+ if (!transform_context.current_css_hash) {
2802
+ error(
2803
+ '`{style "class_name"}` requires a <style> block in the current component.',
2804
+ transform_context.filename,
2805
+ node,
2806
+ transform_context.errors,
2807
+ transform_context.comments,
2808
+ );
2809
+ }
2810
+ }
2811
+
2812
+ /**
2813
+ * @param {any} node
2814
+ * @param {any[]} path
2815
+ * @returns {{ attribute: any, element: any }}
2816
+ */
2817
+ function get_style_attribute_context(node, path) {
2818
+ const parent = path.at(-1);
2819
+ const attribute =
2820
+ parent?.type === 'Attribute' && parent.value === node
2821
+ ? parent
2822
+ : path
2823
+ .findLast((ancestor) => ancestor?.type === 'Element')
2824
+ ?.attributes?.find(
2825
+ (/** @type {any} */ attr) =>
2826
+ attr?.type === 'Attribute' &&
2827
+ (attr.value === node || node_contains(attr.value, node)),
2828
+ );
2829
+ const element = path.findLast(
2830
+ (ancestor) =>
2831
+ ancestor?.type === 'Element' &&
2832
+ (!attribute || ancestor.attributes?.some((/** @type {any} */ attr) => attr === attribute)),
2833
+ );
2834
+
2835
+ return { attribute: attribute ?? null, element: element ?? null };
2836
+ }
2837
+
2838
+ /**
2839
+ * @param {any} root
2840
+ * @param {any} target
2841
+ * @returns {boolean}
2842
+ */
2843
+ function node_contains(root, target) {
2844
+ if (!root || typeof root !== 'object') {
2845
+ return false;
2846
+ }
2847
+ if (root === target) {
2848
+ return true;
2849
+ }
2850
+ if (Array.isArray(root)) {
2851
+ return root.some((child) => node_contains(child, target));
2852
+ }
2853
+ for (const key of Object.keys(root)) {
2854
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
2855
+ continue;
2856
+ }
2857
+ if (node_contains(root[key], target)) {
2858
+ return true;
2859
+ }
2860
+ }
2861
+ return false;
2862
+ }
2863
+
2864
+ /**
2865
+ * @param {any} element
2866
+ * @returns {boolean}
2867
+ */
2868
+ function is_dom_style_target(element) {
2869
+ if (!element?.id || is_dynamic_element_id(element.id)) {
2870
+ return false;
2871
+ }
2872
+ return element.id.type === 'Identifier' && /^[a-z]/.test(element.id.name);
2873
+ }
2874
+
2774
2875
  /**
2775
2876
  * @param {any} node
2776
2877
  * @param {TransformContext} transform_context
@@ -380,6 +380,37 @@ export function convert_source_map_to_mappings(
380
380
  css_element_info,
381
381
  });
382
382
 
383
+ /** @type {Map<string, number>} */
384
+ const generated_position_indexes = new Map();
385
+
386
+ /**
387
+ * When a transform expands one source identifier into multiple generated
388
+ * identifiers (e.g. `import { foo } from server` -> `const foo =
389
+ * _$_server_$_.foo`), esrap records multiple generated positions for the
390
+ * same source location. Keep token mappings in generated-order by consuming
391
+ * the next matching generated token instead of always using the first one.
392
+ * @param {Token} token
393
+ * @returns {{ line: number; column: number }}
394
+ */
395
+ function get_generated_position_for_token(token) {
396
+ const key = `${token.loc.start.line}:${token.loc.start.column}`;
397
+ const positions = src_to_gen_map.get(key);
398
+ if (!positions || positions.length === 0) {
399
+ throw new Error(`No source map entry for position "${key}"`);
400
+ }
401
+
402
+ const matching_positions = positions.filter((position) => {
403
+ const offset = loc_to_offset(position.line, position.column, gen_line_offsets);
404
+ return generated_code.startsWith(token.generated, offset);
405
+ });
406
+ const candidates = matching_positions.length > 0 ? matching_positions : positions;
407
+ const index_key = `${key}:${token.generated}`;
408
+ const index = generated_position_indexes.get(index_key) ?? 0;
409
+ generated_position_indexes.set(index_key, index + 1);
410
+
411
+ return candidates[Math.min(index, candidates.length - 1)];
412
+ }
413
+
383
414
  /**
384
415
  * Needed for a mapping that includes the computed brackets for diagnostics
385
416
  * @param {AST.MethodDefinition | AST.Property} node
@@ -2182,11 +2213,7 @@ export function convert_source_map_to_mappings(
2182
2213
  const gen_length = gen_text.length;
2183
2214
  let gen_line_col;
2184
2215
  try {
2185
- gen_line_col = get_generated_position(
2186
- token.loc.start.line,
2187
- token.loc.start.column,
2188
- src_to_gen_map,
2189
- );
2216
+ gen_line_col = get_generated_position_for_token(token);
2190
2217
  } catch {
2191
2218
  continue;
2192
2219
  }
package/types/index.d.ts CHANGED
@@ -64,8 +64,9 @@ interface BaseNodeMetaData {
64
64
  scoped?: boolean;
65
65
  path: AST.Node[];
66
66
  has_template?: boolean;
67
- source_name?: string | '#server' | '#style';
67
+ source_name?: string;
68
68
  source_length?: number;
69
+ module_keyword?: 'module' | 'namespace';
69
70
  is_capitalized?: boolean;
70
71
  commentContainerId?: number;
71
72
  parenthesized?: boolean;
@@ -206,12 +207,9 @@ declare module 'estree' {
206
207
  TsxCompat: TsxCompat;
207
208
  TSRXExpression: TSRXExpression;
208
209
  Html: Html;
210
+ Style: Style;
209
211
  Element: Element;
210
212
  Text: TextNode;
211
- ServerBlock: ServerBlock;
212
- ServerBlockStatement: ServerBlockStatement;
213
- ServerIdentifier: ServerIdentifier;
214
- StyleIdentifier: StyleIdentifier;
215
213
  Attribute: Attribute;
216
214
  RefAttribute: RefAttribute;
217
215
  SpreadAttribute: SpreadAttribute;
@@ -220,8 +218,7 @@ declare module 'estree' {
220
218
  }
221
219
 
222
220
  interface ExpressionMap {
223
- StyleIdentifier: StyleIdentifier;
224
- ServerIdentifier: ServerIdentifier;
221
+ Style: Style;
225
222
  Text: TextNode;
226
223
  JSXEmptyExpression: ESTreeJSX.JSXEmptyExpression;
227
224
  ParenthesizedExpression: ParenthesizedExpression;
@@ -268,14 +265,6 @@ declare module 'estree' {
268
265
  key?: AST.Expression | null;
269
266
  }
270
267
 
271
- interface ServerIdentifier extends AST.BaseExpression {
272
- type: 'ServerIdentifier';
273
- }
274
-
275
- interface StyleIdentifier extends AST.BaseExpression {
276
- type: 'StyleIdentifier';
277
- }
278
-
279
268
  interface ImportDeclaration {
280
269
  importKind: TSESTree.ImportDeclaration['importKind'];
281
270
  }
@@ -338,7 +327,6 @@ declare module 'estree' {
338
327
  metadata: BaseNodeMetaData & {
339
328
  topScopedClasses?: TopScopedClasses;
340
329
  styleClasses?: StyleClasses;
341
- styleIdentifierPresent?: boolean;
342
330
  };
343
331
  default: boolean;
344
332
  typeParameters?: AST.TSTypeParameterDeclaration;
@@ -370,6 +358,12 @@ declare module 'estree' {
370
358
  expression: AST.Expression;
371
359
  }
372
360
 
361
+ interface Style extends AST.BaseExpression {
362
+ type: 'Style';
363
+ value: AST.Literal;
364
+ loc?: AST.SourceLocation;
365
+ }
366
+
373
367
  export interface TSRXExpression extends AST.BaseExpression {
374
368
  type: 'TSRXExpression';
375
369
  expression: AST.Expression;
@@ -416,18 +410,6 @@ declare module 'estree' {
416
410
  loc?: AST.SourceLocation;
417
411
  }
418
412
 
419
- interface ServerBlockStatement extends Omit<BlockStatement, 'body'> {
420
- body: (AST.Statement | AST.ExportNamedDeclaration)[];
421
- }
422
-
423
- interface ServerBlock extends AST.BaseNode {
424
- type: 'ServerBlock';
425
- body: ServerBlockStatement;
426
- metadata: BaseNodeMetaData & {
427
- exports: Set<string>;
428
- };
429
- }
430
-
431
413
  interface ScriptContent extends Omit<AST.Element, 'type'> {
432
414
  type: 'ScriptContent';
433
415
  content: string;
@@ -694,6 +676,7 @@ declare module 'estree-jsx' {
694
676
  interface JSXExpressionContainer {
695
677
  html?: boolean;
696
678
  text?: boolean;
679
+ style?: boolean;
697
680
  }
698
681
 
699
682
  interface JSXMemberExpression {
@@ -977,6 +960,9 @@ declare module 'estree' {
977
960
  > {
978
961
  body: TSModuleBlock;
979
962
  id: AST.Identifier;
963
+ metadata: BaseNodeMetaData & {
964
+ exports?: Set<string>;
965
+ };
980
966
  }
981
967
  interface TSNamedTupleMember extends Omit<
982
968
  AcornTSNode<TSESTree.TSNamedTupleMember>,
@@ -1184,7 +1170,9 @@ export interface AnalysisResult {
1184
1170
  scope: ScopeInterface;
1185
1171
  component_metadata: Array<{ id: string }>;
1186
1172
  metadata: {
1187
- serverIdentifierPresent: boolean;
1173
+ serverImportsPresent: boolean;
1174
+ serverImportDeclarations: AST.ImportDeclaration[];
1175
+ serverModule: AST.TSModuleDeclaration | null;
1188
1176
  };
1189
1177
  errors: CompileError[];
1190
1178
  comments: AST.CommentWithLocation[];
@@ -1209,6 +1197,7 @@ export type DeclarationKind =
1209
1197
  | 'rest_param'
1210
1198
  | 'component'
1211
1199
  | 'import'
1200
+ | 'module'
1212
1201
  | 'using'
1213
1202
  | 'await using';
1214
1203
 
@@ -1239,7 +1228,8 @@ export interface Binding {
1239
1228
  | AST.Expression
1240
1229
  | AST.FunctionDeclaration
1241
1230
  | AST.ClassDeclaration
1242
- | AST.ImportDeclaration;
1231
+ | AST.ImportDeclaration
1232
+ | AST.TSModuleDeclaration;
1243
1233
  /** Whether this binding has been reassigned */
1244
1234
  reassigned: boolean;
1245
1235
  /** Whether this binding has been mutated (property access) */
@@ -1331,7 +1321,8 @@ export interface ScopeInterface {
1331
1321
  | AST.Expression
1332
1322
  | AST.FunctionDeclaration
1333
1323
  | AST.ClassDeclaration
1334
- | AST.ImportDeclaration,
1324
+ | AST.ImportDeclaration
1325
+ | AST.TSModuleDeclaration,
1335
1326
  ): Binding;
1336
1327
  /** Get binding by name */
1337
1328
  get(name: string): Binding | null;
@@ -1357,8 +1348,7 @@ export interface BaseState {
1357
1348
  /** For utils */
1358
1349
  scope: ScopeInterface;
1359
1350
  scopes: Map<AST.Node | AST.Node[], ScopeInterface>;
1360
- serverIdentifierPresent: boolean;
1361
- ancestor_server_block: AST.ServerBlock | undefined;
1351
+ ancestor_server_block: AST.TSModuleDeclaration | undefined;
1362
1352
  inside_head?: boolean;
1363
1353
  keep_component_style?: boolean;
1364
1354
 
package/types/parse.d.ts CHANGED
@@ -930,7 +930,7 @@ export namespace Parse {
930
930
  refDestructuringErrors?: DestructuringErrors,
931
931
  forInit?: ForInit,
932
932
  forNew?: boolean,
933
- ): AST.ServerIdentifier | AST.StyleIdentifier | AST.Component | AST.Identifier | AST.Literal;
933
+ ): AST.Component | AST.Identifier | AST.Literal;
934
934
 
935
935
  /** Default handler for parseExprAtom when no other case matches */
936
936
  parseExprAtomDefault(): AST.Expression;
@@ -1172,8 +1172,6 @@ export namespace Parse {
1172
1172
  */
1173
1173
  parseTopLevel(node: AST.Program): AST.Program;
1174
1174
 
1175
- parseServerBlock(): AST.ServerBlock;
1176
-
1177
1175
  parseElement(): AST.Element | AST.Tsx | AST.TsxCompat;
1178
1176
 
1179
1177
  parseDoubleQuotedTextChild(): AST.TextNode;
@@ -1210,7 +1208,6 @@ export namespace Parse {
1210
1208
  | AST.TextNode
1211
1209
  | ESTreeJSX.JSXEmptyExpression
1212
1210
  | ESTreeJSX.JSXExpressionContainer
1213
- | AST.ServerBlock
1214
1211
  | AST.Component
1215
1212
  | AST.ExpressionStatement
1216
1213
  | ReturnType<Parser['parseElement']>
@@ -1543,7 +1540,7 @@ export namespace Parse {
1543
1540
  /**
1544
1541
  * Check if a local export refers to a defined variable.
1545
1542
  * Acorn's default implementation only checks the top-level module scope,
1546
- * but Ripple overrides this to check all scopes (for server blocks).
1543
+ * but Ripple overrides this to check all scopes (for submodules).
1547
1544
  * @param id The identifier being exported
1548
1545
  */
1549
1546
  checkLocalExport(id: AST.Identifier): void;