@tsrx/core 0.0.22 → 0.0.24

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.24",
7
7
  "type": "module",
8
8
  "repository": {
9
9
  "type": "git",
@@ -7,6 +7,18 @@ import { error } from '../errors.js';
7
7
 
8
8
  export const COMPONENT_RETURN_VALUE_ERROR =
9
9
  'Return statements inside components cannot have a return value.';
10
+ export const COMPONENT_LOOP_RETURN_ERROR =
11
+ 'Return statements are not allowed inside component for...of loops. Use continue instead.';
12
+ export const COMPONENT_LOOP_BREAK_ERROR =
13
+ 'Break statements are not allowed inside component for...of loops.';
14
+ export const COMPONENT_FOR_STATEMENT_ERROR =
15
+ 'For loops are not supported in components. Use for...of instead.';
16
+ export const COMPONENT_FOR_IN_STATEMENT_ERROR =
17
+ 'For...in loops are not supported in components. Use for...of instead.';
18
+ export const COMPONENT_WHILE_STATEMENT_ERROR =
19
+ 'While loops are not supported in components. Move the while loop into a function.';
20
+ export const COMPONENT_DO_WHILE_STATEMENT_ERROR =
21
+ 'Do...while loops are not supported in components. Move the do...while loop into a function.';
10
22
 
11
23
  const invalid_nestings = {
12
24
  // <p> cannot contain block-level elements
@@ -133,19 +145,29 @@ function get_element_tag(element) {
133
145
  * @returns {AST.ReturnStatement}
134
146
  */
135
147
  export function get_return_keyword_node(node) {
136
- const return_keyword_length = 'return'.length;
148
+ return get_statement_keyword_node(node, 'return');
149
+ }
150
+
151
+ /**
152
+ * @template {AST.Node} T
153
+ * @param {T} node
154
+ * @param {string} keyword
155
+ * @returns {T}
156
+ */
157
+ export function get_statement_keyword_node(node, keyword) {
158
+ const keyword_length = keyword.length;
137
159
  const start = /** @type {AST.NodeWithLocation} */ (node).start ?? 0;
138
160
  const loc = /** @type {AST.NodeWithLocation} */ (node).loc;
139
161
 
140
- return /** @type {AST.ReturnStatement} */ ({
162
+ return /** @type {T} */ ({
141
163
  ...node,
142
- end: start + return_keyword_length,
164
+ end: start + keyword_length,
143
165
  loc: loc
144
166
  ? {
145
167
  start: loc.start,
146
168
  end: {
147
169
  line: loc.start.line,
148
- column: loc.start.column + return_keyword_length,
170
+ column: loc.start.column + keyword_length,
149
171
  },
150
172
  }
151
173
  : undefined,
@@ -172,6 +194,59 @@ export function validate_component_return_statement(node, filename, errors, comm
172
194
  );
173
195
  }
174
196
 
197
+ /**
198
+ * @param {AST.ReturnStatement} node
199
+ * @param {string | null | undefined} filename
200
+ * @param {CompileError[]} [errors]
201
+ * @param {AST.CommentWithLocation[]} [comments]
202
+ */
203
+ export function validate_component_loop_return_statement(node, filename, errors, comments) {
204
+ error(
205
+ COMPONENT_LOOP_RETURN_ERROR,
206
+ filename ?? null,
207
+ get_return_keyword_node(node),
208
+ errors,
209
+ comments,
210
+ );
211
+ }
212
+
213
+ /**
214
+ * @param {AST.BreakStatement} node
215
+ * @param {string | null | undefined} filename
216
+ * @param {CompileError[]} [errors]
217
+ * @param {AST.CommentWithLocation[]} [comments]
218
+ */
219
+ export function validate_component_loop_break_statement(node, filename, errors, comments) {
220
+ error(
221
+ COMPONENT_LOOP_BREAK_ERROR,
222
+ filename ?? null,
223
+ get_statement_keyword_node(node, 'break'),
224
+ errors,
225
+ comments,
226
+ );
227
+ }
228
+
229
+ /**
230
+ * @param {AST.ForStatement | AST.ForInStatement | AST.WhileStatement | AST.DoWhileStatement} node
231
+ * @param {string | null | undefined} filename
232
+ * @param {CompileError[]} [errors]
233
+ * @param {AST.CommentWithLocation[]} [comments]
234
+ */
235
+ export function validate_component_unsupported_loop_statement(node, filename, errors, comments) {
236
+ let message;
237
+ if (node.type === 'ForStatement') {
238
+ message = COMPONENT_FOR_STATEMENT_ERROR;
239
+ } else if (node.type === 'ForInStatement') {
240
+ message = COMPONENT_FOR_IN_STATEMENT_ERROR;
241
+ } else if (node.type === 'WhileStatement') {
242
+ message = COMPONENT_WHILE_STATEMENT_ERROR;
243
+ } else {
244
+ message = COMPONENT_DO_WHILE_STATEMENT_ERROR;
245
+ }
246
+
247
+ error(message, filename ?? null, node, errors, comments);
248
+ }
249
+
175
250
  /**
176
251
  * @param {AST.Element} element
177
252
  * @param {AnalysisContext} context
@@ -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,
@@ -141,6 +140,7 @@ export { escape } from './utils/escaping.js';
141
140
  export {
142
141
  createJsxTransform,
143
142
  merge_duplicate_refs as mergeDuplicateRefs,
143
+ rewrite_loop_continues_to_bare_returns as rewriteLoopContinuesToBareReturns,
144
144
  to_jsx_attribute as toJsxAttribute,
145
145
  validate_at_most_one_ref_attribute as validateAtMostOneRefAttribute,
146
146
  component_to_function_declaration as componentToFunctionDeclaration,
@@ -210,8 +210,18 @@ export {
210
210
  // Analyze
211
211
  export { analyze_css as analyzeCss } from './analyze/css-analyze.js';
212
212
  export {
213
+ COMPONENT_DO_WHILE_STATEMENT_ERROR,
214
+ COMPONENT_FOR_IN_STATEMENT_ERROR,
215
+ COMPONENT_FOR_STATEMENT_ERROR,
216
+ COMPONENT_LOOP_BREAK_ERROR,
217
+ COMPONENT_LOOP_RETURN_ERROR,
213
218
  COMPONENT_RETURN_VALUE_ERROR,
219
+ COMPONENT_WHILE_STATEMENT_ERROR,
214
220
  get_return_keyword_node as getReturnKeywordNode,
221
+ get_statement_keyword_node as getStatementKeywordNode,
222
+ validate_component_loop_break_statement as validateComponentLoopBreakStatement,
223
+ validate_component_loop_return_statement as validateComponentLoopReturnStatement,
215
224
  validate_component_return_statement as validateComponentReturnStatement,
225
+ validate_component_unsupported_loop_statement as validateComponentUnsupportedLoopStatement,
216
226
  validate_nesting as validateNesting,
217
227
  } from './analyze/validation.js';
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) {
@@ -246,6 +269,50 @@ export function TSRXPlugin(config) {
246
269
  return null;
247
270
  }
248
271
 
272
+ #popTsxTokenContextBeforeTemplateExpressionChild() {
273
+ let index = this.pos;
274
+ let has_newline = false;
275
+
276
+ // Text-only Tsx nodes can leave the tokenizer in JSX text mode.
277
+ // Only unwind it for ASI before a following TSRX `{expr}` child;
278
+ // fragment props like `content={<></>}` still need the JSX context.
279
+ while (index < this.input.length) {
280
+ const ch = this.input.charCodeAt(index);
281
+ if (ch === 32 || ch === 9) {
282
+ index++;
283
+ } else if (ch === 10 || ch === 13) {
284
+ has_newline = true;
285
+ index++;
286
+ } else if (ch === 47 && this.input.charCodeAt(index + 1) === 42) {
287
+ const end = this.input.indexOf('*/', index + 2);
288
+ const comment_end = end === -1 ? this.input.length : end + 2;
289
+ if (this.input.slice(index, comment_end).match(regex_newline_characters)) {
290
+ has_newline = true;
291
+ }
292
+ index = comment_end;
293
+ } else if (ch === 47 && this.input.charCodeAt(index + 1) === 47) {
294
+ has_newline = true;
295
+ index += 2;
296
+ while (index < this.input.length) {
297
+ const comment_ch = this.input.charCodeAt(index);
298
+ if (comment_ch === 10 || comment_ch === 13) break;
299
+ index++;
300
+ }
301
+ } else {
302
+ break;
303
+ }
304
+ }
305
+
306
+ if (!has_newline || this.input.charCodeAt(index) !== 123) {
307
+ return;
308
+ }
309
+
310
+ const context_index = this.context.lastIndexOf(tstc.tc_expr);
311
+ if (context_index !== -1) {
312
+ this.context.length = context_index;
313
+ }
314
+ }
315
+
249
316
  #isDoubleQuotedTextChildStart() {
250
317
  if (this.#path.findLast((n) => n.type === 'TsxCompat' || n.type === 'Tsx')) {
251
318
  return false;
@@ -820,44 +887,6 @@ export function TSRXPlugin(config) {
820
887
  }
821
888
  }
822
889
 
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
890
  this.#allowTagStartAfterDoubleQuotedText = false;
862
891
  return super.getTokenFromCode(code);
863
892
  }
@@ -970,22 +999,6 @@ export function TSRXPlugin(config) {
970
999
  * @type {Parse.Parser['parseExprAtom']}
971
1000
  */
972
1001
  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
1002
  // Check if this is a component expression (e.g., in object literal values)
990
1003
  if (this.type === tt.name && this.value === 'component') {
991
1004
  return this.parseComponent();
@@ -1015,9 +1028,9 @@ export function TSRXPlugin(config) {
1015
1028
 
1016
1029
  /**
1017
1030
  * 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).
1031
+ * This is needed because submodules create nested scopes, but exports
1032
+ * from within submodules should still be valid if the identifier is
1033
+ * declared in the submodule scope (not just the top-level module scope).
1021
1034
  * @type {Parse.Parser['checkLocalExport']}
1022
1035
  */
1023
1036
  checkLocalExport(id) {
@@ -1036,31 +1049,6 @@ export function TSRXPlugin(config) {
1036
1049
  this.undefinedExports[name] = id;
1037
1050
  }
1038
1051
 
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
1052
  /**
1065
1053
  * Parse a component - common implementation used by statements, expressions, and export defaults
1066
1054
  * @type {Parse.Parser['parseComponent']}
@@ -1415,10 +1403,26 @@ export function TSRXPlugin(config) {
1415
1403
  '"text" is a TSRX keyword and must be used in the form {text some_value}',
1416
1404
  );
1417
1405
  }
1406
+ } else if (
1407
+ this.type === tt.name &&
1408
+ this.value === 'style' &&
1409
+ this.lookahead().type === tt.string
1410
+ ) {
1411
+ node.style = true;
1412
+ this.next();
1418
1413
  }
1419
1414
 
1420
1415
  node.expression =
1421
1416
  this.type === tt.braceR ? this.jsx_parseEmptyExpression() : this.parseExpression();
1417
+ if (
1418
+ node.style &&
1419
+ (node.expression.type !== 'Literal' || typeof node.expression.value !== 'string')
1420
+ ) {
1421
+ this.raise(
1422
+ /** @type {number} */ (node.expression.start),
1423
+ '"style" is a TSRX keyword and must be used in the form {style "class_name"}',
1424
+ );
1425
+ }
1422
1426
  this.expect(tt.braceR);
1423
1427
 
1424
1428
  return this.finishNode(node, 'JSXExpressionContainer');
@@ -1812,6 +1816,7 @@ export function TSRXPlugin(config) {
1812
1816
  break;
1813
1817
  }
1814
1818
  // If not a comment, fall through to default case
1819
+ this.#resetTokenStartToCurrentPosition();
1815
1820
  this.context.push(b_stat);
1816
1821
  this.exprAllowed = true;
1817
1822
  return original.readToken.call(this, ch);
@@ -1831,6 +1836,7 @@ export function TSRXPlugin(config) {
1831
1836
  this.#path.at(-1)?.type === 'Component' ||
1832
1837
  this.#path.at(-1)?.type === 'Element')
1833
1838
  ) {
1839
+ this.#resetTokenStartToCurrentPosition();
1834
1840
  return original.readToken.call(this, ch);
1835
1841
  }
1836
1842
  this.raise(
@@ -1855,6 +1861,7 @@ export function TSRXPlugin(config) {
1855
1861
  } else if (ch === 32 || ch === 9) {
1856
1862
  ++this.pos;
1857
1863
  } else {
1864
+ this.#resetTokenStartToCurrentPosition();
1858
1865
  this.context.push(b_stat);
1859
1866
  this.exprAllowed = true;
1860
1867
  return original.readToken.call(this, ch);
@@ -1986,6 +1993,14 @@ export function TSRXPlugin(config) {
1986
1993
  if (attr.value !== null) {
1987
1994
  if (attr.value.type === 'JSXExpressionContainer') {
1988
1995
  const expression = attr.value.expression;
1996
+ if (attr.value.style) {
1997
+ /** @type {AST.Style} */ (/** @type {unknown} */ (attr.value)).type = 'Style';
1998
+ /** @type {AST.Style} */ (/** @type {unknown} */ (attr.value)).value =
1999
+ /** @type {AST.Literal} */ (expression);
2000
+ delete (/** @type {any} */ (attr.value).expression);
2001
+ delete (/** @type {any} */ (attr.value).style);
2002
+ continue;
2003
+ }
1989
2004
  if (expression.type === 'Literal') {
1990
2005
  expression.was_expression = true;
1991
2006
  }
@@ -2038,6 +2053,7 @@ export function TSRXPlugin(config) {
2038
2053
  if (this.type !== tstt.jsxTagEnd) {
2039
2054
  raise_error();
2040
2055
  }
2056
+ this.#popTsxTokenContextBeforeTemplateExpressionChild();
2041
2057
  this.next();
2042
2058
  }
2043
2059
  }
@@ -2223,6 +2239,7 @@ export function TSRXPlugin(config) {
2223
2239
  if (this.type !== tstt.jsxTagEnd) {
2224
2240
  raise_error();
2225
2241
  }
2242
+ this.#popTsxTokenContextBeforeTemplateExpressionChild();
2226
2243
  this.next();
2227
2244
  }
2228
2245
  } else if (element.type === 'TsxCompat') {
@@ -2254,6 +2271,7 @@ export function TSRXPlugin(config) {
2254
2271
  if (this.type !== tstt.jsxTagEnd) {
2255
2272
  raise_error();
2256
2273
  }
2274
+ this.#popTsxTokenContextBeforeTemplateExpressionChild();
2257
2275
  this.next();
2258
2276
  }
2259
2277
  } else if (this.#path[this.#path.length - 1] === element) {
@@ -2455,11 +2473,23 @@ export function TSRXPlugin(config) {
2455
2473
  // Keep JSXEmptyExpression as-is (for prettier to handle comments)
2456
2474
  // but convert other expressions to Html/TSRXExpression/Text nodes
2457
2475
  if (node.expression.type !== 'JSXEmptyExpression') {
2458
- /** @type {AST.TSRXExpression | AST.Html | AST.TextNode} */ (
2476
+ /** @type {AST.TSRXExpression | AST.Html | AST.TextNode | AST.Style} */ (
2459
2477
  /** @type {unknown} */ (node)
2460
- ).type = node.html ? 'Html' : node.text ? 'Text' : 'TSRXExpression';
2478
+ ).type = node.html
2479
+ ? 'Html'
2480
+ : node.text
2481
+ ? 'Text'
2482
+ : node.style
2483
+ ? 'Style'
2484
+ : 'TSRXExpression';
2485
+ if (node.style) {
2486
+ /** @type {AST.Style} */ (/** @type {unknown} */ (node)).value =
2487
+ /** @type {AST.Literal} */ (node.expression);
2488
+ delete (/** @type {any} */ (node).expression);
2489
+ }
2461
2490
  delete node.html;
2462
2491
  delete node.text;
2492
+ delete node.style;
2463
2493
  }
2464
2494
  body.push(node);
2465
2495
  } else if (this.type === tt.string && this.input.charCodeAt(this.start) === 34) {
@@ -2601,6 +2631,66 @@ export function TSRXPlugin(config) {
2601
2631
  this.parseTemplateBody(body);
2602
2632
  }
2603
2633
 
2634
+ /**
2635
+ * Parse proposal-style imports from an inline module declaration:
2636
+ * `import { foo } from server;`
2637
+ *
2638
+ * Acorn's import parser currently requires a string literal source. TSRX
2639
+ * extends only the source position; all specifier parsing stays delegated
2640
+ * to Acorn/@sveltejs/acorn-typescript.
2641
+ * @type {Parse.Parser['parseImport']}
2642
+ */
2643
+ parseImport(node) {
2644
+ const tokenIsIdentifier = /** @type {any} */ (Parser.acornTypeScript).tokenIsIdentifier;
2645
+ const parser = /** @type {any} */ (this);
2646
+ const import_node = /** @type {any} */ (node);
2647
+ let enterHead = parser.lookahead();
2648
+ import_node.importKind = 'value';
2649
+ parser.importOrExportOuterKind = 'value';
2650
+ if (tokenIsIdentifier(enterHead.type) || this.match(tt.star) || this.match(tt.braceL)) {
2651
+ let ahead = parser.lookahead(2);
2652
+ if (
2653
+ ahead.type !== tt.comma &&
2654
+ !parser.isContextualWithState('from', ahead) &&
2655
+ ahead.type !== tt.eq &&
2656
+ parser.ts_eatContextualWithState('type', 1, enterHead)
2657
+ ) {
2658
+ parser.importOrExportOuterKind = 'type';
2659
+ import_node.importKind = 'type';
2660
+ enterHead = parser.lookahead();
2661
+ ahead = parser.lookahead(2);
2662
+ }
2663
+ if (tokenIsIdentifier(enterHead.type) && ahead.type === tt.eq) {
2664
+ this.next();
2665
+ const importNode = parser.tsParseImportEqualsDeclaration(node);
2666
+ parser.importOrExportOuterKind = 'value';
2667
+ return importNode;
2668
+ }
2669
+ }
2670
+ this.next();
2671
+ if (this.type === tt.string) {
2672
+ import_node.specifiers = [];
2673
+ import_node.source = this.parseExprAtom();
2674
+ } else {
2675
+ import_node.specifiers = this.parseImportSpecifiers();
2676
+ this.expectContextual('from');
2677
+ if (this.type === tt.string) {
2678
+ import_node.source = this.parseExprAtom();
2679
+ } else if (tokenIsIdentifier(this.type)) {
2680
+ const source = this.parseIdent(false);
2681
+ source.metadata ??= { path: [] };
2682
+ import_node.source = source;
2683
+ } else {
2684
+ this.unexpected();
2685
+ }
2686
+ }
2687
+ parser.parseMaybeImportAttributes(node);
2688
+ this.semicolon();
2689
+ this.finishNode(node, 'ImportDeclaration');
2690
+ parser.importOrExportOuterKind = 'value';
2691
+ return import_node;
2692
+ }
2693
+
2604
2694
  /**
2605
2695
  * @type {Parse.Parser['parseStatement']}
2606
2696
  */
@@ -2616,11 +2706,23 @@ export function TSRXPlugin(config) {
2616
2706
  const node = this.jsx_parseExpressionContainer();
2617
2707
  // Keep JSXEmptyExpression as-is (don't convert to TSRXExpression/Text/Html)
2618
2708
  if (node.expression.type !== 'JSXEmptyExpression') {
2619
- /** @type {AST.TSRXExpression | AST.Html | AST.TextNode} */ (
2709
+ /** @type {AST.TSRXExpression | AST.Html | AST.TextNode | AST.Style} */ (
2620
2710
  /** @type {unknown} */ (node)
2621
- ).type = node.html ? 'Html' : node.text ? 'Text' : 'TSRXExpression';
2711
+ ).type = node.html
2712
+ ? 'Html'
2713
+ : node.text
2714
+ ? 'Text'
2715
+ : node.style
2716
+ ? 'Style'
2717
+ : 'TSRXExpression';
2718
+ if (node.style) {
2719
+ /** @type {AST.Style} */ (/** @type {unknown} */ (node)).value =
2720
+ /** @type {AST.Literal} */ (node.expression);
2721
+ delete (/** @type {any} */ (node).expression);
2722
+ }
2622
2723
  delete node.html;
2623
2724
  delete node.text;
2725
+ delete node.style;
2624
2726
  }
2625
2727
 
2626
2728
  return /** @type {ESTreeJSX.JSXEmptyExpression | AST.TSRXExpression | AST.Html | AST.TextNode | ESTreeJSX.JSXExpressionContainer} */ (
@@ -2628,18 +2730,6 @@ export function TSRXPlugin(config) {
2628
2730
  );
2629
2731
  }
2630
2732
 
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
2733
  if (this.value === 'component') {
2644
2734
  this.awaitPos = 0;
2645
2735
  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