ripple 0.3.3 → 0.3.5

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.
Files changed (128) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/package.json +2 -2
  3. package/src/compiler/identifier-utils.js +1 -8
  4. package/src/compiler/phases/1-parse/index.js +101 -195
  5. package/src/compiler/phases/2-analyze/index.js +115 -174
  6. package/src/compiler/phases/2-analyze/prune.js +2 -2
  7. package/src/compiler/phases/3-transform/client/index.js +177 -261
  8. package/src/compiler/phases/3-transform/server/index.js +185 -42
  9. package/src/compiler/types/index.d.ts +15 -34
  10. package/src/compiler/utils.js +32 -20
  11. package/src/runtime/index-client.js +0 -17
  12. package/src/runtime/internal/client/bindings.js +118 -7
  13. package/src/runtime/internal/client/render.js +5 -1
  14. package/src/runtime/internal/client/runtime.js +1 -1
  15. package/src/runtime/internal/client/types.d.ts +4 -0
  16. package/src/runtime/internal/server/index.js +11 -0
  17. package/tests/client/array/array.copy-within.test.ripple +7 -7
  18. package/tests/client/array/array.derived.test.ripple +24 -24
  19. package/tests/client/array/array.iteration.test.ripple +7 -7
  20. package/tests/client/array/array.mutations.test.ripple +17 -17
  21. package/tests/client/array/array.to-methods.test.ripple +4 -4
  22. package/tests/client/async-suspend.test.ripple +3 -3
  23. package/tests/client/basic/basic.attributes.test.ripple +31 -31
  24. package/tests/client/basic/basic.collections.test.ripple +6 -6
  25. package/tests/client/basic/basic.components.test.ripple +8 -8
  26. package/tests/client/basic/basic.errors.test.ripple +31 -34
  27. package/tests/client/basic/basic.events.test.ripple +11 -11
  28. package/tests/client/basic/basic.get-set.test.ripple +18 -18
  29. package/tests/client/basic/basic.reactivity.test.ripple +36 -36
  30. package/tests/client/basic/basic.rendering.test.ripple +7 -7
  31. package/tests/client/basic/basic.utilities.test.ripple +4 -4
  32. package/tests/client/boundaries.test.ripple +7 -7
  33. package/tests/client/compiler/__snapshots__/compiler.typescript.test.ripple.snap +24 -0
  34. package/tests/client/compiler/compiler.assignments.test.ripple +12 -10
  35. package/tests/client/compiler/compiler.basic.test.ripple +57 -58
  36. package/tests/client/compiler/compiler.tracked-access.test.ripple +14 -8
  37. package/tests/client/compiler/compiler.typescript.test.ripple +31 -0
  38. package/tests/client/composite/composite.dynamic-components.test.ripple +6 -6
  39. package/tests/client/composite/composite.props.test.ripple +9 -9
  40. package/tests/client/composite/composite.reactivity.test.ripple +23 -23
  41. package/tests/client/composite/composite.render.test.ripple +52 -4
  42. package/tests/client/computed-properties.test.ripple +3 -3
  43. package/tests/client/context.test.ripple +3 -3
  44. package/tests/client/css/global-additional-cases.test.ripple +5 -2
  45. package/tests/client/css/style-identifier.test.ripple +40 -49
  46. package/tests/client/date.test.ripple +39 -39
  47. package/tests/client/dynamic-elements.test.ripple +37 -37
  48. package/tests/client/events.test.ripple +25 -25
  49. package/tests/client/for.test.ripple +8 -8
  50. package/tests/client/head.test.ripple +7 -7
  51. package/tests/client/html.test.ripple +2 -2
  52. package/tests/client/input-value.test.ripple +376 -177
  53. package/tests/client/lazy-destructuring.test.ripple +209 -0
  54. package/tests/client/map.test.ripple +20 -20
  55. package/tests/client/media-query.test.ripple +4 -4
  56. package/tests/client/object.test.ripple +5 -5
  57. package/tests/client/portal.test.ripple +4 -4
  58. package/tests/client/ref.test.ripple +3 -3
  59. package/tests/client/return.test.ripple +17 -17
  60. package/tests/client/set.test.ripple +10 -10
  61. package/tests/client/svg.test.ripple +6 -5
  62. package/tests/client/switch.test.ripple +10 -10
  63. package/tests/client/tracked-expression.test.ripple +3 -1
  64. package/tests/client/try.test.ripple +4 -4
  65. package/tests/client/url/url.derived.test.ripple +6 -7
  66. package/tests/client/url/url.parsing.test.ripple +9 -9
  67. package/tests/client/url/url.partial-removal.test.ripple +9 -9
  68. package/tests/client/url/url.reactivity.test.ripple +16 -16
  69. package/tests/client/url/url.serialization.test.ripple +3 -3
  70. package/tests/client/url-search-params/url-search-params.derived.test.ripple +7 -8
  71. package/tests/client/url-search-params/url-search-params.initialization.test.ripple +6 -4
  72. package/tests/client/url-search-params/url-search-params.iteration.test.ripple +12 -12
  73. package/tests/client/url-search-params/url-search-params.mutation.test.ripple +18 -18
  74. package/tests/client/url-search-params/url-search-params.retrieval.test.ripple +16 -16
  75. package/tests/client/url-search-params/url-search-params.serialization.test.ripple +4 -4
  76. package/tests/client/url-search-params/url-search-params.tracked-url.test.ripple +3 -3
  77. package/tests/hydration/build-components.js +4 -10
  78. package/tests/hydration/compiled/client/basic.js +4 -4
  79. package/tests/hydration/compiled/client/events.js +2 -0
  80. package/tests/hydration/compiled/client/for.js +2 -0
  81. package/tests/hydration/compiled/client/head.js +13 -11
  82. package/tests/hydration/compiled/client/hmr.js +4 -2
  83. package/tests/hydration/compiled/client/html.js +82 -95
  84. package/tests/hydration/compiled/client/if-children.js +8 -9
  85. package/tests/hydration/compiled/client/if.js +2 -0
  86. package/tests/hydration/compiled/client/mixed-control-flow.js +4 -2
  87. package/tests/hydration/compiled/client/portal.js +1 -1
  88. package/tests/hydration/compiled/client/reactivity.js +2 -0
  89. package/tests/hydration/compiled/client/return.js +2 -0
  90. package/tests/hydration/compiled/client/switch.js +2 -0
  91. package/tests/hydration/compiled/server/composite.js +2 -2
  92. package/tests/hydration/compiled/server/events.js +2 -0
  93. package/tests/hydration/compiled/server/for.js +2 -0
  94. package/tests/hydration/compiled/server/head.js +13 -11
  95. package/tests/hydration/compiled/server/hmr.js +2 -0
  96. package/tests/hydration/compiled/server/html.js +2 -0
  97. package/tests/hydration/compiled/server/if-children.js +2 -0
  98. package/tests/hydration/compiled/server/if.js +2 -0
  99. package/tests/hydration/compiled/server/mixed-control-flow.js +2 -0
  100. package/tests/hydration/compiled/server/portal.js +1 -1
  101. package/tests/hydration/compiled/server/reactivity.js +2 -0
  102. package/tests/hydration/compiled/server/return.js +2 -0
  103. package/tests/hydration/compiled/server/switch.js +2 -0
  104. package/tests/hydration/components/composite.ripple +1 -1
  105. package/tests/hydration/components/events.ripple +10 -8
  106. package/tests/hydration/components/for.ripple +22 -20
  107. package/tests/hydration/components/head.ripple +8 -6
  108. package/tests/hydration/components/hmr.ripple +3 -1
  109. package/tests/hydration/components/html.ripple +3 -1
  110. package/tests/hydration/components/if-children.ripple +9 -7
  111. package/tests/hydration/components/if.ripple +7 -5
  112. package/tests/hydration/components/mixed-control-flow.ripple +5 -3
  113. package/tests/hydration/components/portal.ripple +2 -2
  114. package/tests/hydration/components/reactivity.ripple +11 -9
  115. package/tests/hydration/components/return.ripple +13 -11
  116. package/tests/hydration/components/switch.ripple +6 -4
  117. package/tests/server/__snapshots__/compiler.test.ripple.snap +22 -0
  118. package/tests/server/await.test.ripple +2 -2
  119. package/tests/server/basic.attributes.test.ripple +21 -19
  120. package/tests/server/basic.components.test.ripple +5 -4
  121. package/tests/server/basic.test.ripple +21 -20
  122. package/tests/server/compiler.test.ripple +36 -5
  123. package/tests/server/composite.props.test.ripple +7 -6
  124. package/tests/server/context.test.ripple +3 -1
  125. package/tests/server/dynamic-elements.test.ripple +24 -24
  126. package/tests/server/head.test.ripple +7 -5
  127. package/tests/server/lazy-destructuring.test.ripple +103 -0
  128. package/tests/server/style-identifier.test.ripple +95 -16
