ripple 0.2.3 → 0.2.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.
- package/README.md +7 -353
- package/package.json +1 -1
- package/src/compiler/phases/1-parse/index.js +68 -11
- package/src/compiler/phases/2-analyze/index.js +7 -15
- package/src/compiler/phases/2-analyze/prune.js +232 -109
- package/src/compiler/phases/3-transform/index.js +35 -14
- package/src/runtime/internal/client/constants.js +1 -0
- package/src/runtime/internal/client/index.js +1 -1
- package/src/runtime/internal/client/render.js +0 -10
- package/src/runtime/internal/client/runtime.js +21 -5
- package/src/runtime/internal/client/try.js +2 -2
- package/types/index.d.ts +1 -1
package/README.md
CHANGED
|
@@ -1,356 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
<picture>
|
|
2
|
+
<source media="(prefers-color-scheme: dark)" srcset="assets/ripple-dark.png">
|
|
3
|
+
<img src="assets/ripple-light.png" alt="Ripple - the elegant UI framework for the web" />
|
|
4
|
+
</picture>
|
|
2
5
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Ripple is a TypeScript UI framework for the web.
|
|
6
|
-
|
|
7
|
-
I wrote Ripple as a love letter for frontend web – and this is largely a project that I built in less than a week, so it's very raw.
|
|
8
|
-
|
|
9
|
-
Personally, I ([@trueadm](https://github.com/trueadm)) have been involved in some truly amazing frontend frameworks along their journeys – from [Inferno](https://github.com/infernojs/inferno), where it all began, to [React](https://github.com/facebook/react) and the journey of React Hooks, to creating [Lexical](https://github.com/facebook/lexical), to [Svelte 5](https://github.com/sveltejs/svelte) and its new compiler and signal-based reactivity runtime. Along that journey, I collected ideas, and intriguing thoughts that may or may not pay off. Given my time between roles, I decided it was the best opportunity to try them out, and for open source to see what I was cooking.
|
|
10
|
-
|
|
11
|
-
Ripple was designed to be a JS/TS-first framework, rather than HTML-first. Ripple modules have their own `.ripple` extension and these modules
|
|
12
|
-
fully support TypeScript. By introducing a new extension, it affords Ripple to invent its own superset language, that plays really nicely with
|
|
13
|
-
TypeScript and JSX, but with a few interesting touches. In my experience, this has led to better DX not only for humans, but also for LLMs.
|
|
14
|
-
|
|
15
|
-
Right now, there will be plenty of bugs, things just won't work either and you'll find TODOs everywhere. At this stage, Ripple is more of an early alpha version of something that _might_ be, rather than something you should try and adopt. If anything, maybe some of the ideas can be shared and incubated back into other frameworks. There's also a lot of similarities with Svelte 5, and that's not by accident, that's because of my recent time working on Svelte 5.
|
|
16
|
-
|
|
17
|
-
## Features
|
|
18
|
-
|
|
19
|
-
- **Reactive State Management**: Built-in reactivity with `$` prefixed variables
|
|
20
|
-
- **Component-Based Architecture**: Clean, reusable components with props and children
|
|
21
|
-
- **JSX-like Syntax**: Familiar templating with Ripple-specific enhancements
|
|
22
|
-
- **TypeScript Support**: Full TypeScript integration with type checking
|
|
23
|
-
- **VSCode Integration**: Rich editor support with diagnostics, syntax highlighting, and IntelliSense
|
|
24
|
-
|
|
25
|
-
## Missing Features
|
|
26
|
-
|
|
27
|
-
- **SSR**: Ripple is currently an SPA only, this is because I haven't gotten around to it
|
|
28
|
-
- **Testing & Types**: The codebase is very raw with limited types (I've opted for JavaScript only to avoid build problems). There aren't any tests either – I've been using the `playground` directory to manually test things as I go
|
|
29
|
-
|
|
30
|
-
## Quick Start
|
|
31
|
-
|
|
32
|
-
### Installation
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
pnpm i --save ripple
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
You'll also need Vite and Ripple's Vite plugin to compile Ripple:
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
pnpm i --save-dev vite-plugin-ripple
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
You can see a working example in the [playground demo app](https://github.com/trueadm/ripple/tree/main/playground).
|
|
45
|
-
|
|
46
|
-
### Mounting your app
|
|
47
|
-
|
|
48
|
-
You can use the `mount` API from the `ripple` package to render your Ripple component, using the `target`
|
|
49
|
-
option to specify what DOM element you want to render the component.
|
|
50
|
-
|
|
51
|
-
```ts
|
|
52
|
-
// index.ts
|
|
53
|
-
import { mount } from 'ripple';
|
|
54
|
-
import { App } from '/App.ripple';
|
|
55
|
-
|
|
56
|
-
mount(App, {
|
|
57
|
-
props: {
|
|
58
|
-
title: 'Hello world!'
|
|
59
|
-
},
|
|
60
|
-
target: document.getElementById('root')
|
|
61
|
-
});
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
## Key Concepts
|
|
65
|
-
|
|
66
|
-
### Components
|
|
67
|
-
|
|
68
|
-
Define reusable components with the `component` keyword. These are similar to functions in that they have `props`, but crucially,
|
|
69
|
-
they allow for a JSX-like syntax to be defined alongside standard TypeScript. That means you do not _return JSX_ like in other frameworks,
|
|
70
|
-
but you instead use it like a JavaScript statement, as shown:
|
|
71
|
-
|
|
72
|
-
```ripple
|
|
73
|
-
component Button(props: { text: string, onClick: () => void }) {
|
|
74
|
-
<button onClick={props.onClick}>
|
|
75
|
-
{props.text}
|
|
76
|
-
</button>
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Usage
|
|
80
|
-
<Button text="Click me" onClick={() => console.log("Clicked!")} />
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
### Reactive Variables
|
|
84
|
-
|
|
85
|
-
Variables prefixed with `$` are automatically reactive:
|
|
86
|
-
|
|
87
|
-
```ripple
|
|
88
|
-
let $name = "World";
|
|
89
|
-
let $count = 0;
|
|
90
|
-
|
|
91
|
-
// Updates automatically trigger re-renders
|
|
92
|
-
$count++;
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
Object properties prefixed with `$` are also automatically reactive:
|
|
96
|
-
|
|
97
|
-
```ripple
|
|
98
|
-
let counter = { $current: 0 };
|
|
99
|
-
|
|
100
|
-
// Updates automatically trigger re-renders
|
|
101
|
-
counter.$current++;
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
Derived values are simply `$` variables that combined different parts of state:
|
|
105
|
-
|
|
106
|
-
```ripple
|
|
107
|
-
let $count = 0;
|
|
108
|
-
let $double = $count * 2;
|
|
109
|
-
let $quadruple = $double * 2;
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
That means `$count` itself might be derived if it were to reference another reactive property. For example:
|
|
113
|
-
|
|
114
|
-
```ripple
|
|
115
|
-
component Counter({ $startingCount }) {
|
|
116
|
-
let $count = $startingCount;
|
|
117
|
-
let $double = $count * 2;
|
|
118
|
-
let $quadruple = $double * 2;
|
|
119
|
-
}
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
Now given `$startingCount` is reactive, it would mean that `$count` might reset each time an incoming change to `$startingCount` occurs. That might not be desirable, so Ripple provides a way to `untrack` reactivity in those cases:
|
|
123
|
-
|
|
124
|
-
```ripple
|
|
125
|
-
import { untrack } from 'ripple';
|
|
126
|
-
|
|
127
|
-
component Counter({ $startingCount }) {
|
|
128
|
-
let $count = untrack(() => $startingCount);
|
|
129
|
-
let $double = $count * 2;
|
|
130
|
-
let $quadruple = $double * 2;
|
|
131
|
-
}
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
Now `$count` will only reactively create its value on initialization.
|
|
135
|
-
|
|
136
|
-
> Note: you cannot define reactive variables in module/global scope, they have to be created on access from an active component
|
|
137
|
-
|
|
138
|
-
### Effects
|
|
139
|
-
|
|
140
|
-
When dealing with reactive state, you might want to be able to create side-effects based upon changes that happen upon updates.
|
|
141
|
-
To do this, you can use `effect`:
|
|
142
|
-
|
|
143
|
-
```ripple
|
|
144
|
-
import { effect } from 'ripple';
|
|
145
|
-
|
|
146
|
-
component App() {
|
|
147
|
-
let $count = 0;
|
|
148
|
-
|
|
149
|
-
effect(() => {
|
|
150
|
-
console.log($count);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
<button onClick={() => $count++}>Increment</button>
|
|
154
|
-
}
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
### Control flow
|
|
6
|
+
# What is Ripple?
|
|
158
7
|
|
|
159
|
-
|
|
160
|
-
can only occur _inside_ a `component` body – you can't create JSX inside functions, or assign it to variables as an expression.
|
|
161
|
-
|
|
162
|
-
```ripple
|
|
163
|
-
<div>
|
|
164
|
-
// you can create variables inside the template!
|
|
165
|
-
const str = "hello world";
|
|
166
|
-
|
|
167
|
-
console.log(str); // and function calls too!
|
|
168
|
-
|
|
169
|
-
debugger; // you can put breakpoints anywhere to help debugging!
|
|
170
|
-
|
|
171
|
-
{str}
|
|
172
|
-
</div>
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
Note that strings inside the template need to be inside `{"string"}`, you can't do `<div>hello</div>` as Ripple
|
|
176
|
-
has no idea if `hello` is a string or maybe some JavaScript code that needs evaluating, so just ensure you wrap them
|
|
177
|
-
in curly braces. This shouldn't be an issue in the real-world anyway, as you'll likely use an i18n library that means
|
|
178
|
-
using JavaScript expressions regardless.
|
|
179
|
-
|
|
180
|
-
### If statements
|
|
181
|
-
|
|
182
|
-
If blocks work seemlessly with Ripple's templating language, you can put them inside the JSX-like
|
|
183
|
-
statements, making control-flow far easier to read and reason with.
|
|
184
|
-
|
|
185
|
-
```ripple
|
|
186
|
-
component Truthy({ x }) {
|
|
187
|
-
<div>
|
|
188
|
-
if (x) {
|
|
189
|
-
<span>
|
|
190
|
-
{"x is truthy"}
|
|
191
|
-
</span>
|
|
192
|
-
} else {
|
|
193
|
-
<span>
|
|
194
|
-
{"x is truthy"}
|
|
195
|
-
</span>
|
|
196
|
-
}
|
|
197
|
-
</div>
|
|
198
|
-
}
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
### For statements
|
|
202
|
-
|
|
203
|
-
You can render collections using a `for...of` block, and you don't need to specify a `key` prop like
|
|
204
|
-
other frameworks.
|
|
205
|
-
|
|
206
|
-
```ripple
|
|
207
|
-
component ListView({ title, items }) {
|
|
208
|
-
<h2>{title}</h2>
|
|
209
|
-
<ul>
|
|
210
|
-
for (const item of items) {
|
|
211
|
-
<li>{item.text}</li>
|
|
212
|
-
}
|
|
213
|
-
</ul>
|
|
214
|
-
}
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
### Try statements
|
|
218
|
-
|
|
219
|
-
Try blocks work to building the foundation for **error boundaries**, when the runtime encounters
|
|
220
|
-
an error in the `try` block, you can easily render a fallback in the `catch` block.
|
|
221
|
-
|
|
222
|
-
```ripple
|
|
223
|
-
import { reportError } from 'some-library';
|
|
224
|
-
|
|
225
|
-
component ErrorBoundary() {
|
|
226
|
-
<div>
|
|
227
|
-
try {
|
|
228
|
-
<ComponentThatFails />
|
|
229
|
-
} catch (e) {
|
|
230
|
-
reportError(e);
|
|
231
|
-
|
|
232
|
-
<div>{"An error occured! " + e.message}</div>
|
|
233
|
-
}
|
|
234
|
-
</div>
|
|
235
|
-
}
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
### Props
|
|
239
|
-
|
|
240
|
-
If you want a prop to be reactive, you should also give it a `$` prefix.
|
|
241
|
-
|
|
242
|
-
```ripple
|
|
243
|
-
component Button(props: { $text: string, onClick: () => void }) {
|
|
244
|
-
<button onClick={props.onClick}>
|
|
245
|
-
{props.$text}
|
|
246
|
-
</button>
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Usage
|
|
250
|
-
<Button $text={some_text} onClick={() => console.log("Clicked!")} />
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
### Children
|
|
254
|
-
|
|
255
|
-
Use `$children` prop and the `<$component />` directive for component composition.
|
|
256
|
-
|
|
257
|
-
When you pass in children to a component, it gets implicitly passed as the `$children` prop, in the form of a component.
|
|
258
|
-
|
|
259
|
-
```ripple
|
|
260
|
-
import type { Component } from 'ripple';
|
|
261
|
-
|
|
262
|
-
component Card(props: { $children: Component }) {
|
|
263
|
-
<div class="card">
|
|
264
|
-
<$component />
|
|
265
|
-
</div>
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Usage
|
|
269
|
-
<Card>
|
|
270
|
-
<p>{"Card content here"}</p>
|
|
271
|
-
</Card>
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
You could also explicitly write the same code as shown:
|
|
275
|
-
|
|
276
|
-
```ripple
|
|
277
|
-
import type { Component } from 'ripple';
|
|
278
|
-
|
|
279
|
-
component Card(props: { $children: Component }) {
|
|
280
|
-
<div class="card">
|
|
281
|
-
<$component />
|
|
282
|
-
</div>
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Usage with explicit component
|
|
286
|
-
<Card>
|
|
287
|
-
component $children() {
|
|
288
|
-
<p>{"Card content here"}</p>
|
|
289
|
-
}
|
|
290
|
-
</Card>
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
### Events
|
|
294
|
-
|
|
295
|
-
Like React, events are props that start with `on` and then continue with an uppercase character, such as:
|
|
296
|
-
|
|
297
|
-
- `onClick`
|
|
298
|
-
- `onPointerMove`
|
|
299
|
-
- `onPointerDown`
|
|
300
|
-
- `onKeyDown`
|
|
301
|
-
|
|
302
|
-
For `capture` phase events, just add `Capture` to the end of the prop name:
|
|
303
|
-
|
|
304
|
-
- `onClickCapture`
|
|
305
|
-
- `onPointerMoveCapture`
|
|
306
|
-
- `onPointerDownCapture`
|
|
307
|
-
- `onKeyDownCapture`
|
|
308
|
-
|
|
309
|
-
> Note: Some events are automatically delegated where possible by Ripple to improve runtime performance.
|
|
310
|
-
|
|
311
|
-
### Styling
|
|
312
|
-
|
|
313
|
-
Ripple supports native CSS styling that is localized to the given component using the `<style>` element.
|
|
314
|
-
|
|
315
|
-
```ripple
|
|
316
|
-
component MyComponent() {
|
|
317
|
-
<div class="container">
|
|
318
|
-
<h1>{"Hello World"}</h1>
|
|
319
|
-
</div>
|
|
320
|
-
|
|
321
|
-
<style>
|
|
322
|
-
.container {
|
|
323
|
-
background: blue;
|
|
324
|
-
padding: 1rem;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
h1 {
|
|
328
|
-
color: white;
|
|
329
|
-
font-size: 2rem;
|
|
330
|
-
}
|
|
331
|
-
</style>
|
|
332
|
-
}
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
> Note: the `<style>` element must be top-level within a `component`.
|
|
336
|
-
|
|
337
|
-
## VSCode Extension
|
|
338
|
-
|
|
339
|
-
The Ripple VSCode extension provides:
|
|
340
|
-
|
|
341
|
-
- **Syntax Highlighting** for `.ripple` files
|
|
342
|
-
- **Real-time Diagnostics** for compilation errors
|
|
343
|
-
- **TypeScript Integration** for type checking
|
|
344
|
-
- **IntelliSense** for autocompletion
|
|
345
|
-
|
|
346
|
-
Clone the repository, and manually install the extension from the `packages/ripple-vscode-plugin/` directory.
|
|
347
|
-
|
|
348
|
-
## Playground
|
|
349
|
-
|
|
350
|
-
Feel free to play around with how Ripple works. If you clone the repo, you can then:
|
|
351
|
-
|
|
352
|
-
```bash
|
|
353
|
-
pnpm i && cd playground && pnpm dev
|
|
354
|
-
```
|
|
8
|
+
> Currently, this project is still in early development, and should not be used in production.
|
|
355
9
|
|
|
356
|
-
|
|
10
|
+
Ripple is a TypeScript UI framework for the web. To find out more, view [Ripple's Github README](https://github.com/trueadm/ripple).
|
package/package.json
CHANGED
|
@@ -9,8 +9,8 @@ function convert_from_jsx(node) {
|
|
|
9
9
|
node.type = 'Identifier';
|
|
10
10
|
} else if (node.type === 'JSXMemberExpression') {
|
|
11
11
|
node.type = 'MemberExpression';
|
|
12
|
-
node.object = convert_from_jsx(node.object)
|
|
13
|
-
node.property = convert_from_jsx(node.property)
|
|
12
|
+
node.object = convert_from_jsx(node.object);
|
|
13
|
+
node.property = convert_from_jsx(node.property);
|
|
14
14
|
}
|
|
15
15
|
return node;
|
|
16
16
|
}
|
|
@@ -24,6 +24,38 @@ function RipplePlugin(config) {
|
|
|
24
24
|
class RippleParser extends Parser {
|
|
25
25
|
#path = [];
|
|
26
26
|
|
|
27
|
+
parseExportDefaultDeclaration() {
|
|
28
|
+
// Check if this is "export default component"
|
|
29
|
+
if (this.value === 'component') {
|
|
30
|
+
const node = this.startNode();
|
|
31
|
+
node.type = 'Component';
|
|
32
|
+
node.css = null;
|
|
33
|
+
node.default = true;
|
|
34
|
+
this.next();
|
|
35
|
+
this.enterScope(0);
|
|
36
|
+
|
|
37
|
+
node.id = this.type.label === 'name' ? this.parseIdent() : null;
|
|
38
|
+
|
|
39
|
+
this.parseFunctionParams(node);
|
|
40
|
+
this.eat(tt.braceL);
|
|
41
|
+
node.body = [];
|
|
42
|
+
this.#path.push(node);
|
|
43
|
+
|
|
44
|
+
this.parseTemplateBody(node.body);
|
|
45
|
+
|
|
46
|
+
this.#path.pop();
|
|
47
|
+
this.exitScope();
|
|
48
|
+
|
|
49
|
+
this.next();
|
|
50
|
+
this.finishNode(node, 'Component');
|
|
51
|
+
this.awaitPos = 0;
|
|
52
|
+
|
|
53
|
+
return node;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return super.parseExportDefaultDeclaration();
|
|
57
|
+
}
|
|
58
|
+
|
|
27
59
|
shouldParseExportStatement() {
|
|
28
60
|
if (super.shouldParseExportStatement()) {
|
|
29
61
|
return true;
|
|
@@ -253,10 +285,7 @@ function RipplePlugin(config) {
|
|
|
253
285
|
case 62: // '>'
|
|
254
286
|
case 125: {
|
|
255
287
|
// '}'
|
|
256
|
-
if (
|
|
257
|
-
ch === 125 &&
|
|
258
|
-
(this.#path.at(-1).type === 'Component')
|
|
259
|
-
) {
|
|
288
|
+
if (ch === 125 && this.#path.at(-1).type === 'Component') {
|
|
260
289
|
return original.readToken.call(this, ch);
|
|
261
290
|
}
|
|
262
291
|
this.raise(
|
|
@@ -318,7 +347,7 @@ function RipplePlugin(config) {
|
|
|
318
347
|
if (open.name.type === 'JSXIdentifier') {
|
|
319
348
|
open.name.type = 'Identifier';
|
|
320
349
|
}
|
|
321
|
-
|
|
350
|
+
|
|
322
351
|
element.id = convert_from_jsx(open.name);
|
|
323
352
|
element.attributes = open.attributes;
|
|
324
353
|
element.selfClosing = open.selfClosing;
|
|
@@ -364,6 +393,37 @@ function RipplePlugin(config) {
|
|
|
364
393
|
return element;
|
|
365
394
|
}
|
|
366
395
|
|
|
396
|
+
parseSubscript(base, startPos, startLoc, noCalls, maybeAsyncArrow, optionalChained, forInit) {
|
|
397
|
+
if (this.value === '<' && this.#path.findLast((n) => n.type === 'Component')) {
|
|
398
|
+
// Check if this looks like JSX by looking ahead
|
|
399
|
+
const ahead = this.lookahead();
|
|
400
|
+
if (ahead.type.label === 'name' || ahead.value === '/' || ahead.value === '>') {
|
|
401
|
+
// This is JSX, rewind to the end of the object expression
|
|
402
|
+
// and let ASI handle the semicolon insertion naturally
|
|
403
|
+
this.pos = base.end;
|
|
404
|
+
this.type = tt.braceR;
|
|
405
|
+
this.value = '}';
|
|
406
|
+
this.start = base.end - 1;
|
|
407
|
+
this.end = base.end;
|
|
408
|
+
const position = this.curPosition();
|
|
409
|
+
this.startLoc = position;
|
|
410
|
+
this.endLoc = position;
|
|
411
|
+
this.next();
|
|
412
|
+
|
|
413
|
+
return base;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return super.parseSubscript(
|
|
417
|
+
base,
|
|
418
|
+
startPos,
|
|
419
|
+
startLoc,
|
|
420
|
+
noCalls,
|
|
421
|
+
maybeAsyncArrow,
|
|
422
|
+
optionalChained,
|
|
423
|
+
forInit
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
367
427
|
parseTemplateBody(body) {
|
|
368
428
|
var inside_func =
|
|
369
429
|
this.context.some((n) => n.token === 'function') || this.scopeStack.length > 1;
|
|
@@ -456,10 +516,7 @@ function RipplePlugin(config) {
|
|
|
456
516
|
parseBlock(createNewLexicalScope, node, exitStrict) {
|
|
457
517
|
const parent = this.#path.at(-1);
|
|
458
518
|
|
|
459
|
-
if (
|
|
460
|
-
parent?.type === 'Component' ||
|
|
461
|
-
parent?.type === 'Element'
|
|
462
|
-
) {
|
|
519
|
+
if (parent?.type === 'Component' || parent?.type === 'Element') {
|
|
463
520
|
if (createNewLexicalScope === void 0) createNewLexicalScope = true;
|
|
464
521
|
if (node === void 0) node = this.startNode();
|
|
465
522
|
|
|
@@ -71,7 +71,13 @@ function mark_as_tracked(path) {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
const visitors = {
|
|
74
|
-
_(node, { state, next }) {
|
|
74
|
+
_(node, { state, next, path }) {
|
|
75
|
+
// Set up metadata.path for each node (needed for CSS pruning)
|
|
76
|
+
if (!node.metadata) {
|
|
77
|
+
node.metadata = {};
|
|
78
|
+
}
|
|
79
|
+
node.metadata.path = [...path];
|
|
80
|
+
|
|
75
81
|
const scope = state.scopes.get(node);
|
|
76
82
|
next(scope !== undefined && scope !== state.scope ? { ...state, scope } : state);
|
|
77
83
|
},
|
|
@@ -439,20 +445,6 @@ const visitors = {
|
|
|
439
445
|
attribute
|
|
440
446
|
);
|
|
441
447
|
}
|
|
442
|
-
} else if (name === 'ref') {
|
|
443
|
-
if (is_dom_element) {
|
|
444
|
-
error(
|
|
445
|
-
'Cannot have a `ref` prop on an element, did you mean `$ref`?',
|
|
446
|
-
state.analysis.module.filename,
|
|
447
|
-
attribute
|
|
448
|
-
);
|
|
449
|
-
} else {
|
|
450
|
-
error(
|
|
451
|
-
'Cannot have a `ref` prop on a component, did you mean `$ref`?',
|
|
452
|
-
state.analysis.module.filename,
|
|
453
|
-
attribute
|
|
454
|
-
);
|
|
455
|
-
}
|
|
456
448
|
}
|
|
457
449
|
|
|
458
450
|
if (is_tracked_name(name)) {
|
|
@@ -2,6 +2,41 @@ import { walk } from 'zimmerframe';
|
|
|
2
2
|
|
|
3
3
|
const seen = new Set();
|
|
4
4
|
const regex_backslash_and_following_character = /\\(.)/g;
|
|
5
|
+
const FORWARD = 0;
|
|
6
|
+
const BACKWARD = 1;
|
|
7
|
+
|
|
8
|
+
// CSS selector constants
|
|
9
|
+
const descendant_combinator = { name: ' ', type: 'Combinator' };
|
|
10
|
+
const nesting_selector = {
|
|
11
|
+
type: 'NestingSelector',
|
|
12
|
+
name: '&',
|
|
13
|
+
selectors: [],
|
|
14
|
+
metadata: { scoped: false }
|
|
15
|
+
};
|
|
16
|
+
const any_selector = {
|
|
17
|
+
type: 'RelativeSelector',
|
|
18
|
+
selectors: [{ type: 'TypeSelector', name: '*' }],
|
|
19
|
+
combinator: null,
|
|
20
|
+
metadata: { scoped: false }
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Whitelist for attribute selectors on specific elements
|
|
24
|
+
const whitelist_attribute_selector = new Map([
|
|
25
|
+
['details', ['open']],
|
|
26
|
+
['dialog', ['open']],
|
|
27
|
+
['form', ['novalidate']],
|
|
28
|
+
['iframe', ['allow', 'allowfullscreen', 'allowpaymentrequest', 'loading', 'referrerpolicy']],
|
|
29
|
+
['img', ['loading']],
|
|
30
|
+
['input', ['accept', 'autocomplete', 'capture', 'checked', 'disabled', 'max', 'maxlength', 'min', 'minlength', 'multiple', 'pattern', 'placeholder', 'readonly', 'required', 'size', 'step']],
|
|
31
|
+
['object', ['typemustmatch']],
|
|
32
|
+
['ol', ['reversed', 'start', 'type']],
|
|
33
|
+
['optgroup', ['disabled']],
|
|
34
|
+
['option', ['disabled', 'selected']],
|
|
35
|
+
['script', ['async', 'defer', 'nomodule', 'type']],
|
|
36
|
+
['select', ['disabled', 'multiple', 'required', 'size']],
|
|
37
|
+
['textarea', ['autocomplete', 'disabled', 'maxlength', 'minlength', 'placeholder', 'readonly', 'required', 'rows', 'wrap']],
|
|
38
|
+
['video', ['autoplay', 'controls', 'loop', 'muted', 'playsinline']]
|
|
39
|
+
]);
|
|
5
40
|
|
|
6
41
|
function get_relative_selectors(node) {
|
|
7
42
|
const selectors = truncate(node);
|
|
@@ -62,14 +97,14 @@ function truncate(node) {
|
|
|
62
97
|
});
|
|
63
98
|
}
|
|
64
99
|
|
|
65
|
-
function apply_selector(relative_selectors, rule, element) {
|
|
66
|
-
const
|
|
67
|
-
const relative_selector =
|
|
100
|
+
function apply_selector(relative_selectors, rule, element, direction) {
|
|
101
|
+
const rest_selectors = relative_selectors.slice();
|
|
102
|
+
const relative_selector = direction === FORWARD ? rest_selectors.shift() : rest_selectors.pop();
|
|
68
103
|
|
|
69
104
|
const matched =
|
|
70
105
|
!!relative_selector &&
|
|
71
|
-
relative_selector_might_apply_to_node(relative_selector, rule, element) &&
|
|
72
|
-
apply_combinator(relative_selector,
|
|
106
|
+
relative_selector_might_apply_to_node(relative_selector, rule, element, direction) &&
|
|
107
|
+
apply_combinator(relative_selector, rest_selectors, rule, element, direction);
|
|
73
108
|
|
|
74
109
|
if (matched) {
|
|
75
110
|
if (!is_outer_global(relative_selector)) {
|
|
@@ -82,55 +117,163 @@ function apply_selector(relative_selectors, rule, element) {
|
|
|
82
117
|
return matched;
|
|
83
118
|
}
|
|
84
119
|
|
|
85
|
-
function
|
|
86
|
-
|
|
120
|
+
function get_ancestor_elements(node, adjacent_only, seen = new Set()) {
|
|
121
|
+
const ancestors = [];
|
|
87
122
|
|
|
88
|
-
const
|
|
123
|
+
const path = node.metadata.path;
|
|
124
|
+
let i = path.length;
|
|
89
125
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
case '>': {
|
|
93
|
-
let parent_matched = false;
|
|
126
|
+
while (i--) {
|
|
127
|
+
const parent = path[i];
|
|
94
128
|
|
|
95
|
-
|
|
96
|
-
|
|
129
|
+
if (parent.type === 'Element') {
|
|
130
|
+
ancestors.push(parent);
|
|
131
|
+
if (adjacent_only) {
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
97
136
|
|
|
98
|
-
|
|
99
|
-
|
|
137
|
+
return ancestors;
|
|
138
|
+
}
|
|
100
139
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
140
|
+
function get_descendant_elements(node, adjacent_only) {
|
|
141
|
+
const descendants = [];
|
|
142
|
+
|
|
143
|
+
function visit(current_node, depth = 0) {
|
|
144
|
+
if (current_node.type === 'Element' && current_node !== node) {
|
|
145
|
+
descendants.push(current_node);
|
|
146
|
+
if (adjacent_only) return; // Only direct children for '>' combinator
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Visit children based on Ripple's AST structure
|
|
150
|
+
if (current_node.children) {
|
|
151
|
+
for (const child of current_node.children) {
|
|
152
|
+
visit(child, depth + 1);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (current_node.body) {
|
|
157
|
+
for (const child of current_node.body) {
|
|
158
|
+
visit(child, depth + 1);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// For template nodes and text interpolations
|
|
163
|
+
if (current_node.expression && typeof current_node.expression === 'object') {
|
|
164
|
+
visit(current_node.expression, depth + 1);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Start from node's children
|
|
169
|
+
if (node.children) {
|
|
170
|
+
for (const child of node.children) {
|
|
171
|
+
visit(child);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (node.body) {
|
|
176
|
+
for (const child of node.body) {
|
|
177
|
+
visit(child);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
105
180
|
|
|
106
|
-
|
|
181
|
+
return descendants;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function get_possible_element_siblings(node, direction, adjacent_only) {
|
|
185
|
+
const siblings = new Map();
|
|
186
|
+
const parent = get_element_parent(node);
|
|
187
|
+
|
|
188
|
+
if (!parent) {
|
|
189
|
+
return siblings;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Get the container that holds the siblings
|
|
193
|
+
const container = parent.children || parent.body || [];
|
|
194
|
+
const node_index = container.indexOf(node);
|
|
195
|
+
|
|
196
|
+
if (node_index === -1) return siblings;
|
|
197
|
+
|
|
198
|
+
// Determine which siblings to check based on direction
|
|
199
|
+
let start, end, step;
|
|
200
|
+
if (direction === FORWARD) {
|
|
201
|
+
start = node_index + 1;
|
|
202
|
+
end = container.length;
|
|
203
|
+
step = 1;
|
|
204
|
+
} else {
|
|
205
|
+
start = node_index - 1;
|
|
206
|
+
end = -1;
|
|
207
|
+
step = -1;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Collect siblings
|
|
211
|
+
for (let i = start; i !== end; i += step) {
|
|
212
|
+
const sibling = container[i];
|
|
213
|
+
|
|
214
|
+
if (sibling.type === 'Element' || sibling.type === 'Component') {
|
|
215
|
+
siblings.set(sibling, true);
|
|
216
|
+
if (adjacent_only) break; // Only immediate sibling for '+' combinator
|
|
217
|
+
}
|
|
218
|
+
// Stop at non-whitespace text nodes for adjacent selectors
|
|
219
|
+
else if (adjacent_only && sibling.type === 'Text' && sibling.value?.trim()) {
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return siblings;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function apply_combinator(relative_selector, rest_selectors, rule, node, direction) {
|
|
228
|
+
const combinator =
|
|
229
|
+
direction == FORWARD ? rest_selectors[0]?.combinator : relative_selector.combinator;
|
|
230
|
+
if (!combinator) return true;
|
|
231
|
+
|
|
232
|
+
switch (combinator.name) {
|
|
233
|
+
case ' ':
|
|
234
|
+
case '>': {
|
|
235
|
+
const is_adjacent = combinator.name === '>';
|
|
236
|
+
const parents =
|
|
237
|
+
direction === FORWARD
|
|
238
|
+
? get_descendant_elements(node, is_adjacent)
|
|
239
|
+
: get_ancestor_elements(node, is_adjacent);
|
|
240
|
+
let parent_matched = false;
|
|
241
|
+
|
|
242
|
+
for (const parent of parents) {
|
|
243
|
+
if (apply_selector(rest_selectors, rule, parent, direction)) {
|
|
244
|
+
parent_matched = true;
|
|
107
245
|
}
|
|
108
246
|
}
|
|
109
247
|
|
|
110
|
-
return
|
|
248
|
+
return (
|
|
249
|
+
parent_matched ||
|
|
250
|
+
(direction === BACKWARD &&
|
|
251
|
+
(!is_adjacent || parents.length === 0) &&
|
|
252
|
+
rest_selectors.every((selector) => is_global(selector, rule)))
|
|
253
|
+
);
|
|
111
254
|
}
|
|
112
255
|
|
|
113
256
|
case '+':
|
|
114
257
|
case '~': {
|
|
115
|
-
const siblings = get_possible_element_siblings(node, name === '+');
|
|
258
|
+
const siblings = get_possible_element_siblings(node, direction, combinator.name === '+');
|
|
116
259
|
|
|
117
260
|
let sibling_matched = false;
|
|
118
261
|
|
|
119
262
|
for (const possible_sibling of siblings.keys()) {
|
|
120
|
-
if (possible_sibling.type === '
|
|
121
|
-
|
|
122
|
-
if (parent_selectors.length === 1 && parent_selectors[0].metadata.is_global) {
|
|
263
|
+
if (possible_sibling.type === 'Component') {
|
|
264
|
+
if (rest_selectors.length === 1 && rest_selectors[0].metadata.is_global) {
|
|
123
265
|
sibling_matched = true;
|
|
124
266
|
}
|
|
125
|
-
} else if (apply_selector(
|
|
267
|
+
} else if (apply_selector(rest_selectors, rule, possible_sibling, direction)) {
|
|
126
268
|
sibling_matched = true;
|
|
127
269
|
}
|
|
128
270
|
}
|
|
129
271
|
|
|
130
272
|
return (
|
|
131
273
|
sibling_matched ||
|
|
132
|
-
(
|
|
133
|
-
|
|
274
|
+
(direction === BACKWARD &&
|
|
275
|
+
get_element_parent(node) === null &&
|
|
276
|
+
rest_selectors.every((selector) => is_global(selector, rule)))
|
|
134
277
|
);
|
|
135
278
|
}
|
|
136
279
|
|
|
@@ -141,13 +284,18 @@ function apply_combinator(relative_selector, parent_selectors, rule, node) {
|
|
|
141
284
|
}
|
|
142
285
|
|
|
143
286
|
function get_element_parent(node) {
|
|
287
|
+
// Check if metadata and path exist
|
|
288
|
+
if (!node.metadata || !node.metadata.path) {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
|
|
144
292
|
let path = node.metadata.path;
|
|
145
293
|
let i = path.length;
|
|
146
294
|
|
|
147
295
|
while (i--) {
|
|
148
296
|
const parent = path[i];
|
|
149
297
|
|
|
150
|
-
if (parent.type === '
|
|
298
|
+
if (parent.type === 'Element') {
|
|
151
299
|
return parent;
|
|
152
300
|
}
|
|
153
301
|
}
|
|
@@ -252,7 +400,7 @@ function is_outer_global(relative_selector) {
|
|
|
252
400
|
);
|
|
253
401
|
}
|
|
254
402
|
|
|
255
|
-
function relative_selector_might_apply_to_node(relative_selector, rule, element) {
|
|
403
|
+
function relative_selector_might_apply_to_node(relative_selector, rule, element, direction) {
|
|
256
404
|
// Sort :has(...) selectors in one bucket and everything else into another
|
|
257
405
|
const has_selectors = [];
|
|
258
406
|
const other_selectors = [];
|
|
@@ -268,13 +416,6 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
|
|
|
268
416
|
// If we're called recursively from a :has(...) selector, we're on the way of checking if the other selectors match.
|
|
269
417
|
// In that case ignore this check (because we just came from this) to avoid an infinite loop.
|
|
270
418
|
if (has_selectors.length > 0) {
|
|
271
|
-
/** @type {Array<Compiler.AST.RegularElement | Compiler.AST.SvelteElement>} */
|
|
272
|
-
const child_elements = [];
|
|
273
|
-
/** @type {Array<Compiler.AST.RegularElement | Compiler.AST.SvelteElement>} */
|
|
274
|
-
const descendant_elements = [];
|
|
275
|
-
/** @type {Array<Compiler.AST.RegularElement | Compiler.AST.SvelteElement>} */
|
|
276
|
-
let sibling_elements; // do them lazy because it's rarely used and expensive to calculate
|
|
277
|
-
|
|
278
419
|
// If this is a :has inside a global selector, we gotta include the element itself, too,
|
|
279
420
|
// because the global selector might be for an element that's outside the component,
|
|
280
421
|
// e.g. :root:has(.scoped), :global(.foo):has(.scoped), or :root { &:has(.scoped) {} }
|
|
@@ -290,37 +431,6 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
|
|
|
290
431
|
)
|
|
291
432
|
)
|
|
292
433
|
);
|
|
293
|
-
if (include_self) {
|
|
294
|
-
child_elements.push(element);
|
|
295
|
-
descendant_elements.push(element);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* @param {Compiler.AST.SvelteNode} node
|
|
300
|
-
* @param {{ is_child: boolean }} state
|
|
301
|
-
*/
|
|
302
|
-
function walk_children(node, state) {
|
|
303
|
-
walk(node, state, {
|
|
304
|
-
_(node, context) {
|
|
305
|
-
if (node.type === 'Element') {
|
|
306
|
-
descendant_elements.push(node);
|
|
307
|
-
|
|
308
|
-
if (context.state.is_child) {
|
|
309
|
-
child_elements.push(node);
|
|
310
|
-
context.state.is_child = false;
|
|
311
|
-
context.next();
|
|
312
|
-
context.state.is_child = true;
|
|
313
|
-
} else {
|
|
314
|
-
context.next();
|
|
315
|
-
}
|
|
316
|
-
} else {
|
|
317
|
-
context.next();
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
walk_children(element.fragment, { is_child: true });
|
|
324
434
|
|
|
325
435
|
// :has(...) is special in that it means "look downwards in the CSS tree". Since our matching algorithm goes
|
|
326
436
|
// upwards and back-to-front, we need to first check the selectors inside :has(...), then check the rest of the
|
|
@@ -331,37 +441,34 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
|
|
|
331
441
|
let matched = false;
|
|
332
442
|
|
|
333
443
|
for (const complex_selector of complex_selectors) {
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
...selectors[0],
|
|
341
|
-
combinator: null
|
|
342
|
-
};
|
|
444
|
+
const [first, ...rest] = truncate(complex_selector);
|
|
445
|
+
// if it was just a :global(...)
|
|
446
|
+
if (!first) {
|
|
447
|
+
complex_selector.metadata.used = true;
|
|
448
|
+
matched = true;
|
|
449
|
+
continue;
|
|
343
450
|
}
|
|
344
451
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
?
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
let selector_matched = false;
|
|
353
|
-
|
|
354
|
-
// Iterate over all descendant elements and check if the selector inside :has matches
|
|
355
|
-
for (const element of descendants) {
|
|
356
|
-
if (
|
|
357
|
-
selectors.length === 0 /* is :global(...) */ ||
|
|
358
|
-
(element.metadata.scoped && selector_matched) ||
|
|
359
|
-
apply_selector(selectors, rule, element)
|
|
360
|
-
) {
|
|
452
|
+
if (include_self) {
|
|
453
|
+
const selector_including_self = [
|
|
454
|
+
first.combinator ? { ...first, combinator: null } : first,
|
|
455
|
+
...rest
|
|
456
|
+
];
|
|
457
|
+
if (apply_selector(selector_including_self, rule, element, FORWARD)) {
|
|
361
458
|
complex_selector.metadata.used = true;
|
|
362
|
-
|
|
459
|
+
matched = true;
|
|
363
460
|
}
|
|
364
461
|
}
|
|
462
|
+
|
|
463
|
+
const selector_excluding_self = [
|
|
464
|
+
any_selector,
|
|
465
|
+
first.combinator ? first : { ...first, combinator: descendant_combinator },
|
|
466
|
+
...rest
|
|
467
|
+
];
|
|
468
|
+
if (apply_selector(selector_excluding_self, rule, element, FORWARD)) {
|
|
469
|
+
complex_selector.metadata.used = true;
|
|
470
|
+
matched = true;
|
|
471
|
+
}
|
|
365
472
|
}
|
|
366
473
|
|
|
367
474
|
if (!matched) {
|
|
@@ -386,7 +493,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
|
|
|
386
493
|
) {
|
|
387
494
|
const args = selector.args;
|
|
388
495
|
const complex_selector = args.children[0];
|
|
389
|
-
return apply_selector(complex_selector.children, rule, element);
|
|
496
|
+
return apply_selector(complex_selector.children, rule, element, BACKWARD);
|
|
390
497
|
}
|
|
391
498
|
|
|
392
499
|
// We came across a :global, everything beyond it is global and therefore a potential match
|
|
@@ -413,7 +520,6 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
|
|
|
413
520
|
selector.metadata.scoped = true;
|
|
414
521
|
}
|
|
415
522
|
|
|
416
|
-
/** @type {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | null} */
|
|
417
523
|
let el = element;
|
|
418
524
|
while (el) {
|
|
419
525
|
el.metadata.scoped = true;
|
|
@@ -435,7 +541,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
|
|
|
435
541
|
if (is_global) {
|
|
436
542
|
complex_selector.metadata.used = true;
|
|
437
543
|
matched = true;
|
|
438
|
-
} else if (apply_selector(relative, rule, element)) {
|
|
544
|
+
} else if (apply_selector(relative, rule, element, BACKWARD)) {
|
|
439
545
|
complex_selector.metadata.used = true;
|
|
440
546
|
matched = true;
|
|
441
547
|
} else if (complex_selector.children.length > 1 && (name == 'is' || name == 'where')) {
|
|
@@ -465,7 +571,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
|
|
|
465
571
|
case 'AttributeSelector': {
|
|
466
572
|
const whitelisted = whitelist_attribute_selector.get(element.id.name.toLowerCase());
|
|
467
573
|
if (
|
|
468
|
-
!whitelisted?.includes(selector.
|
|
574
|
+
!whitelisted?.includes(selector.name.toLowerCase()) &&
|
|
469
575
|
!attribute_matches(
|
|
470
576
|
element,
|
|
471
577
|
selector.name,
|
|
@@ -480,12 +586,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
|
|
|
480
586
|
}
|
|
481
587
|
|
|
482
588
|
case 'ClassSelector': {
|
|
483
|
-
if (
|
|
484
|
-
!attribute_matches(element, 'class', name, '~=', false) &&
|
|
485
|
-
!element.attributes.some(
|
|
486
|
-
(attribute) => attribute.type === 'ClassDirective' && attribute.name === name
|
|
487
|
-
)
|
|
488
|
-
) {
|
|
589
|
+
if (!attribute_matches(element, 'class', name, '~=', false)) {
|
|
489
590
|
return false;
|
|
490
591
|
}
|
|
491
592
|
|
|
@@ -502,9 +603,9 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
|
|
|
502
603
|
|
|
503
604
|
case 'TypeSelector': {
|
|
504
605
|
if (
|
|
606
|
+
element.id.type === 'Identifier' &&
|
|
505
607
|
element.id.name.toLowerCase() !== name.toLowerCase() &&
|
|
506
|
-
name !== '*'
|
|
507
|
-
element.id.name[0].toLowerCase() === element.id.name[0]
|
|
608
|
+
name !== '*'
|
|
508
609
|
) {
|
|
509
610
|
return false;
|
|
510
611
|
}
|
|
@@ -519,7 +620,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
|
|
|
519
620
|
|
|
520
621
|
for (const complex_selector of parent.prelude.children) {
|
|
521
622
|
if (
|
|
522
|
-
apply_selector(get_relative_selectors(complex_selector), parent, element) ||
|
|
623
|
+
apply_selector(get_relative_selectors(complex_selector), parent, element, direction) ||
|
|
523
624
|
complex_selector.children.every((s) => is_global(s, parent))
|
|
524
625
|
) {
|
|
525
626
|
complex_selector.metadata.used = true;
|
|
@@ -540,6 +641,27 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
|
|
|
540
641
|
return true;
|
|
541
642
|
}
|
|
542
643
|
|
|
644
|
+
// Utility functions for parsing CSS values
|
|
645
|
+
function unquote(str) {
|
|
646
|
+
if ((str[0] === '"' && str[str.length - 1] === '"') ||
|
|
647
|
+
(str[0] === "'" && str[str.length - 1] === "'")) {
|
|
648
|
+
return str.slice(1, -1);
|
|
649
|
+
}
|
|
650
|
+
return str;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function get_parent_rules(rule) {
|
|
654
|
+
const rules = [rule];
|
|
655
|
+
let current = rule;
|
|
656
|
+
|
|
657
|
+
while (current.metadata.parent_rule) {
|
|
658
|
+
current = current.metadata.parent_rule;
|
|
659
|
+
rules.unshift(current);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return rules;
|
|
663
|
+
}
|
|
664
|
+
|
|
543
665
|
export function prune_css(css, element) {
|
|
544
666
|
walk(css, null, {
|
|
545
667
|
Rule(node, context) {
|
|
@@ -558,7 +680,8 @@ export function prune_css(css, element) {
|
|
|
558
680
|
apply_selector(
|
|
559
681
|
selectors,
|
|
560
682
|
/** @type {Compiler.AST.CSS.Rule} */ (node.metadata.rule),
|
|
561
|
-
element
|
|
683
|
+
element,
|
|
684
|
+
BACKWARD
|
|
562
685
|
)
|
|
563
686
|
) {
|
|
564
687
|
node.metadata.used = true;
|
|
@@ -32,6 +32,12 @@ function visit_function(node, context) {
|
|
|
32
32
|
const metadata = node.metadata;
|
|
33
33
|
const state = context.state;
|
|
34
34
|
|
|
35
|
+
delete node.returnType;
|
|
36
|
+
|
|
37
|
+
for (const param of node.params) {
|
|
38
|
+
delete param.typeAnnotation;
|
|
39
|
+
}
|
|
40
|
+
|
|
35
41
|
if (metadata?.hoisted === true) {
|
|
36
42
|
const params = build_hoisted_params(node, context);
|
|
37
43
|
|
|
@@ -44,7 +50,7 @@ function visit_function(node, context) {
|
|
|
44
50
|
|
|
45
51
|
let body = context.visit(node.body, state);
|
|
46
52
|
|
|
47
|
-
if (metadata
|
|
53
|
+
if (metadata?.tracked === true) {
|
|
48
54
|
return /** @type {FunctionExpression} */ ({
|
|
49
55
|
...node,
|
|
50
56
|
params: node.params.map((param) => context.visit(param, state)),
|
|
@@ -66,7 +72,7 @@ function build_getter(node, context) {
|
|
|
66
72
|
|
|
67
73
|
// don't transform the declaration itself
|
|
68
74
|
if (node !== binding?.node && binding?.transform) {
|
|
69
|
-
return binding.transform.read(node);
|
|
75
|
+
return binding.transform.read(node, context.state?.metadata?.spread);
|
|
70
76
|
}
|
|
71
77
|
}
|
|
72
78
|
|
|
@@ -409,18 +415,12 @@ const visitors = {
|
|
|
409
415
|
continue;
|
|
410
416
|
}
|
|
411
417
|
|
|
412
|
-
if (name === '$ref') {
|
|
413
|
-
const id = state.flush_node();
|
|
414
|
-
const value = attr.value;
|
|
415
|
-
|
|
416
|
-
state.init.push(b.stmt(b.call('$.set_ref', id, visit(value, state))));
|
|
417
|
-
continue;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
418
|
if (is_event_attribute(name)) {
|
|
421
|
-
|
|
419
|
+
let capture = name.endsWith('Capture');
|
|
420
|
+
let event_name = capture
|
|
421
|
+
? name.slice(2, -7).toLowerCase()
|
|
422
|
+
: name.slice(2).toLowerCase();
|
|
422
423
|
let handler = visit(attr.value, state);
|
|
423
|
-
let capture = false; // TODO
|
|
424
424
|
|
|
425
425
|
if (attr.metadata?.delegated) {
|
|
426
426
|
let delegated_assignment;
|
|
@@ -552,8 +552,9 @@ const visitors = {
|
|
|
552
552
|
|
|
553
553
|
state.template.push('<!>');
|
|
554
554
|
|
|
555
|
+
const is_spreading = node.attributes.some((attr) => attr.type === 'SpreadAttribute');
|
|
555
556
|
const tracked = [];
|
|
556
|
-
|
|
557
|
+
const props = [];
|
|
557
558
|
let children_prop = null;
|
|
558
559
|
|
|
559
560
|
for (const attr of node.attributes) {
|
|
@@ -577,6 +578,15 @@ const visitors = {
|
|
|
577
578
|
} else {
|
|
578
579
|
props.push(b.prop('init', attr.name, visit(attr.value, state)));
|
|
579
580
|
}
|
|
581
|
+
} else if (attr.type === 'SpreadAttribute') {
|
|
582
|
+
props.push(
|
|
583
|
+
b.spread(
|
|
584
|
+
b.call(
|
|
585
|
+
'$.spread_object',
|
|
586
|
+
visit(attr.argument, { ...state, metadata: { ...state.metadata, spread: true } })
|
|
587
|
+
)
|
|
588
|
+
)
|
|
589
|
+
);
|
|
580
590
|
} else {
|
|
581
591
|
throw new Error('TODO');
|
|
582
592
|
}
|
|
@@ -600,7 +610,18 @@ const visitors = {
|
|
|
600
610
|
}
|
|
601
611
|
}
|
|
602
612
|
|
|
603
|
-
if (
|
|
613
|
+
if (is_spreading) {
|
|
614
|
+
state.init.push(
|
|
615
|
+
b.stmt(
|
|
616
|
+
b.call(
|
|
617
|
+
node.id,
|
|
618
|
+
id,
|
|
619
|
+
b.call('$.tracked_spread_object', b.thunk(b.object(props))),
|
|
620
|
+
b.id('$.active_block')
|
|
621
|
+
)
|
|
622
|
+
)
|
|
623
|
+
);
|
|
624
|
+
} else if (tracked.length > 0) {
|
|
604
625
|
state.init.push(
|
|
605
626
|
b.stmt(
|
|
606
627
|
b.call(
|
|
@@ -7,7 +7,6 @@ export {
|
|
|
7
7
|
set_value,
|
|
8
8
|
set_checked,
|
|
9
9
|
set_selected,
|
|
10
|
-
set_ref
|
|
11
10
|
} from './render.js';
|
|
12
11
|
|
|
13
12
|
export { render, render_spread, async } from './blocks.js';
|
|
@@ -25,6 +24,7 @@ export {
|
|
|
25
24
|
async_computed,
|
|
26
25
|
tracked,
|
|
27
26
|
tracked_object,
|
|
27
|
+
tracked_spread_object,
|
|
28
28
|
computed_property,
|
|
29
29
|
get_property,
|
|
30
30
|
set_property,
|
|
@@ -157,16 +157,6 @@ export function set_selected(element, selected) {
|
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
export function set_ref(dom, fn) {
|
|
161
|
-
effect(() => {
|
|
162
|
-
fn(dom);
|
|
163
|
-
|
|
164
|
-
return () => {
|
|
165
|
-
fn(null);
|
|
166
|
-
};
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
|
|
170
160
|
export function apply_element_spread(element, fn) {
|
|
171
161
|
return () => {
|
|
172
162
|
set_attributes(element, fn());
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
EFFECT_BLOCK,
|
|
19
19
|
PAUSED,
|
|
20
20
|
ROOT_BLOCK,
|
|
21
|
+
SPREAD_OBJECT,
|
|
21
22
|
TRACKED,
|
|
22
23
|
TRACKED_OBJECT,
|
|
23
24
|
TRY_BLOCK,
|
|
@@ -217,9 +218,9 @@ export function tracked(v, b) {
|
|
|
217
218
|
};
|
|
218
219
|
}
|
|
219
220
|
|
|
220
|
-
export function computed(fn,
|
|
221
|
+
export function computed(fn, block) {
|
|
221
222
|
return {
|
|
222
|
-
b,
|
|
223
|
+
b: block,
|
|
223
224
|
blocks: null,
|
|
224
225
|
c: 0,
|
|
225
226
|
d: null,
|
|
@@ -644,7 +645,7 @@ export function flush_sync(fn) {
|
|
|
644
645
|
var previous_queued_root_blocks = queued_root_blocks;
|
|
645
646
|
|
|
646
647
|
try {
|
|
647
|
-
|
|
648
|
+
var root_blocks = [];
|
|
648
649
|
|
|
649
650
|
scheduler_mode = FLUSH_SYNC;
|
|
650
651
|
queued_root_blocks = root_blocks;
|
|
@@ -667,6 +668,17 @@ export function flush_sync(fn) {
|
|
|
667
668
|
}
|
|
668
669
|
}
|
|
669
670
|
|
|
671
|
+
export function tracked_spread_object(fn) {
|
|
672
|
+
var obj = fn();
|
|
673
|
+
|
|
674
|
+
define_property(obj, SPREAD_OBJECT, {
|
|
675
|
+
value: fn,
|
|
676
|
+
enumerable: false
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
return obj;
|
|
680
|
+
}
|
|
681
|
+
|
|
670
682
|
export function tracked_object(obj, properties, block) {
|
|
671
683
|
var tracked_properties = obj[TRACKED_OBJECT];
|
|
672
684
|
|
|
@@ -719,6 +731,10 @@ export function get_property(obj, property, chain = false) {
|
|
|
719
731
|
|
|
720
732
|
if (tracked_property !== undefined) {
|
|
721
733
|
value = obj[property] = get(tracked_property);
|
|
734
|
+
} else if (SPREAD_OBJECT in obj) {
|
|
735
|
+
var spread_fn = obj[SPREAD_OBJECT];
|
|
736
|
+
var properties = spread_fn();
|
|
737
|
+
return get_property(properties, property, chain);
|
|
722
738
|
}
|
|
723
739
|
|
|
724
740
|
return value;
|
|
@@ -851,11 +867,11 @@ export function spread_object(obj) {
|
|
|
851
867
|
return { ...obj };
|
|
852
868
|
}
|
|
853
869
|
var keys = original_object_keys(obj);
|
|
854
|
-
|
|
870
|
+
var values = {};
|
|
855
871
|
|
|
856
872
|
for (var i = 0; i < keys.length; i++) {
|
|
857
873
|
var key = keys[i];
|
|
858
|
-
values[key] =
|
|
874
|
+
values[key] = get_property(obj, key);
|
|
859
875
|
}
|
|
860
876
|
|
|
861
877
|
return values;
|
|
@@ -109,13 +109,13 @@ export function capture() {
|
|
|
109
109
|
var previous_tracking = tracking;
|
|
110
110
|
var previous_block = active_block;
|
|
111
111
|
var previous_reaction = active_reaction;
|
|
112
|
-
|
|
112
|
+
var previous_component = active_component;
|
|
113
113
|
|
|
114
114
|
return () => {
|
|
115
115
|
set_tracking(previous_tracking);
|
|
116
116
|
set_active_block(previous_block);
|
|
117
117
|
set_active_reaction(previous_reaction);
|
|
118
|
-
|
|
118
|
+
set_active_component(previous_component);
|
|
119
119
|
|
|
120
120
|
queue_microtask(exit);
|
|
121
121
|
};
|
package/types/index.d.ts
CHANGED