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.
- package/CHANGELOG.md +43 -0
- package/README.md +2 -1
- package/package.json +2 -6
- package/shims/rollup-estree-types.d.ts +1 -1
- package/src/compiler/index.d.ts +1 -0
- package/src/compiler/index.js +7 -1
- package/src/compiler/phases/1-parse/index.js +15 -6
- package/src/compiler/phases/2-analyze/css-analyze.js +100 -104
- package/src/compiler/phases/2-analyze/index.js +215 -2
- package/src/compiler/phases/3-transform/client/index.js +388 -50
- package/src/compiler/phases/3-transform/segments.js +123 -39
- package/src/compiler/phases/3-transform/server/index.js +266 -13
- package/src/compiler/types/index.d.ts +16 -3
- package/src/compiler/utils.js +1 -15
- package/src/constants.js +0 -2
- package/src/helpers.d.ts +4 -0
- package/src/html-tree-validation.js +211 -0
- package/src/jsx-runtime.d.ts +260 -259
- package/src/jsx-runtime.js +12 -12
- package/src/runtime/array.js +17 -17
- package/src/runtime/create-subscriber.js +1 -1
- package/src/runtime/index-client.js +1 -5
- package/src/runtime/index-server.js +15 -0
- package/src/runtime/internal/client/bindings.js +26 -20
- package/src/runtime/internal/client/compat.js +3 -3
- package/src/runtime/internal/client/composite.js +6 -1
- package/src/runtime/internal/client/head.js +50 -4
- package/src/runtime/internal/client/html.js +73 -12
- package/src/runtime/internal/client/hydration.js +12 -0
- package/src/runtime/internal/client/index.js +1 -1
- package/src/runtime/internal/client/portal.js +54 -29
- package/src/runtime/internal/client/rpc.js +3 -1
- package/src/runtime/internal/client/switch.js +5 -0
- package/src/runtime/internal/client/template.js +117 -11
- package/src/runtime/internal/client/try.js +1 -0
- package/src/runtime/internal/server/index.js +113 -1
- package/src/runtime/internal/server/rpc.js +4 -4
- package/src/runtime/map.js +2 -2
- package/src/runtime/object.js +6 -6
- package/src/runtime/proxy.js +12 -11
- package/src/runtime/reactive-value.js +9 -1
- package/src/runtime/set.js +12 -7
- package/src/runtime/url-search-params.js +0 -1
- package/src/server/index.js +4 -0
- package/src/utils/hashing.js +15 -0
- package/src/utils/normalize_css_property_name.js +1 -1
- package/tests/client/array/array.mutations.test.ripple +8 -8
- package/tests/client/basic/basic.errors.test.ripple +28 -0
- package/tests/client/basic/basic.events.test.ripple +6 -3
- package/tests/client/basic/basic.utilities.test.ripple +1 -1
- package/tests/client/compiler/compiler.regex.test.ripple +10 -8
- package/tests/client/composite/composite.generics.test.ripple +5 -2
- package/tests/client/dynamic-elements.test.ripple +30 -1
- package/tests/client/function-overload-import.ripple +6 -7
- package/tests/client/html.test.ripple +0 -1
- package/tests/client/input-value.test.ripple +539 -469
- package/tests/client/object.test.ripple +2 -2
- package/tests/client/portal.test.ripple +3 -3
- package/tests/client/return.test.ripple +2500 -0
- package/tests/client/try.test.ripple +69 -0
- package/tests/client/typescript-generics.test.ripple +1 -1
- package/tests/client/url/url.derived.test.ripple +1 -1
- package/tests/client/url/url.parsing.test.ripple +3 -3
- package/tests/client/url/url.partial-removal.test.ripple +7 -7
- package/tests/client/url/url.reactivity.test.ripple +15 -15
- package/tests/client/url/url.serialization.test.ripple +2 -2
- package/tests/hydration/basic.test.js +23 -0
- package/tests/hydration/build-components.js +10 -4
- package/tests/hydration/compiled/client/basic.js +165 -3
- package/tests/hydration/compiled/client/for.js +1140 -23
- package/tests/hydration/compiled/client/head.js +234 -0
- package/tests/hydration/compiled/client/html.js +135 -0
- package/tests/hydration/compiled/client/portal.js +172 -0
- package/tests/hydration/compiled/client/reactivity.js +3 -1
- package/tests/hydration/compiled/client/return.js +1976 -0
- package/tests/hydration/compiled/client/switch.js +162 -0
- package/tests/hydration/compiled/server/basic.js +249 -0
- package/tests/hydration/compiled/server/events.js +1 -1
- package/tests/hydration/compiled/server/for.js +891 -1
- package/tests/hydration/compiled/server/head.js +291 -0
- package/tests/hydration/compiled/server/html.js +133 -0
- package/tests/hydration/compiled/server/if.js +1 -1
- package/tests/hydration/compiled/server/portal.js +250 -0
- package/tests/hydration/compiled/server/reactivity.js +1 -1
- package/tests/hydration/compiled/server/return.js +1969 -0
- package/tests/hydration/compiled/server/switch.js +130 -0
- package/tests/hydration/components/basic.ripple +55 -0
- package/tests/hydration/components/for.ripple +403 -0
- package/tests/hydration/components/head.ripple +111 -0
- package/tests/hydration/components/html.ripple +38 -0
- package/tests/hydration/components/portal.ripple +49 -0
- package/tests/hydration/components/return.ripple +564 -0
- package/tests/hydration/components/switch.ripple +51 -0
- package/tests/hydration/for.test.js +363 -0
- package/tests/hydration/head.test.js +105 -0
- package/tests/hydration/html.test.js +46 -0
- package/tests/hydration/portal.test.js +71 -0
- package/tests/hydration/return.test.js +544 -0
- package/tests/hydration/switch.test.js +42 -0
- package/tests/server/basic.attributes.test.ripple +1 -1
- package/tests/server/compiler.test.ripple +22 -0
- package/tests/server/composite.test.ripple +5 -2
- package/tests/server/html-nesting-validation.test.ripple +237 -0
- package/tests/server/return.test.ripple +1379 -0
- package/tests/setup-hydration.js +6 -1
- package/tests/utils/escaping.test.js +3 -1
- package/tests/utils/normalize_css_property_name.test.js +0 -1
- package/tests/utils/patterns.test.js +6 -2
- package/tests/utils/sanitize_template_string.test.js +3 -2
- 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
|
|
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.
|
|
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.
|
|
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
|
|
2
|
+
export * from 'rollup/dist/rollup.js';
|
package/src/compiler/index.d.ts
CHANGED
package/src/compiler/index.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
for (
|
|
52
|
-
let
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
if (is_global_block) {
|
|
60
|
+
// All selectors after :global are unscoped
|
|
61
|
+
child.metadata.is_global_like = true;
|
|
62
|
+
}
|
|
66
63
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
86
|
+
ComplexSelector(node, context) {
|
|
87
|
+
// Set the rule metadata before analyzing children
|
|
88
|
+
node.metadata.rule = context.state.rule;
|
|
92
89
|
|
|
93
|
-
|
|
90
|
+
context.next(); // analyze relevant selectors first
|
|
94
91
|
|
|
95
|
-
|
|
96
|
-
|
|
92
|
+
{
|
|
93
|
+
const global = node.children.find(is_global);
|
|
97
94
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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
|
|