package/CHANGELOG.md CHANGED
@@ -1,5 +1,95 @@
1
1
  # ripple
2
2
 
3
+ ## 0.3.5
4
+
5
+ ### Patch Changes
6
+
7
+ - [#827](https://github.com/Ripple-TS/ripple/pull/827)
8
+ [`218a72c`](https://github.com/Ripple-TS/ripple/commit/218a72c3e663910636eec1d065c58afe30813c84)
9
+ Thanks [@trueadm](https://github.com/trueadm)! - fix(compiler): handle
10
+ UpdateExpression on lazy bindings with default values
11
+
12
+ Update expressions (`++`/`--`) on lazy destructured bindings with default values
13
+ now work correctly. For postfix operations (`count++`), an IIFE captures the
14
+ fallback value before incrementing. Also added `fallback` function to server
15
+ runtime.
16
+
17
+ - Updated dependencies
18
+ [[`218a72c`](https://github.com/Ripple-TS/ripple/commit/218a72c3e663910636eec1d065c58afe30813c84)]:
19
+ - ripple@0.3.5
20
+
21
+ ## 0.3.4
22
+
23
+ ### Patch Changes
24
+
25
+ - [`92982cd`](https://github.com/Ripple-TS/ripple/commit/92982cd7b918d0afee9334c74765573b30c8a645)
26
+ Thanks [@trueadm](https://github.com/trueadm)! - feat(compiler): add lazy
27
+ destructuring syntax (`&{...}` and `&[...]`)
28
+
29
+ Lazy destructuring defers property/index access until the binding is read,
30
+ preserving reactivity for destructured props. Works with default values,
31
+ compound assignment operators, and update expressions.
32
+
33
+ - [#814](https://github.com/Ripple-TS/ripple/pull/814)
34
+ [`747ae1f`](https://github.com/Ripple-TS/ripple/commit/747ae1fc7948e994eeb521f3ed78711c9dd3e802)
35
+ Thanks [@RazinShafayet2007](https://github.com/RazinShafayet2007)! -
36
+ fix(compiler): strip TypeScript class syntax from JS output
37
+
38
+ This fixes compiler output for `.ripple` classes by stripping TypeScript-only
39
+ `implements` clauses and `extends` type arguments from emitted JavaScript.
40
+
41
+ - [#820](https://github.com/Ripple-TS/ripple/pull/820)
42
+ [`abe1caa`](https://github.com/Ripple-TS/ripple/commit/abe1caa6ab636722099a6ecd4cafbf117d208ec2)
43
+ Thanks [@RazinShafayet2007](https://github.com/RazinShafayet2007)! - fix: sync
44
+ `<select>` `bindValue` with typed and dynamic options
45
+
46
+ - [#817](https://github.com/Ripple-TS/ripple/pull/817)
47
+ [`046d0ba`](https://github.com/Ripple-TS/ripple/commit/046d0baf190d161c3b851799080d11eb4f95e094)
48
+ Thanks [@RazinShafayet2007](https://github.com/RazinShafayet2007)! -
49
+ fix(compiler): preserve class `extends` generics in volar output
50
+
51
+ - [`79a920e`](https://github.com/Ripple-TS/ripple/commit/79a920e30f0f35f2ec07ff8d52dc709f8bb74c77)
52
+ Thanks [@trueadm](https://github.com/trueadm)! - Remove `#ripple` namespace
53
+ syntax in favor of direct imports from `'ripple'`
54
+
55
+ The `#ripple` namespace (`#ripple.track()`, `#ripple.effect()`,
56
+ `#ripple.array()`, etc.) has been removed. All reactive APIs are now accessed
57
+ via standard imports:
58
+
59
+ ```ripple
60
+ import {
61
+ track,
62
+ effect,
63
+ untrack,
64
+ Context,
65
+ RippleArray,
66
+ RippleObject,
67
+ } from 'ripple';
68
+ ```
69
+
70
+ - `#ripple.track(value)` → `track(value)`
71
+ - `#ripple.effect(fn)` → `effect(fn)`
72
+ - `#ripple.untrack(fn)` → `untrack(fn)`
73
+ - `#ripple.context(value)` → `new Context(value)`
74
+ - `#ripple[1, 2, 3]` → `new RippleArray(1, 2, 3)`
75
+ - `#ripple{ key: value }` → `new RippleObject({ key: value })`
76
+ - `#ripple.style` → `#style`
77
+ - `#ripple.server` → `#server`
78
+
79
+ - [#824](https://github.com/Ripple-TS/ripple/pull/824)
80
+ [`83807a4`](https://github.com/Ripple-TS/ripple/commit/83807a412603ff49c398f9365b011fd4b4a5f8bf)
81
+ Thanks [@RazinShafayet2007](https://github.com/RazinShafayet2007)! -
82
+ fix(parser): avoid hanging on unclosed tsx compat tags
83
+
84
+ - Updated dependencies
85
+ [[`92982cd`](https://github.com/Ripple-TS/ripple/commit/92982cd7b918d0afee9334c74765573b30c8a645),
86
+ [`747ae1f`](https://github.com/Ripple-TS/ripple/commit/747ae1fc7948e994eeb521f3ed78711c9dd3e802),
87
+ [`abe1caa`](https://github.com/Ripple-TS/ripple/commit/abe1caa6ab636722099a6ecd4cafbf117d208ec2),
88
+ [`046d0ba`](https://github.com/Ripple-TS/ripple/commit/046d0baf190d161c3b851799080d11eb4f95e094),
89
+ [`79a920e`](https://github.com/Ripple-TS/ripple/commit/79a920e30f0f35f2ec07ff8d52dc709f8bb74c77),
90
+ [`83807a4`](https://github.com/Ripple-TS/ripple/commit/83807a412603ff49c398f9365b011fd4b4a5f8bf)]:
91
+ - ripple@0.3.4
92
+
3
93
  ## 0.3.3
4
94
 
5
95
  ### Patch Changes
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Ripple is an elegant TypeScript UI framework",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.3.3",
6
+ "version": "0.3.5",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -105,6 +105,6 @@
105
105
  "vscode-languageserver-types": "^3.17.5"
106
106
  },
107
107
  "peerDependencies": {
108
- "ripple": "0.3.3"
108
+ "ripple": "0.3.5"
109
109
  }
110
110
  }
@@ -1,13 +1,6 @@
1
1
  export const IDENTIFIER_OBFUSCATION_PREFIX = '_$_';
2
- export const RIPPLE_NAMESPACE_IDENTIFIER =
3
- IDENTIFIER_OBFUSCATION_PREFIX + encode_utf16_char('#') + 'ripple';
4
2
  export const STYLE_IDENTIFIER = IDENTIFIER_OBFUSCATION_PREFIX + encode_utf16_char('#') + 'style';
5
- export const SERVER_IDENTIFIER =
6
- IDENTIFIER_OBFUSCATION_PREFIX +
7
- encode_utf16_char('#') +
8
- 'ripple' +
9
- encode_utf16_char('.') +
10
- 'server';
3
+ export const SERVER_IDENTIFIER = IDENTIFIER_OBFUSCATION_PREFIX + encode_utf16_char('#') + 'server';
11
4
  export const CSS_HASH_IDENTIFIER = IDENTIFIER_OBFUSCATION_PREFIX + 'hash';
12
5
 
13
6
  const DECODE_UTF16_REGEX = /_u([0-9a-fA-F]{4})_/g;
@@ -665,46 +665,14 @@ function RipplePlugin(config) {
665
665
  ch === 13 || // carriage return
666
666
  ch === -1; // EOF
667
667
 
668
- if (startsWith('#ripple[')) {
669
- this.pos += 8;
670
- return this.finishToken(tt.bracketL, '#ripple[');
671
- }
672
-
673
- if (startsWith('#ripple{')) {
674
- this.pos += 8;
675
- return this.finishToken(tt.braceL, '#ripple{');
676
- }
677
-
678
- const ripple_keywords = [
679
- '#ripple.map',
680
- '#ripple.set',
681
- '#ripple.array',
682
- '#ripple.object',
683
- '#ripple.track',
684
- '#ripple.trackSplit',
685
- '#ripple.untrack',
686
- '#ripple.effect',
687
- '#ripple.context',
688
- '#ripple.date',
689
- '#ripple.url',
690
- '#ripple.urlSearchParams',
691
- '#ripple.mediaQuery',
692
- '#ripple.server',
693
- '#ripple.style',
694
- '#ripple.validate',
695
- ];
696
-
697
- for (let i = 0; i < ripple_keywords.length; i++) {
698
- const keyword = ripple_keywords[i];
699
- if (startsWith(keyword) && is_ripple_delimiter(char_after(keyword.length))) {
700
- this.pos += keyword.length;
701
- return this.finishToken(tt.name, keyword);
702
- }
668
+ if (startsWith('#server') && is_ripple_delimiter(char_after(7))) {
669
+ this.pos += 7;
670
+ return this.finishToken(tt.name, '#server');
703
671
  }
704
672
 
705
- if (this.#loose && startsWith('#ripple') && is_ripple_delimiter(char_after(7))) {
706
- this.pos += 7;
707
- return this.finishToken(tt.name, '#ripple');
673
+ if (startsWith('#style') && is_ripple_delimiter(char_after(6))) {
674
+ this.pos += 6;
675
+ return this.finishToken(tt.name, '#style');
708
676
  }
709
677
  }
710
678
  }
@@ -904,6 +872,49 @@ function RipplePlugin(config) {
904
872
  );
905
873
  }
906
874
 
875
+ /**
876
+ * Override isLet to recognize `let &{` and `let &[` as variable declarations.
877
+ * Acorn's isLet checks the char after `let` and only recognizes `{`, `[`, or identifiers.
878
+ * The `&` char (38) is not in that set, so `let &{...}` would not be parsed as a declaration.
879
+ * @param {string} context
880
+ * @returns {boolean}
881
+ */
882
+ isLet(context) {
883
+ if (!this.isContextual('let')) return false;
884
+ const skip = /\s*/y;
885
+ skip.lastIndex = this.pos;
886
+ const match = skip.exec(this.input);
887
+ if (!match) return super.isLet(context);
888
+ const next = this.pos + match[0].length;
889
+ const nextCh = this.input.charCodeAt(next);
890
+ // If next char is &, check if char after & is { or [
891
+ if (nextCh === 38) {
892
+ const afterAmp = this.input.charCodeAt(next + 1);
893
+ if (afterAmp === 123 || afterAmp === 91) return true;
894
+ }
895
+ return super.isLet(context);
896
+ }
897
+
898
+ /**
899
+ * Parse binding atom - handles lazy destructuring patterns (&{...} and &[...])
900
+ * When & is directly followed by { or [, parse as a lazy destructuring pattern.
901
+ * The resulting ObjectPattern/ArrayPattern node gets a `lazy: true` flag.
902
+ */
903
+ parseBindingAtom() {
904
+ if (this.type === tt.bitwiseAND) {
905
+ // Check that the char immediately after & is { or [ (no whitespace)
906
+ const charAfterAmp = this.input.charCodeAt(this.end);
907
+ if (charAfterAmp === 123 || charAfterAmp === 91) {
908
+ // & directly followed by { or [ — lazy destructuring
909
+ this.next(); // consume &, now current token is { or [
910
+ const pattern = super.parseBindingAtom();
911
+ pattern.lazy = true;
912
+ return pattern;
913
+ }
914
+ }
915
+ return super.parseBindingAtom();
916
+ }
917
+
907
918
  /**
908
919
  * Parse expression atom - handles RippleArray and RippleObject literals
909
920
  * @type {Parse.Parser['parseExprAtom']}
@@ -917,69 +928,19 @@ function RipplePlugin(config) {
917
928
  return this.parseTrackedExpression();
918
929
  }
919
930
 
920
- // Check if this is #ripple.server identifier for server function calls
921
- if (this.type === tt.name && this.value === '#ripple.server') {
931
+ // Check if this is #server identifier for server function calls
932
+ if (this.type === tt.name && this.value === '#server') {
922
933
  const node = this.startNode();
923
934
  this.next();
924
935
  return /** @type {AST.ServerIdentifier} */ (this.finishNode(node, 'ServerIdentifier'));
925
936
  }
926
937
 
927
- if (this.type === tt.name && this.value === '#ripple.style') {
938
+ if (this.type === tt.name && this.value === '#style') {
928
939
  const node = this.startNode();
929
940
  this.next();
930
941
  return /** @type {AST.StyleIdentifier} */ (this.finishNode(node, 'StyleIdentifier'));
931
942
  }
932
943
 
933
- if (this.type === tt.name && typeof this.value === 'string') {
934
- const ripple_identifier_map = {
935
- '#ripple.array': 'RippleArray',
936
- '#ripple.object': 'RippleObject',
937
- '#ripple.track': 'track',
938
- '#ripple.trackSplit': 'trackSplit',
939
- '#ripple.untrack': 'untrack',
940
- '#ripple.effect': 'effect',
941
- '#ripple.context': 'Context',
942
- '#ripple.date': 'RippleDate',
943
- '#ripple.map': 'RippleMap',
944
- '#ripple.set': 'RippleSet',
945
- '#ripple.url': 'RippleURL',
946
- '#ripple.urlSearchParams': 'RippleURLSearchParams',
947
- '#ripple.mediaQuery': 'MediaQuery',
948
- };
949
-
950
- const identifier_name =
951
- ripple_identifier_map[/** @type {keyof typeof ripple_identifier_map} */ (this.value)];
952
- if (identifier_name !== undefined) {
953
- const node = /** @type {AST.Identifier} */ (this.startNode());
954
- node.name = identifier_name;
955
- node.metadata ??= { path: [] };
956
- node.metadata.source_name = this.value;
957
- this.next();
958
- return this.finishNode(node, 'Identifier');
959
- }
960
- }
961
-
962
- // In loose mode, handle incomplete #ripple prefixes for autocomplete
963
- if (
964
- this.#loose &&
965
- this.type === tt.name &&
966
- typeof this.value === 'string' &&
967
- this.value === '#ripple'
968
- ) {
969
- // Return an Identifier node for incomplete tracked syntax
970
- const node = /** @type {AST.Identifier} */ (this.startNode());
971
- node.name = this.value;
972
- this.next();
973
- return this.finishNode(node, 'Identifier');
974
- }
975
-
976
- // Check if this is a tuple literal starting with #ripple[
977
- if (this.type === tt.bracketL && this.value === '#ripple[') {
978
- return this.parseRippleArrayExpression();
979
- } else if (this.type === tt.braceL && this.value === '#ripple{') {
980
- return this.parseRippleObjectExpression();
981
- }
982
-
983
944
  // Check if this is a component expression (e.g., in object literal values)
984
945
  if (this.type === tt.name && this.value === 'component') {
985
946
  return this.parseComponent();
@@ -1080,79 +1041,6 @@ function RipplePlugin(config) {
1080
1041
  return this.finishNode(node, 'ServerBlock');
1081
1042
  }
1082
1043
 
1083
- /**
1084
- * @type {Parse.Parser['parseRippleArrayExpression']}
1085
- */
1086
- parseRippleArrayExpression() {
1087
- const node = /** @type {AST.RippleArrayExpression} */ (this.startNode());
1088
- this.next(); // consume the '#ripple['
1089
-
1090
- node.elements = [];
1091
-
1092
- // Parse array elements similar to regular array parsing
1093
- let first = true;
1094
- while (!this.eat(tt.bracketR)) {
1095
- if (!first) {
1096
- this.expect(tt.comma);
1097
- if (this.afterTrailingComma(tt.bracketR)) break;
1098
- } else {
1099
- first = false;
1100
- }
1101
-
1102
- if (this.type === tt.comma) {
1103
- // Hole in array
1104
- node.elements.push(null);
1105
- } else if (this.type === tt.ellipsis) {
1106
- // Spread element
1107
- const element = this.parseSpread();
1108
- node.elements.push(element);
1109
- if (this.type === tt.comma && this.input.charCodeAt(this.pos) === 93) {
1110
- this.raise(this.pos, 'Trailing comma is not permitted after the rest element');
1111
- }
1112
- } else {
1113
- // Regular element
1114
- node.elements.push(this.parseMaybeAssign(false));
1115
- }
1116
- }
1117
-
1118
- return this.finishNode(node, 'RippleArrayExpression');
1119
- }
1120
-
1121
- /**
1122
- * @type {Parse.Parser['parseRippleObjectExpression']}
1123
- */
1124
- parseRippleObjectExpression() {
1125
- const node = /** @type {AST.RippleObjectExpression} */ (this.startNode());
1126
- this.next(); // consume the '#ripple{'
1127
-
1128
- node.properties = [];
1129
-
1130
- // Parse object properties similar to regular object parsing
1131
- let first = true;
1132
- while (!this.eat(tt.braceR)) {
1133
- if (!first) {
1134
- this.expect(tt.comma);
1135
- if (this.afterTrailingComma(tt.braceR)) break;
1136
- } else {
1137
- first = false;
1138
- }
1139
-
1140
- if (this.type === tt.ellipsis) {
1141
- // Spread property
1142
- const prop = this.parseSpread();
1143
- node.properties.push(prop);
1144
- if (this.type === tt.comma && this.input.charCodeAt(this.pos) === 125) {
1145
- this.raise(this.pos, 'Trailing comma is not permitted after the rest element');
1146
- }
1147
- } else {
1148
- // Regular property
1149
- node.properties.push(this.parseProperty(false, new DestructuringErrors()));
1150
- }
1151
- }
1152
-
1153
- return this.finishNode(node, 'RippleObjectExpression');
1154
- }
1155
-
1156
1044
  /**
1157
1045
  * Parse a component - common implementation used by statements, expressions, and export defaults
1158
1046
  * @type {Parse.Parser['parseComponent']}
@@ -2146,32 +2034,34 @@ function RipplePlugin(config) {
2146
2034
  if (element.type === 'TsxCompat') {
2147
2035
  this.#path.pop();
2148
2036
 
2149
- const raise_error = () => {
2150
- this.raise(this.start, `Expected closing tag '</tsx:${element.kind}>'`);
2151
- };
2037
+ if (!element.unclosed) {
2038
+ const raise_error = () => {
2039
+ this.raise(this.start, `Expected closing tag '</tsx:${element.kind}>'`);
2040
+ };
2152
2041
 
2153
- this.next();
2154
- // we should expect to see </tsx:kind>
2155
- if (this.value !== '/') {
2156
- raise_error();
2157
- }
2158
- this.next();
2159
- if (this.value !== 'tsx') {
2160
- raise_error();
2161
- }
2162
- this.next();
2163
- if (this.type.label !== ':') {
2164
- raise_error();
2165
- }
2166
- this.next();
2167
- if (this.value !== element.kind) {
2168
- raise_error();
2169
- }
2170
- this.next();
2171
- if (this.type !== tstt.jsxTagEnd) {
2172
- raise_error();
2042
+ this.next();
2043
+ // we should expect to see </tsx:kind>
2044
+ if (this.value !== '/') {
2045
+ raise_error();
2046
+ }
2047
+ this.next();
2048
+ if (this.value !== 'tsx') {
2049
+ raise_error();
2050
+ }
2051
+ this.next();
2052
+ if (this.type.label !== ':') {
2053
+ raise_error();
2054
+ }
2055
+ this.next();
2056
+ if (this.value !== element.kind) {
2057
+ raise_error();
2058
+ }
2059
+ this.next();
2060
+ if (this.type !== tstt.jsxTagEnd) {
2061
+ raise_error();
2062
+ }
2063
+ this.next();
2173
2064
  }
2174
- this.next();
2175
2065
  } else if (this.#path[this.#path.length - 1] === element) {
2176
2066
  // Check if this element was properly closed
2177
2067
  if (!this.#loose) {
@@ -2235,6 +2125,22 @@ function RipplePlugin(config) {
2235
2125
  this.exprAllowed = true;
2236
2126
 
2237
2127
  while (true) {
2128
+ if (this.type === tt.eof || this.pos >= this.input.length || this.type === tt.braceR) {
2129
+ if (!this.#loose) {
2130
+ this.raise(
2131
+ this.start,
2132
+ `Unclosed tag '<tsx:${inside_tsx_compat.kind}>'. Expected '</tsx:${inside_tsx_compat.kind}>' before end of component.`,
2133
+ );
2134
+ } else {
2135
+ inside_tsx_compat.unclosed = true;
2136
+ /** @type {AST.NodeWithLocation} */ (inside_tsx_compat).loc.end = {
2137
+ .../** @type {AST.SourceLocation} */ (inside_tsx_compat.openingElement.loc).end,
2138
+ };
2139
+ inside_tsx_compat.end = inside_tsx_compat.openingElement.end;
2140
+ }
2141
+ return;
2142
+ }
2143
+
2238
2144
  if (this.input.slice(this.pos, this.pos + 5) === '/tsx:') {
2239
2145
  return;
2240
2146
  }
@@ -2254,9 +2160,9 @@ function RipplePlugin(config) {
2254
2160
  while (this.pos < this.input.length) {
2255
2161
  const ch = this.input.charCodeAt(this.pos);
2256
2162
 
2257
- // Stop at opening tag, closing tag, or expression
2258
- if (ch === 60 || ch === 123) {
2259
- // < or {
2163
+ // Stop at opening tag, expression, or the component-closing brace
2164
+ if (ch === 60 || ch === 123 || ch === 125) {
2165
+ // < or { or }
2260
2166
  break;
2261
2167
  }
2262
2168
 
@@ -2433,16 +2339,16 @@ function RipplePlugin(config) {
2433
2339
  );
2434
2340
  }
2435
2341
 
2436
- if (this.value === '#ripple.server') {
2437
- // Peek ahead to see if this is a server block (#ripple.server { ... }) vs
2438
- // a server identifier expression (#ripple.server.fn(), #ripple.server.fn().then())
2342
+ if (this.value === '#server') {
2343
+ // Peek ahead to see if this is a server block (#server { ... }) vs
2344
+ // a server identifier expression (#server.fn(), #server.fn().then())
2439
2345
  let peek_pos = this.end;
2440
2346
  while (peek_pos < this.input.length && /\s/.test(this.input[peek_pos])) peek_pos++;
2441
2347
  if (peek_pos < this.input.length && this.input.charCodeAt(peek_pos) === 123) {
2442
2348
  // Next non-whitespace character is '{' — parse as server block
2443
2349
  return this.parseServerBlock();
2444
2350
  }
2445
- // Otherwise fall through to parse as expression statement (e.g., #ripple.server.fn().then(...))
2351
+ // Otherwise fall through to parse as expression statement (e.g., #server.fn().then(...))
2446
2352
  }
2447
2353
 
2448
2354
  if (this.value === 'component') {