ripple 0.2.2 → 0.2.4

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 ADDED
@@ -0,0 +1,387 @@
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>
5
+
6
+ # What is Ripple?
7
+
8
+ > Currently, this project is still in early development, and should not be used in production.
9
+
10
+ Ripple is a TypeScript UI framework for the web.
11
+
12
+ 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.
13
+
14
+ 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.
15
+
16
+ Ripple was designed to be a JS/TS-first framework, rather than HTML-first. Ripple modules have their own `.ripple` extension and these modules
17
+ fully support TypeScript. By introducing a new extension, it affords Ripple to invent its own superset language, that plays really nicely with
18
+ 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.
19
+
20
+ 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.
21
+
22
+ If you'd like to know more, join the [Ripple Discord](https://discord.gg/JBF2ySrh2W).
23
+
24
+ ## Features
25
+
26
+ - **Reactive State Management**: Built-in reactivity with `$` prefixed variables
27
+ - **Component-Based Architecture**: Clean, reusable components with props and children
28
+ - **JSX-like Syntax**: Familiar templating with Ripple-specific enhancements
29
+ - **TypeScript Support**: Full TypeScript integration with type checking
30
+ - **VSCode Integration**: Rich editor support with diagnostics, syntax highlighting, and IntelliSense
31
+
32
+ ## Missing Features
33
+
34
+ - **SSR**: Ripple is currently an SPA only, this is because I haven't gotten around to it
35
+ - **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
36
+
37
+ ## Quick Start
38
+
39
+ ### Try Ripple
40
+
41
+ > We're working hard on getting an online playgroud available. Watch this space!
42
+
43
+ You can try Ripple now by using our basic Vite template by running these commands in your terminal:
44
+
45
+ ```bash
46
+ npx degit trueadm/ripple/templates/basic my-app
47
+ cd my-app
48
+ npm i # or yarn or pnpm
49
+ npm run dev # or yarn or pnpm
50
+ ```
51
+
52
+ ### Mounting your app
53
+
54
+ You can use the `mount` API from the `ripple` package to render your Ripple component, using the `target`
55
+ option to specify what DOM element you want to render the component.
56
+
57
+ ```ts
58
+ // index.ts
59
+ import { mount } from 'ripple';
60
+ import { App } from '/App.ripple';
61
+
62
+ mount(App, {
63
+ props: {
64
+ title: 'Hello world!'
65
+ },
66
+ target: document.getElementById('root')
67
+ });
68
+ ```
69
+
70
+ ## Key Concepts
71
+
72
+ ### Components
73
+
74
+ Define reusable components with the `component` keyword. These are similar to functions in that they have `props`, but crucially,
75
+ they allow for a JSX-like syntax to be defined alongside standard TypeScript. That means you do not _return JSX_ like in other frameworks,
76
+ but you instead use it like a JavaScript statement, as shown:
77
+
78
+ ```ripple
79
+ component Button(props: { text: string, onClick: () => void }) {
80
+ <button onClick={props.onClick}>
81
+ {props.text}
82
+ </button>
83
+ }
84
+
85
+ // Usage
86
+ component App() {
87
+ <Button text="Click me" onClick={() => console.log("Clicked!")} />
88
+ }
89
+ ```
90
+
91
+ ![iamge for the sourcecode above](assets/readme-1.png)
92
+
93
+ Ripple's templating language also supports shorthands and object spreads too:
94
+
95
+ ```svelte
96
+ // you can do a normal prop
97
+ <div onClick={onClick}>{text}</div>
98
+
99
+ // or using the shorthand prop
100
+ <div {onClick}>{text}</div>
101
+
102
+ // and you can spread props
103
+ <div {...properties}>{text}</div>
104
+ ```
105
+
106
+ ### Reactive Variables
107
+
108
+ Variables prefixed with `$` are automatically reactive:
109
+
110
+ ```ts
111
+ let $name = "World";
112
+ let $count = 0;
113
+
114
+ // Updates automatically trigger re-renders
115
+ $count++;
116
+ ```
117
+
118
+ Object properties prefixed with `$` are also automatically reactive:
119
+
120
+ ```ts
121
+ let counter = { $current: 0 };
122
+
123
+ // Updates automatically trigger re-renders
124
+ counter.$current++;
125
+ ```
126
+
127
+ Derived values are simply `$` variables that combined different parts of state:
128
+
129
+ ```ts
130
+ let $count = 0;
131
+ let $double = $count * 2;
132
+ let $quadruple = $double * 2;
133
+ ```
134
+
135
+ That means `$count` itself might be derived if it were to reference another reactive property. For example:
136
+
137
+ ```ripple
138
+ component Counter({ $startingCount }) {
139
+ let $count = $startingCount;
140
+ let $double = $count * 2;
141
+ let $quadruple = $double * 2;
142
+ }
143
+ ```
144
+
145
+ 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:
146
+
147
+ ```ripple
148
+ import { untrack } from 'ripple';
149
+
150
+ component Counter({ $startingCount }) {
151
+ let $count = untrack(() => $startingCount);
152
+ let $double = $count * 2;
153
+ let $quadruple = $double * 2;
154
+ }
155
+ ```
156
+
157
+ Now `$count` will only reactively create its value on initialization.
158
+
159
+ > Note: you cannot define reactive variables in module/global scope, they have to be created on access from an active component
160
+
161
+ ### Effects
162
+
163
+ When dealing with reactive state, you might want to be able to create side-effects based upon changes that happen upon updates.
164
+ To do this, you can use `effect`:
165
+
166
+ ```ripple
167
+ import { effect } from 'ripple';
168
+
169
+ component App() {
170
+ let $count = 0;
171
+
172
+ effect(() => {
173
+ console.log($count);
174
+ });
175
+
176
+ <button onClick={() => $count++}>Increment</button>
177
+ }
178
+ ```
179
+
180
+ ### Control flow
181
+
182
+ The JSX-like syntax might take some time to get used to if you're coming from another framework. For one, templating in Ripple
183
+ can only occur _inside_ a `component` body – you can't create JSX inside functions, or assign it to variables as an expression.
184
+
185
+ ```ripple
186
+ <div>
187
+ // you can create variables inside the template!
188
+ const str = "hello world";
189
+
190
+ console.log(str); // and function calls too!
191
+
192
+ debugger; // you can put breakpoints anywhere to help debugging!
193
+
194
+ {str}
195
+ </div>
196
+ ```
197
+
198
+ ![iamge for the sourcecode above](assets/readme-5.png)
199
+
200
+ Note that strings inside the template need to be inside `{"string"}`, you can't do `<div>hello</div>` as Ripple
201
+ has no idea if `hello` is a string or maybe some JavaScript code that needs evaluating, so just ensure you wrap them
202
+ in curly braces. This shouldn't be an issue in the real-world anyway, as you'll likely use an i18n library that means
203
+ using JavaScript expressions regardless.
204
+
205
+ ### If statements
206
+
207
+ If blocks work seemlessly with Ripple's templating language, you can put them inside the JSX-like
208
+ statements, making control-flow far easier to read and reason with.
209
+
210
+ ```ripple
211
+ component Truthy({ x }) {
212
+ <div>
213
+ if (x) {
214
+ <span>
215
+ {"x is truthy"}
216
+ </span>
217
+ } else {
218
+ <span>
219
+ {"x is falsy"}
220
+ </span>
221
+ }
222
+ </div>
223
+ }
224
+ ```
225
+
226
+ ![iamge for the sourcecode above](assets/readme-2.png)
227
+
228
+ ### For statements
229
+
230
+ You can render collections using a `for...of` block, and you don't need to specify a `key` prop like
231
+ other frameworks.
232
+
233
+ ```ripple
234
+ component ListView({ title, items }) {
235
+ <h2>{title}</h2>
236
+ <ul>
237
+ for (const item of items) {
238
+ <li>{item.text}</li>
239
+ }
240
+ </ul>
241
+ }
242
+ ```
243
+
244
+ ![iamge for the sourcecode above](assets/readme-3.png)
245
+
246
+ ### Try statements
247
+
248
+ Try blocks work to building the foundation for **error boundaries**, when the runtime encounters
249
+ an error in the `try` block, you can easily render a fallback in the `catch` block.
250
+
251
+ ```ripple
252
+ import { reportError } from 'some-library';
253
+
254
+ component ErrorBoundary() {
255
+ <div>
256
+ try {
257
+ <ComponentThatFails />
258
+ } catch (e) {
259
+ reportError(e);
260
+
261
+ <div>{"An error occured! " + e.message}</div>
262
+ }
263
+ </div>
264
+ }
265
+ ```
266
+
267
+ ![iamge for the sourcecode above](assets/readme-4.png)
268
+
269
+ ### Props
270
+
271
+ If you want a prop to be reactive, you should also give it a `$` prefix.
272
+
273
+ ```ripple
274
+ component Button(props: { $text: string, onClick: () => void }) {
275
+ <button onClick={props.onClick}>
276
+ {props.$text}
277
+ </button>
278
+ }
279
+
280
+ // Usage
281
+ <Button $text={some_text} onClick={() => console.log("Clicked!")} />
282
+ ```
283
+
284
+ ### Children
285
+
286
+ Use `$children` prop and then use it in the form of `<$children />` for component composition.
287
+
288
+ When you pass in children to a component, it gets implicitly passed as the `$children` prop, in the form of a component.
289
+
290
+ ```ripple
291
+ import type { Component } from 'ripple';
292
+
293
+ component Card(props: { $children: Component }) {
294
+ <div class="card">
295
+ <$children />
296
+ </div>
297
+ }
298
+
299
+ // Usage
300
+ <Card>
301
+ <p>{"Card content here"}</p>
302
+ </Card>
303
+ ```
304
+
305
+ You could also explicitly write the same code as shown:
306
+
307
+ ```ripple
308
+ import type { Component } from 'ripple';
309
+
310
+ component Card(props: { $children: Component }) {
311
+ <div class="card">
312
+ <$children />
313
+ </div>
314
+ }
315
+
316
+ // Usage with explicit component
317
+ <Card>
318
+ component $children() {
319
+ <p>{"Card content here"}</p>
320
+ }
321
+ </Card>
322
+ ```
323
+
324
+ ### Events
325
+
326
+ Like React, events are props that start with `on` and then continue with an uppercase character, such as:
327
+
328
+ - `onClick`
329
+ - `onPointerMove`
330
+ - `onPointerDown`
331
+ - `onKeyDown`
332
+
333
+ For `capture` phase events, just add `Capture` to the end of the prop name:
334
+
335
+ - `onClickCapture`
336
+ - `onPointerMoveCapture`
337
+ - `onPointerDownCapture`
338
+ - `onKeyDownCapture`
339
+
340
+ > Note: Some events are automatically delegated where possible by Ripple to improve runtime performance.
341
+
342
+ ### Styling
343
+
344
+ Ripple supports native CSS styling that is localized to the given component using the `<style>` element.
345
+
346
+ ```ripple
347
+ component MyComponent() {
348
+ <div class="container">
349
+ <h1>{"Hello World"}</h1>
350
+ </div>
351
+
352
+ <style>
353
+ .container {
354
+ background: blue;
355
+ padding: 1rem;
356
+ }
357
+
358
+ h1 {
359
+ color: white;
360
+ font-size: 2rem;
361
+ }
362
+ </style>
363
+ }
364
+ ```
365
+
366
+ > Note: the `<style>` element must be top-level within a `component`.
367
+
368
+ ## VSCode Extension
369
+
370
+ The [Ripple VSCode extension](https://marketplace.visualstudio.com/items?itemName=ripplejs.ripple-vscode-plugin) provides:
371
+
372
+ - **Syntax Highlighting** for `.ripple` files
373
+ - **Real-time Diagnostics** for compilation errors
374
+ - **TypeScript Integration** for type checking
375
+ - **IntelliSense** for autocompletion
376
+
377
+ You can find the extension on the VS Code Marketplace as [`Ripple for VS Code`](https://marketplace.visualstudio.com/items?itemName=ripplejs.ripple-vscode-plugin).
378
+
379
+ ## Playground
380
+
381
+ Feel free to play around with how Ripple works. If you clone the repo, you can then:
382
+
383
+ ```bash
384
+ pnpm i && cd playground && pnpm dev
385
+ ```
386
+
387
+ The playground uses Ripple's Vite plugin, where you can play around with things inside the `playground/src` directory.
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.2",
6
+ "version": "0.2.4",
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
 
@@ -439,20 +439,6 @@ const visitors = {
439
439
  attribute
440
440
  );
441
441
  }
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
442
  }
