ripple 0.2.207 → 0.2.210

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 (110) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +2 -1
  3. package/package.json +2 -6
  4. package/shims/rollup-estree-types.d.ts +1 -1
  5. package/src/compiler/index.d.ts +1 -0
  6. package/src/compiler/index.js +7 -1
  7. package/src/compiler/phases/1-parse/index.js +15 -6
  8. package/src/compiler/phases/2-analyze/css-analyze.js +100 -104
  9. package/src/compiler/phases/2-analyze/index.js +215 -2
  10. package/src/compiler/phases/3-transform/client/index.js +388 -50
  11. package/src/compiler/phases/3-transform/segments.js +123 -39
  12. package/src/compiler/phases/3-transform/server/index.js +266 -13
  13. package/src/compiler/types/index.d.ts +16 -3
  14. package/src/compiler/utils.js +1 -15
  15. package/src/constants.js +0 -2
  16. package/src/helpers.d.ts +4 -0
  17. package/src/html-tree-validation.js +211 -0
  18. package/src/jsx-runtime.d.ts +260 -259
  19. package/src/jsx-runtime.js +12 -12
  20. package/src/runtime/array.js +17 -17
  21. package/src/runtime/create-subscriber.js +1 -1
  22. package/src/runtime/index-client.js +1 -5
  23. package/src/runtime/index-server.js +15 -0
  24. package/src/runtime/internal/client/bindings.js +26 -20
  25. package/src/runtime/internal/client/compat.js +3 -3
  26. package/src/runtime/internal/client/composite.js +6 -1
  27. package/src/runtime/internal/client/head.js +50 -4
  28. package/src/runtime/internal/client/html.js +73 -12
  29. package/src/runtime/internal/client/hydration.js +12 -0
  30. package/src/runtime/internal/client/index.js +1 -1
  31. package/src/runtime/internal/client/portal.js +54 -29
  32. package/src/runtime/internal/client/rpc.js +3 -1
  33. package/src/runtime/internal/client/switch.js +5 -0
  34. package/src/runtime/internal/client/template.js +117 -11
  35. package/src/runtime/internal/client/try.js +1 -0
  36. package/src/runtime/internal/server/index.js +113 -1
  37. package/src/runtime/internal/server/rpc.js +4 -4
  38. package/src/runtime/map.js +2 -2
  39. package/src/runtime/object.js +6 -6
  40. package/src/runtime/proxy.js +12 -11
  41. package/src/runtime/reactive-value.js +9 -1
  42. package/src/runtime/set.js +12 -7
  43. package/src/runtime/url-search-params.js +0 -1
  44. package/src/server/index.js +4 -0
  45. package/src/utils/hashing.js +15 -0
  46. package/src/utils/normalize_css_property_name.js +1 -1
  47. package/tests/client/array/array.mutations.test.ripple +8 -8
  48. package/tests/client/basic/basic.errors.test.ripple +28 -0
  49. package/tests/client/basic/basic.events.test.ripple +6 -3
  50. package/tests/client/basic/basic.utilities.test.ripple +1 -1
  51. package/tests/client/compiler/compiler.regex.test.ripple +10 -8
  52. package/tests/client/composite/composite.generics.test.ripple +5 -2
  53. package/tests/client/dynamic-elements.test.ripple +30 -1
  54. package/tests/client/function-overload-import.ripple +6 -7
  55. package/tests/client/html.test.ripple +0 -1
  56. package/tests/client/input-value.test.ripple +539 -469
  57. package/tests/client/object.test.ripple +2 -2
  58. package/tests/client/portal.test.ripple +3 -3
  59. package/tests/client/return.test.ripple +2500 -0
  60. package/tests/client/try.test.ripple +69 -0
  61. package/tests/client/typescript-generics.test.ripple +1 -1
  62. package/tests/client/url/url.derived.test.ripple +1 -1
  63. package/tests/client/url/url.parsing.test.ripple +3 -3
  64. package/tests/client/url/url.partial-removal.test.ripple +7 -7
  65. package/tests/client/url/url.reactivity.test.ripple +15 -15
  66. package/tests/client/url/url.serialization.test.ripple +2 -2
  67. package/tests/hydration/basic.test.js +23 -0
  68. package/tests/hydration/build-components.js +10 -4
  69. package/tests/hydration/compiled/client/basic.js +165 -3
  70. package/tests/hydration/compiled/client/for.js +1140 -23
  71. package/tests/hydration/compiled/client/head.js +234 -0
  72. package/tests/hydration/compiled/client/html.js +135 -0
  73. package/tests/hydration/compiled/client/portal.js +172 -0
  74. package/tests/hydration/compiled/client/reactivity.js +3 -1
  75. package/tests/hydration/compiled/client/return.js +1976 -0
  76. package/tests/hydration/compiled/client/switch.js +162 -0
  77. package/tests/hydration/compiled/server/basic.js +249 -0
  78. package/tests/hydration/compiled/server/events.js +1 -1
  79. package/tests/hydration/compiled/server/for.js +891 -1
  80. package/tests/hydration/compiled/server/head.js +291 -0
  81. package/tests/hydration/compiled/server/html.js +133 -0
  82. package/tests/hydration/compiled/server/if.js +1 -1
  83. package/tests/hydration/compiled/server/portal.js +250 -0
  84. package/tests/hydration/compiled/server/reactivity.js +1 -1
  85. package/tests/hydration/compiled/server/return.js +1969 -0
  86. package/tests/hydration/compiled/server/switch.js +130 -0
  87. package/tests/hydration/components/basic.ripple +55 -0
  88. package/tests/hydration/components/for.ripple +403 -0
  89. package/tests/hydration/components/head.ripple +111 -0
  90. package/tests/hydration/components/html.ripple +38 -0
  91. package/tests/hydration/components/portal.ripple +49 -0
  92. package/tests/hydration/components/return.ripple +564 -0
  93. package/tests/hydration/components/switch.ripple +51 -0
  94. package/tests/hydration/for.test.js +363 -0
  95. package/tests/hydration/head.test.js +105 -0
  96. package/tests/hydration/html.test.js +46 -0
  97. package/tests/hydration/portal.test.js +71 -0
  98. package/tests/hydration/return.test.js +544 -0
  99. package/tests/hydration/switch.test.js +42 -0
  100. package/tests/server/basic.attributes.test.ripple +1 -1
  101. package/tests/server/compiler.test.ripple +22 -0
  102. package/tests/server/composite.test.ripple +5 -2
  103. package/tests/server/html-nesting-validation.test.ripple +237 -0
  104. package/tests/server/return.test.ripple +1379 -0
  105. package/tests/setup-hydration.js +6 -1
  106. package/tests/utils/escaping.test.js +3 -1
  107. package/tests/utils/normalize_css_property_name.test.js +0 -1
  108. package/tests/utils/patterns.test.js +6 -2
  109. package/tests/utils/sanitize_template_string.test.js +3 -2
  110. package/types/server.d.ts +16 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,43 @@
