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 CHANGED
@@ -1,356 +1,10 @@
1
- # Ripple
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
- > Currently, this project is still in early development, and should not be used in production.
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
- The JSX-like syntax might take some time to get used to if you're coming from another framework. For one, templating in Ripple
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
- The playground uses Ripple's Vite plugin, where you can play around with things inside the `playground/src` directory.
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
@@ -3,7 +3,7 @@
3
3
  "description": "Ripple is a TypeScript UI framework for the web",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.2.3",
6
+ "version": "0.2.5",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index.js",
9
9
  "main": "src/runtime/index.js",
@@ -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 parent_selectors = relative_selectors.slice();
67
- const relative_selector = parent_selectors.pop();
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, parent_selectors, rule, element);
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 apply_combinator(relative_selector, parent_selectors, rule, node) {
86
- if (!relative_selector.combinator) return true;
120
+ function get_ancestor_elements(node, adjacent_only, seen = new Set()) {
121
+ const ancestors = [];
87
122
 
88
- const name = relative_selector.combinator.name;
123
+ const path = node.metadata.path;
124
+ let i = path.length;
89
125
 
90
- switch (name) {
91
- case ' ':
92
- case '>': {
93
- let parent_matched = false;
126
+ while (i--) {
127
+ const parent = path[i];
94
128
 
95
- const path = node.metadata.path;
96
- let i = path.length;
129
+ if (parent.type === 'Element') {
130
+ ancestors.push(parent);
131
+ if (adjacent_only) {
132
+ break;
133
+ }
134
+ }
135
+ }
97
136
 
98
- while (i--) {
99
- const parent = path[i];
137
+ return ancestors;
138
+ }
100
139
 
101
- if (parent.type === 'Element') {
102
- if (apply_selector(parent_selectors, rule, parent)) {
103
- parent_matched = true;
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
- if (name === '>') return parent_matched;
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 parent_matched || parent_selectors.every((selector) => is_global(selector, rule));
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 === 'RenderTag' || possible_sibling.type === 'SlotElement') {
121
- // `{@render foo()}<p>foo</p>` with `:global(.x) + p` is a match
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(parent_selectors, rule, possible_sibling)) {
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
- (get_element_parent(node) === null &&
133
- parent_selectors.every((selector) => is_global(selector, rule)))
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 === 'RegularElement' || parent.type === 'SvelteElement') {
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 selectors = truncate(complex_selector);
335
- const left_most_combinator = selectors[0]?.combinator ?? descendant_combinator;
336
- // In .x:has(> y), we want to search for y, ignoring the left-most combinator
337
- // (else it would try to walk further up and fail because there are no selectors left)
338
- if (selectors.length > 0) {
339
- selectors[0] = {
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
- const descendants =
346
- left_most_combinator.name === '+' || left_most_combinator.name === '~'
347
- ? (sibling_elements ??= get_following_sibling_elements(element, include_self))
348
- : left_most_combinator.name === '>'
349
- ? child_elements
350
- : descendant_elements;
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
- selector_matched = matched = true;
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.id.name.toLowerCase()) &&
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.tracked === true) {
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
- const event_name = name.slice(2).toLowerCase();
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
- let props = [];
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 (tracked.length > 0) {
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(
@@ -20,4 +20,5 @@ export var LOGIC_BLOCK = FOR_BLOCK | IF_BLOCK | TRY_BLOCK;
20
20
 
21
21
  export var UNINITIALIZED = Symbol();
22
22
  export var TRACKED_OBJECT = Symbol();
23
+ export var SPREAD_OBJECT = Symbol();
23
24
  export var COMPUTED_PROPERTY = Symbol();
@@ -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, b) {
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
- const root_blocks = [];
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
- const values = {};
870
+ var values = {};
855
871
 
856
872
  for (var i = 0; i < keys.length; i++) {
857
873
  var key = keys[i];
858
- values[key] = get_property_computed(obj, 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
- var previous_component = active_component;
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
- set_active_component(previous_component);
118
+ set_active_component(previous_component);
119
119
 
120
120
  queue_microtask(exit);
121
121
  };
package/types/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type Component<T> = (props: T) => void;
1
+ export type Component<T = Record<string, any>> = (props: T) => void;
2
2
 
3
3
  export declare function mount(
4
4
  component: () => void,