457
443
 
458
444
  if (is_tracked_name(name)) {
@@ -44,7 +44,7 @@ function visit_function(node, context) {
44
44
 
45
45
  let body = context.visit(node.body, state);
46
46
 
47
- if (metadata.tracked === true) {
47
+ if (metadata?.tracked === true) {
48
48
  return /** @type {FunctionExpression} */ ({
49
49
  ...node,
50
50
  params: node.params.map((param) => context.visit(param, state)),
@@ -66,7 +66,7 @@ function build_getter(node, context) {
66
66
 
67
67
  // don't transform the declaration itself
68
68
  if (node !== binding?.node && binding?.transform) {
69
- return binding.transform.read(node);
69
+ return binding.transform.read(node, context.state?.metadata?.spread);
70
70
  }
71
71
  }
72
72
 
@@ -345,11 +345,9 @@ const visitors = {
345
345
  );
346
346
 
347
347
  if (is_spreading) {
348
- if (spread_attributes.length === 0) {
349
- state.template.push(attr_value);
350
- } else {
351
- spread_attributes.push(b.prop('init', b.literal(name), attr_value));
352
- }
348
+ spread_attributes.push(b.prop('init', b.literal(name), attr_value));
349
+ } else {
350
+ state.template.push(attr_value);
353
351
  }
354
352
  };
355
353
 
@@ -411,18 +409,12 @@ const visitors = {
411
409
  continue;
412
410
  }
413
411
 
414
- if (name === '$ref') {
415
- const id = state.flush_node();
416
- const value = attr.value;
417
-
418
- state.init.push(b.stmt(b.call('$.set_ref', id, visit(value, state))));
419
- continue;
420
- }
421
-
422
412
  if (is_event_attribute(name)) {
423
- const event_name = name.slice(2).toLowerCase();
413
+ let capture = name.endsWith('Capture');
414
+ let event_name = capture
415
+ ? name.slice(2, -7).toLowerCase()
416
+ : name.slice(2).toLowerCase();
424
417
  let handler = visit(attr.value, state);
425
- let capture = false; // TODO
426
418
 
427
419
  if (attr.metadata?.delegated) {
428
420
  let delegated_assignment;
@@ -554,8 +546,9 @@ const visitors = {
554
546
 
555
547
  state.template.push('<!>');
556
548
 
549
+ const is_spreading = node.attributes.some((attr) => attr.type === 'SpreadAttribute');
557
550
  const tracked = [];
558
- let props = [];
551
+ const props = [];
559
552
  let children_prop = null;
560
553
 
561
554
  for (const attr of node.attributes) {
@@ -579,6 +572,15 @@ const visitors = {
579
572
  } else {
580
573
  props.push(b.prop('init', attr.name, visit(attr.value, state)));
581
574
  }
575
+ } else if (attr.type === 'SpreadAttribute') {
576
+ props.push(
577
+ b.spread(
578
+ b.call(
579
+ '$.spread_object',
580
+ visit(attr.argument, { ...state, metadata: { ...state.metadata, spread: true } })
581
+ )
582
+ )
583
+ );
582
584
  } else {
583
585
  throw new Error('TODO');
584
586
  }
@@ -602,7 +604,18 @@ const visitors = {
602
604
  }
603
605
  }
604
606
 
605
- if (tracked.length > 0) {
607
+ if (is_spreading) {
608
+ state.init.push(
609
+ b.stmt(
610
+ b.call(
611
+ node.id,
612
+ id,
613
+ b.call('$.tracked_spread_object', b.thunk(b.object(props))),
614
+ b.id('$.active_block')
615
+ )
616
+ )
617
+ );
618
+ } else if (tracked.length > 0) {
606
619
  state.init.push(
607
620
  b.stmt(
608
621
  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,