1
+ # ripple
2
+
3
+ ## 0.2.210
4
+
5
+ ### Patch Changes
6
+
7
+ - Fix npm OIDC publishing workflow
8
+
9
+ - Updated dependencies []:
10
+ - ripple@0.2.210
11
+
12
+ ## 0.2.209
13
+
14
+ ### Patch Changes
15
+
16
+ - [#682](https://github.com/Ripple-TS/ripple/pull/682)
17
+ [`96a5614`](https://github.com/Ripple-TS/ripple/commit/96a56141de8aa667a64bf53ad06f63292e38b1d9)
18
+ Thanks [@copilot-swe-agent](https://github.com/apps/copilot-swe-agent)! - Add
19
+ invalid HTML nesting error detection during SSR in dev mode
20
+
21
+ During SSR, if the HTML is malformed (e.g., `<button>` elements nested inside
22
+ other `<button>` elements), the browser tries to repair the HTML, making
23
+ hydration impossible. This change adds runtime validation of HTML nesting during
24
+ SSR to detect these cases and provide clear error messages.
25
+ - Added `push_element` and `pop_element` functions to the server runtime that
26
+ track the element stack during SSR
27
+ - Added comprehensive HTML nesting validation rules based on the HTML spec
28
+ - The server compiler now emits `push_element`/`pop_element` calls when the
29
+ `dev` option is enabled
30
+ - Added `dev` option to `CompileOptions`
31
+ - The Vite plugin now automatically enables dev mode during `vite dev` (serve
32
+ command)
33
+
34
+ - [#683](https://github.com/Ripple-TS/ripple/pull/683)
35
+ [`ae3aa98`](https://github.com/Ripple-TS/ripple/commit/ae3aa981515f81e62a699497e624dd0c2e3d2c91)
36
+ Thanks [@WebEferen](https://github.com/WebEferen)! - Fix SSR hydration output
37
+ for early-return guarded content by emitting hydration block markers around
38
+ return-guarded regions, and add hydration/server coverage for early return
39
+ scenarios.
40
+ - Updated dependencies
41
+ [[`96a5614`](https://github.com/Ripple-TS/ripple/commit/96a56141de8aa667a64bf53ad06f63292e38b1d9),
42
+ [`ae3aa98`](https://github.com/Ripple-TS/ripple/commit/ae3aa981515f81e62a699497e624dd0c2e3d2c91)]:
43
+ - ripple@0.2.209
package/README.md CHANGED
@@ -5,4 +5,5 @@
5
5
 
6
6
  # What is Ripple?
7
7
 
8
- Ripple is an elegant TypeScript UI framework. To find out more, view [Ripple's Github README](https://github.com/Ripple-TS/ripple).
8
+ Ripple is an elegant TypeScript UI framework. To find out more, view
9
+ [Ripple's Github README](https://github.com/Ripple-TS/ripple).
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.2.207",
6
+ "version": "0.2.210",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -55,10 +55,6 @@
55
55
  "./internal/server": {
56
56
  "default": "./src/runtime/internal/server/index.js"
57
57
  },
58
- "./ssr": {
59
- "types": "./types/index.d.ts",
60
- "default": "./src/runtime/index-server.js"
61
- },
62
58
  "./jsx-runtime": {
63
59
  "types": "./src/jsx-runtime.d.ts",
64
60
  "import": "./src/jsx-runtime.js",
@@ -97,6 +93,6 @@
97
93
  "vscode-languageserver-types": "^3.17.5"
98
94
  },
99
95
  "peerDependencies": {
100
- "ripple": "0.2.207"
96
+ "ripple": "0.2.210"
101
97
  }
102
98
  }
@@ -1,2 +1,2 @@
1
1
  // Re-export Rollup module types WITHOUT globals
2
- export * from "rollup/dist/rollup.js";
2
+ export * from 'rollup/dist/rollup.js';
@@ -97,6 +97,7 @@ export interface RippleCompileError extends Error {
97
97
 
98
98
  interface SharedCompileOptions {
99
99
  minify_css?: boolean;
100
+ dev?: boolean;
100
101
  }
101
102
  export interface CompileOptions extends SharedCompileOptions {
102
103
  mode?: 'client' | 'server';
@@ -27,7 +27,13 @@ export function compile(source, filename, options = {}) {
27
27
  const analysis = analyze(ast, filename, options);
28
28
  const result =
29
29
  options.mode === 'server'
30
- ? transform_server(filename, source, analysis, options?.minify_css ?? false)
30
+ ? transform_server(
31
+ filename,
32
+ source,
33
+ analysis,
34
+ options?.minify_css ?? false,
35
+ options?.dev ?? false,
36
+ )
31
37
  : transform_client(filename, source, analysis, false, options?.minify_css ?? false);
32
38
 
33
39
  return result;
@@ -109,8 +109,15 @@ function skipWhitespace(parser) {
109
109
  // Update line tracking if whitespace was skipped
110
110
  if (parser.start !== originalStart) {
111
111
  lineInfo = acorn.getLineInfo(parser.input, parser.start);
112
- parser.curLine = lineInfo.line;
113
- parser.lineStart = parser.start - lineInfo.column;
112
+ // Only update curLine/lineStart if the tokenizer hasn't already
113
+ // advanced past this position. When parser.pos > parser.start,
114
+ // acorn's internal skipSpace() has already processed comments and
115
+ // whitespace beyond where we stopped, so curLine/lineStart already
116
+ // reflect a later (correct) position that we must not overwrite.
117
+ if (parser.pos <= parser.start) {
118
+ parser.curLine = lineInfo.line;
119
+ parser.lineStart = parser.start - lineInfo.column;
120
+ }
114
121
  }
115
122
 
116
123
  // After skipping whitespace, update startLoc to reflect our actual position
@@ -2007,8 +2014,12 @@ function RipplePlugin(config) {
2007
2014
  }
2008
2015
  if (attr.value !== null) {
2009
2016
  if (attr.value.type === 'JSXExpressionContainer') {
2017
+ const expression = attr.value.expression;
2018
+ if (expression.type === 'Literal') {
2019
+ expression.was_expression = true;
2020
+ }
2010
2021
  /** @type {ESTreeJSX.JSXExpressionContainer['expression']} */ (attr.value) =
2011
- attr.value.expression;
2022
+ expression;
2012
2023
  }
2013
2024
  }
2014
2025
  }
@@ -2274,9 +2285,6 @@ function RipplePlugin(config) {
2274
2285
  const inside_tsx_compat = this.#path.findLast((n) => n.type === 'TsxCompat');
2275
2286
 
2276
2287
  if (!inside_func) {
2277
- if (this.type.label === 'return') {
2278
- throw new Error('`return` statements are not allowed in components');
2279
- }
2280
2288
  if (this.type.label === 'continue') {
2281
2289
  throw new Error('`continue` statements are not allowed in components');
2282
2290
  }
@@ -3158,6 +3166,7 @@ export function parse(source, filename, options) {
3158
3166
  ast = parser.parse(source, {
3159
3167
  sourceType: 'module',
3160
3168
  ecmaVersion: 13,
3169
+ allowReturnOutsideFunction: true,
3161
3170
  locations: true,
3162
3171
  onComment,
3163
3172
  rippleOptions: {
@@ -39,126 +39,122 @@ function is_global(relative_selector) {
39
39
  * @param {AST.CSS.Node} css - The CSS AST
40
40
  */
41
41
  export function analyze_css(css) {
42
- walk(
43
- css,
44
- /** @type {{ rule: AST.CSS.Rule | null }} */ ({ rule: null }),
45
- {
46
- Rule(node, context) {
47
- node.metadata.parent_rule = context.state.rule;
48
-
49
- // Check for :global blocks
50
- // A global block is when the selector starts with :global and has no local selectors before it
51
- for (const complex_selector of node.prelude.children) {
52
- let is_global_block = false;
53
-
54
- for (
55
- let selector_idx = 0;
56
- selector_idx < complex_selector.children.length;
57
- selector_idx++
58
- ) {
59
- const child = complex_selector.children[selector_idx];
60
- const idx = child.selectors.findIndex(is_global_block_selector);
42
+ walk(css, /** @type {{ rule: AST.CSS.Rule | null }} */ ({ rule: null }), {
43
+ Rule(node, context) {
44
+ node.metadata.parent_rule = context.state.rule;
45
+
46
+ // Check for :global blocks
47
+ // A global block is when the selector starts with :global and has no local selectors before it
48
+ for (const complex_selector of node.prelude.children) {
49
+ let is_global_block = false;
50
+
51
+ for (
52
+ let selector_idx = 0;
53
+ selector_idx < complex_selector.children.length;
54
+ selector_idx++
55
+ ) {
56
+ const child = complex_selector.children[selector_idx];
57
+ const idx = child.selectors.findIndex(is_global_block_selector);
61
58
 
62
- if (is_global_block) {
63
- // All selectors after :global are unscoped
64
- child.metadata.is_global_like = true;
65
- }
59
+ if (is_global_block) {
60
+ // All selectors after :global are unscoped
61
+ child.metadata.is_global_like = true;
62
+ }
66
63
 
67
- // Only set is_global_block if this is the FIRST RelativeSelector and it starts with :global
68
- if (selector_idx === 0 && idx === 0) {
69
- // `child` starts with `:global` and is the first selector in the chain
70
- is_global_block = true;
71
- node.metadata.is_global_block = is_global_block;
72
- } else if (idx === 0) {
73
- // :global appears later in the selector chain (e.g., `div :global p`)
74
- // Set is_global_block for marking subsequent selectors as global-like
75
- is_global_block = true;
76
- } else if (idx !== -1) {
77
- // `:global` is not at the start - this is invalid but we'll let it through for now
78
- // The transform phase will handle removal
79
- }
64
+ // Only set is_global_block if this is the FIRST RelativeSelector and it starts with :global
65
+ if (selector_idx === 0 && idx === 0) {
66
+ // `child` starts with `:global` and is the first selector in the chain
67
+ is_global_block = true;
68
+ node.metadata.is_global_block = is_global_block;
69
+ } else if (idx === 0) {
70
+ // :global appears later in the selector chain (e.g., `div :global p`)
71
+ // Set is_global_block for marking subsequent selectors as global-like
72
+ is_global_block = true;
73
+ } else if (idx !== -1) {
74
+ // `:global` is not at the start - this is invalid but we'll let it through for now
75
+ // The transform phase will handle removal
80
76
  }
81
77
  }
78
+ }
82
79
 
83
- // Pass the current rule as state to nested nodes
84
- const state = { rule: node };
85
- context.visit(node.prelude, state);
86
- context.visit(node.block, state);
87
- },
80
+ // Pass the current rule as state to nested nodes
81
+ const state = { rule: node };
82
+ context.visit(node.prelude, state);
83
+ context.visit(node.block, state);
84
+ },
88
85
 
89
- ComplexSelector(node, context) {
90
- // Set the rule metadata before analyzing children
91
- node.metadata.rule = context.state.rule;
86
+ ComplexSelector(node, context) {
87
+ // Set the rule metadata before analyzing children
88
+ node.metadata.rule = context.state.rule;
92
89
 
93
- context.next(); // analyze relevant selectors first
90
+ context.next(); // analyze relevant selectors first
94
91
 
95
- {
96
- const global = node.children.find(is_global);
92
+ {
93
+ const global = node.children.find(is_global);
97
94
 
98
- if (global) {
99
- const is_nested = context.path.at(-2)?.type === 'PseudoClassSelector';
100
- if (
101
- is_nested &&
102
- !(/** @type {AST.CSS.PseudoClassSelector} */ (global.selectors[0]).args)
103
- ) {
104
- throw new Error(`A :global selector cannot be inside a pseudoclass.`);
105
- }
95
+ if (global) {
96
+ const is_nested = context.path.at(-2)?.type === 'PseudoClassSelector';
97
+ if (
98
+ is_nested &&
99
+ !(/** @type {AST.CSS.PseudoClassSelector} */ (global.selectors[0]).args)
100
+ ) {
101
+ throw new Error(`A :global selector cannot be inside a pseudoclass.`);
102
+ }
106
103
 
107
- const idx = node.children.indexOf(global);
108
- const first = /** @type {AST.CSS.PseudoClassSelector} */ (global.selectors[0]);
109
- if (first.args !== null && idx !== 0 && idx !== node.children.length - 1) {
110
- // ensure `:global(...)` is not used in the middle of a selector (but multiple `global(...)` in sequence are ok)
111
- for (let i = idx + 1; i < node.children.length; i++) {
112
- if (!is_global(node.children[i])) {
113
- throw new Error(
114
- `:global(...) can be at the start or end of a selector sequence, but not in the middle.`,
115
- );
116
- }
104
+ const idx = node.children.indexOf(global);
105
+ const first = /** @type {AST.CSS.PseudoClassSelector} */ (global.selectors[0]);
106
+ if (first.args !== null && idx !== 0 && idx !== node.children.length - 1) {
107
+ // ensure `:global(...)` is not used in the middle of a selector (but multiple `global(...)` in sequence are ok)
108
+ for (let i = idx + 1; i < node.children.length; i++) {
109
+ if (!is_global(node.children[i])) {
110
+ throw new Error(
111
+ `:global(...) can be at the start or end of a selector sequence, but not in the middle.`,
112
+ );
117
113
  }
118
114
  }
119
115
  }
120
116
  }
117
+ }
121
118
 
122
- // Set is_global metadata
123
- node.metadata.is_global = node.children.every(
124
- ({ metadata }) => metadata.is_global || metadata.is_global_like,
125
- );
126
-
127
- node.metadata.used ||= node.metadata.is_global;
128
- },
129
-
130
- PseudoClassSelector(node, context) {
131
- // Walk into :is(), :where(), :has(), and :not() to initialize metadata for nested selectors
132
- if (
133
- (node.name === 'is' ||
134
- node.name === 'where' ||
135
- node.name === 'has' ||
136
- node.name === 'not') &&
137
- node.args
138
- ) {
139
- context.next();
140
- }
141
- },
142
- RelativeSelector(node, context) {
143
- // Check if this selector is a :global selector
144
- node.metadata.is_global = node.selectors.length >= 1 && is_global(node);
145
-
146
- // Check for :root and other global-like selectors
147
- if (
148
- node.selectors.length >= 1 &&
149
- node.selectors.every(
150
- (selector) =>
151
- selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector',
152
- )
153
- ) {
154
- const first = node.selectors[0];
155
- node.metadata.is_global_like ||=
156
- (first.type === 'PseudoClassSelector' && first.name === 'host') ||
157
- (first.type === 'PseudoClassSelector' && first.name === 'root');
158
- }
119
+ // Set is_global metadata
120
+ node.metadata.is_global = node.children.every(
121
+ ({ metadata }) => metadata.is_global || metadata.is_global_like,
122
+ );
159
123
 
124
+ node.metadata.used ||= node.metadata.is_global;
125
+ },
126
+
127
+ PseudoClassSelector(node, context) {
128
+ // Walk into :is(), :where(), :has(), and :not() to initialize metadata for nested selectors
129
+ if (
130
+ (node.name === 'is' ||
131
+ node.name === 'where' ||
132
+ node.name === 'has' ||
133
+ node.name === 'not') &&
134
+ node.args
135
+ ) {
160
136
  context.next();
161
- },
137
+ }
162
138
  },
163
- );
139
+ RelativeSelector(node, context) {
140
+ // Check if this selector is a :global selector
141
+ node.metadata.is_global = node.selectors.length >= 1 && is_global(node);
142
+
143
+ // Check for :root and other global-like selectors
144
+ if (
145
+ node.selectors.length >= 1 &&
146
+ node.selectors.every(
147
+ (selector) =>
148
+ selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector',
149
+ )
150
+ ) {
151
+ const first = node.selectors[0];
152
+ node.metadata.is_global_like ||=
153
+ (first.type === 'PseudoClassSelector' && first.name === 'host') ||
154
+ (first.type === 'PseudoClassSelector' && first.name === 'root');
155
+ }
156
+
157
+ context.next();
158
+ },
159
+ });
164
160
  }
@@ -109,6 +109,113 @@ function mark_as_tracked(path) {
109
109
  }
110
110
  }
111
111
 
112
+ /**
113
+ * @param {AST.ReturnStatement} node
114
+ * @returns {AST.ReturnStatement}
115
+ */
116
+ function get_return_keyword_node(node) {
117
+ const return_keyword_length = 'return'.length;
118
+ return /** @type {AST.ReturnStatement} */ ({
119
+ ...node,
120
+ end: /** @type {AST.NodeWithLocation} */ (node).start + return_keyword_length,
121
+ loc: {
122
+ start: /** @type {AST.NodeWithLocation} */ (node).loc.start,
123
+ end: {
124
+ line: /** @type {AST.NodeWithLocation} */ (node).loc.start.line,
125
+ column: /** @type {AST.NodeWithLocation} */ (node).loc.start.column + return_keyword_length,
126
+ },
127
+ },
128
+ });
129
+ }
130
+
131
+ /**
132
+ * @param {AST.ReturnStatement} node
133
+ * @param {AnalysisContext} context
134
+ * @param {string} message
135
+ */
136
+ function error_return_keyword(node, context, message) {
137
+ const return_keyword_node = get_return_keyword_node(node);
138
+
139
+ error(
140
+ message,
141
+ context.state.analysis.module.filename,
142
+ return_keyword_node,
143
+ context.state.loose ? context.state.analysis.errors : undefined,
144
+ );
145
+ }
146
+
147
+ /**
148
+ * @param {AST.Expression} expression
149
+ * @returns {AST.Expression}
150
+ */
151
+ function unwrap_template_expression(expression) {
152
+ /** @type {AST.Expression} */
153
+ let node = expression;
154
+
155
+ while (true) {
156
+ if (
157
+ node.type === 'ParenthesizedExpression' ||
158
+ node.type === 'TSAsExpression' ||
159
+ node.type === 'TSSatisfiesExpression' ||
160
+ node.type === 'TSNonNullExpression' ||
161
+ node.type === 'TSInstantiationExpression'
162
+ ) {
163
+ node = /** @type {AST.Expression} */ (node.expression);
164
+ continue;
165
+ }
166
+
167
+ if (node.type === 'ChainExpression') {
168
+ node = /** @type {AST.Expression} */ (node.expression);
169
+ continue;
170
+ }
171
+
172
+ break;
173
+ }
174
+
175
+ return node;
176
+ }
177
+
178
+ /**
179
+ * @param {AST.Expression} expression
180
+ * @param {AnalysisState} state
181
+ * @returns {boolean}
182
+ */
183
+ function is_children_template_expression(expression, state) {
184
+ const unwrapped = unwrap_template_expression(expression);
185
+
186
+ if (unwrapped.type === 'TrackedExpression') {
187
+ return is_children_template_expression(
188
+ /** @type {AST.Expression} */ (unwrapped.argument),
189
+ state,
190
+ );
191
+ }
192
+
193
+ if (unwrapped.type === 'MemberExpression') {
194
+ let property_name = null;
195
+
196
+ if (!unwrapped.computed && unwrapped.property.type === 'Identifier') {
197
+ property_name = unwrapped.property.name;
198
+ } else if (
199
+ unwrapped.computed &&
200
+ unwrapped.property.type === 'Literal' &&
201
+ typeof unwrapped.property.value === 'string'
202
+ ) {
203
+ property_name = unwrapped.property.value;
204
+ }
205
+
206
+ if (property_name === 'children') {
207
+ const target = unwrap_template_expression(/** @type {AST.Expression} */ (unwrapped.object));
208
+
209
+ if (target.type === 'Identifier') {
210
+ const binding = state.scope.get(target.name);
211
+ return binding?.declaration_kind === 'param';
212
+ }
213
+ }
214
+ }
215
+
216
+ return unwrapped.type === 'Identifier' && unwrapped.name === 'children';
217
+ }
218
+
112
219
  /** @type {Visitors<AST.Node, AnalysisState>} */
113
220
  const visitors = {
114
221
  _(node, { state, next, path }) {
@@ -128,6 +235,14 @@ const visitors = {
128
235
  },
129
236
 
130
237
  ServerBlock(node, context) {
238
+ if (context.path.at(-1)?.type !== 'Program') {
239
+ // fatal since we don't have a transformation defined for this case
240
+ error(
241
+ '`#server` block can only be declared at the module level.',
242
+ context.state.analysis.module.filename,
243
+ node,
244
+ );
245
+ }
131
246
  node.metadata = {
132
247
  ...node.metadata,
133
248
  exports: new Set(),
@@ -331,6 +446,13 @@ const visitors = {
331
446
  context.next();
332
447
  },
333
448
 
449
+ NewExpression(node, context) {
450
+ if (context.state.metadata?.tracking === false) {
451
+ context.state.metadata.tracking = true;
452
+ }
453
+ context.next();
454
+ },
455
+
334
456
  VariableDeclaration(node, context) {
335
457
  const { state, visit } = context;
336
458
 
@@ -732,9 +854,26 @@ const visitors = {
732
854
  has_await: false,
733
855
  };
734
856
 
857
+ const test_metadata = { tracking: false };
858
+ context.visit(node.test, { ...context.state, metadata: test_metadata });
859
+ if (test_metadata.tracking) {
860
+ /** @type {AST.TrackedNode} */ (node.test).tracked = true;
861
+ }
862
+
735
863
  context.visit(node.consequent, context.state);
736
864
 
737
- if (!node.metadata.has_template) {
865
+ const consequent_body =
866
+ node.consequent.type === 'BlockStatement' ? node.consequent.body : [node.consequent];
867
+
868
+ if (
869
+ consequent_body.length === 1 &&
870
+ consequent_body[0].type === 'ReturnStatement' &&
871
+ !node.alternate
872
+ ) {
873
+ node.metadata.lone_return = true;
874
+ }
875
+
876
+ if (!node.metadata.has_template && !node.metadata.has_return) {
738
877
  error(
739
878
  'Component if statements must contain a template in their "then" body. Move the if statement into an effect if it does not render anything.',
740
879
  context.state.analysis.module.filename,
@@ -744,11 +883,13 @@ const visitors = {
744
883
  }
745
884
 
746
885
  if (node.alternate) {
886
+ const saved_has_return = node.metadata.has_return;
887
+ const saved_returns = node.metadata.returns;
747
888
  node.metadata.has_template = false;
748
889
  node.metadata.has_await = false;
749
890
  context.visit(node.alternate, context.state);
750
891
 
751
- if (!node.metadata.has_template) {
892
+ if (!node.metadata.has_template && !node.metadata.has_return) {
752
893
  error(
753
894
  'Component if statements must contain a template in their "else" body. Move the if statement into an effect if it does not render anything.',
754
895
  context.state.analysis.module.filename,
@@ -756,6 +897,63 @@ const visitors = {
756
897
  context.state.loose ? context.state.analysis.errors : undefined,
757
898
  );
758
899
  }
900
+
901
+ if (saved_has_return) {
902
+ node.metadata.has_return = true;
903
+ if (saved_returns) {
904
+ node.metadata.returns = [...saved_returns, ...(node.metadata.returns || [])];
905
+ }
906
+ }
907
+ }
908
+ },
909
+
910
+ ReturnStatement(node, context) {
911
+ const parent = context.path.at(-1);
912
+
913
+ if (!is_inside_component(context)) {
914
+ if (parent?.type === 'Program') {
915
+ error_return_keyword(
916
+ node,
917
+ context,
918
+ 'Return statements are not allowed at the top level of a module.',
919
+ );
920
+ }
921
+
922
+ return context.next();
923
+ }
924
+
925
+ if (node.argument !== null) {
926
+ error_return_keyword(
927
+ node,
928
+ context,
929
+ 'Return statements inside components cannot have a return value.',
930
+ );
931
+ }
932
+
933
+ for (let i = context.path.length - 1; i >= 0; i--) {
934
+ const ancestor = context.path[i];
935
+
936
+ if (
937
+ ancestor.type === 'Component' ||
938
+ ancestor.type === 'FunctionExpression' ||
939
+ ancestor.type === 'ArrowFunctionExpression' ||
940
+ ancestor.type === 'FunctionDeclaration'
941
+ ) {
942
+ break;
943
+ }
944
+
945
+ if (
946
+ ancestor.type === 'IfStatement' &&
947
+ /** @type {AST.TrackedNode} */ (ancestor.test).tracked
948
+ ) {
949
+ node.metadata.is_reactive = true;
950
+ }
951
+
952
+ if (!ancestor.metadata.returns) {
953
+ ancestor.metadata.returns = [];
954
+ }
955
+ ancestor.metadata.returns.push(node);
956
+ ancestor.metadata.has_return = true;
759
957
  }
760
958
  },
761
959
 
@@ -1092,6 +1290,21 @@ const visitors = {
1092
1290
 
1093
1291
  Text(node, context) {
1094
1292
  mark_control_flow_has_template(context.path);
1293
+
1294
+ if (
1295
+ is_children_template_expression(
1296
+ /** @type {AST.Expression} */ (node.expression),
1297
+ context.state,
1298
+ )
1299
+ ) {
1300
+ error(
1301
+ '`children` cannot be rendered using text interpolation. Use `<children />` instead.',
1302
+ context.state.analysis.module.filename,
1303
+ node.expression,
1304
+ context.state.loose ? context.state.analysis.errors : undefined,
1305
+ );
1306
+ }
1307
+
1095
1308
  context.next();
1096
1309
  },
1097
1310