ripple 0.2.143 → 0.2.145
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/package.json +2 -2
- package/src/compiler/phases/1-parse/index.js +50 -45
- package/src/compiler/phases/2-analyze/index.js +7 -4
- package/src/compiler/scope.js +4 -1
- package/src/runtime/index-client.js +1 -0
- package/src/runtime/internal/client/bindings.js +33 -18
- package/tests/client/basic/basic.components.test.ripple +36 -0
- package/types/index.d.ts +2 -0
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Ripple is an elegant TypeScript UI framework",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Dominic Gannaway",
|
|
6
|
-
"version": "0.2.
|
|
6
|
+
"version": "0.2.145",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"module": "src/runtime/index-client.js",
|
|
9
9
|
"main": "src/runtime/index-client.js",
|
|
@@ -81,6 +81,6 @@
|
|
|
81
81
|
"typescript": "^5.9.2"
|
|
82
82
|
},
|
|
83
83
|
"peerDependencies": {
|
|
84
|
-
"ripple": "0.2.
|
|
84
|
+
"ripple": "0.2.145"
|
|
85
85
|
}
|
|
86
86
|
}
|
|
@@ -491,6 +491,7 @@ function RipplePlugin(config) {
|
|
|
491
491
|
forInit,
|
|
492
492
|
);
|
|
493
493
|
}
|
|
494
|
+
|
|
494
495
|
/**
|
|
495
496
|
* Parse expression atom - handles TrackedArray and TrackedObject literals
|
|
496
497
|
* @param {any} [refDestructuringErrors]
|
|
@@ -524,6 +525,11 @@ function RipplePlugin(config) {
|
|
|
524
525
|
return this.parseTrackedObjectExpression();
|
|
525
526
|
}
|
|
526
527
|
|
|
528
|
+
// Check if this is a component expression (e.g., in object literal values)
|
|
529
|
+
if (this.type === tt.name && this.value === 'component') {
|
|
530
|
+
return this.parseComponent();
|
|
531
|
+
}
|
|
532
|
+
|
|
527
533
|
return super.parseExprAtom(refDestructuringErrors, forNew, forInit);
|
|
528
534
|
}
|
|
529
535
|
|
|
@@ -714,33 +720,55 @@ function RipplePlugin(config) {
|
|
|
714
720
|
return this.finishNode(node, 'TrackedObjectExpression');
|
|
715
721
|
}
|
|
716
722
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
723
|
+
/**
|
|
724
|
+
* Parse a component - common implementation used by statements, expressions, and export defaults
|
|
725
|
+
* @param {Object} options - Parsing options
|
|
726
|
+
* @param {boolean} [options.requireName=false] - Whether component name is required
|
|
727
|
+
* @param {boolean} [options.isDefault=false] - Whether this is an export default component
|
|
728
|
+
* @param {boolean} [options.declareName=false] - Whether to declare the name in scope
|
|
729
|
+
* @returns {any} Component node
|
|
730
|
+
*/
|
|
731
|
+
parseComponent({ requireName = false, isDefault = false, declareName = false } = {}) {
|
|
732
|
+
const node = this.startNode();
|
|
733
|
+
node.type = 'Component';
|
|
734
|
+
node.css = null;
|
|
735
|
+
node.default = isDefault;
|
|
736
|
+
this.next(); // consume 'component'
|
|
737
|
+
this.enterScope(0);
|
|
726
738
|
|
|
739
|
+
if (requireName) {
|
|
740
|
+
node.id = this.parseIdent();
|
|
741
|
+
if (declareName) {
|
|
742
|
+
this.declareName(node.id.name, 'var', node.id.start);
|
|
743
|
+
}
|
|
744
|
+
} else {
|
|
727
745
|
node.id = this.type.label === 'name' ? this.parseIdent() : null;
|
|
746
|
+
if (declareName && node.id) {
|
|
747
|
+
this.declareName(node.id.name, 'var', node.id.start);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
728
750
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
751
|
+
this.parseFunctionParams(node);
|
|
752
|
+
this.eat(tt.braceL);
|
|
753
|
+
node.body = [];
|
|
754
|
+
this.#path.push(node);
|
|
733
755
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
756
|
+
this.parseTemplateBody(node.body);
|
|
757
|
+
this.#path.pop();
|
|
758
|
+
this.exitScope();
|
|
737
759
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
760
|
+
this.next();
|
|
761
|
+
skipWhitespace(this);
|
|
762
|
+
this.finishNode(node, 'Component');
|
|
763
|
+
this.awaitPos = 0;
|
|
742
764
|
|
|
743
|
-
|
|
765
|
+
return node;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
parseExportDefaultDeclaration() {
|
|
769
|
+
// Check if this is "export default component"
|
|
770
|
+
if (this.value === 'component') {
|
|
771
|
+
return this.parseComponent({ isDefault: true });
|
|
744
772
|
}
|
|
745
773
|
|
|
746
774
|
return super.parseExportDefaultDeclaration();
|
|
@@ -1650,31 +1678,8 @@ function RipplePlugin(config) {
|
|
|
1650
1678
|
|
|
1651
1679
|
if (this.value === 'component') {
|
|
1652
1680
|
this.awaitPos = 0;
|
|
1653
|
-
|
|
1654
|
-
node.type = 'Component';
|
|
1655
|
-
node.css = null;
|
|
1656
|
-
this.next();
|
|
1657
|
-
this.enterScope(0);
|
|
1658
|
-
node.id = this.parseIdent();
|
|
1659
|
-
this.declareName(node.id.name, 'var', node.id.start);
|
|
1660
|
-
this.parseFunctionParams(node);
|
|
1661
|
-
this.eat(tt.braceL);
|
|
1662
|
-
node.body = [];
|
|
1663
|
-
this.#path.push(node);
|
|
1664
|
-
|
|
1665
|
-
this.parseTemplateBody(node.body);
|
|
1666
|
-
|
|
1667
|
-
this.#path.pop();
|
|
1668
|
-
this.exitScope();
|
|
1669
|
-
|
|
1670
|
-
this.next();
|
|
1671
|
-
skipWhitespace(this);
|
|
1672
|
-
this.finishNode(node, 'Component');
|
|
1673
|
-
this.awaitPos = 0;
|
|
1674
|
-
|
|
1675
|
-
return node;
|
|
1681
|
+
return this.parseComponent({ requireName: true, declareName: true });
|
|
1676
1682
|
}
|
|
1677
|
-
|
|
1678
1683
|
if (this.type.label === '@') {
|
|
1679
1684
|
// Try to parse as an expression statement first using tryParse
|
|
1680
1685
|
// This allows us to handle Ripple @ syntax like @count++ without
|
|
@@ -356,10 +356,13 @@ const visitors = {
|
|
|
356
356
|
}
|
|
357
357
|
|
|
358
358
|
// Store component metadata in analysis
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
359
|
+
// Only add metadata if component has a name (not anonymous)
|
|
360
|
+
if (node.id) {
|
|
361
|
+
context.state.analysis.component_metadata.push({
|
|
362
|
+
id: node.id.name,
|
|
363
|
+
async: metadata.await,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
363
366
|
},
|
|
364
367
|
|
|
365
368
|
ForStatement(node, context) {
|
package/src/compiler/scope.js
CHANGED
|
@@ -150,7 +150,10 @@ export function create_scopes(ast, root, parent) {
|
|
|
150
150
|
const scope = state.scope.child();
|
|
151
151
|
scopes.set(node, scope);
|
|
152
152
|
|
|
153
|
-
|
|
153
|
+
// Only declare the component name if it has an id (not anonymous)
|
|
154
|
+
if (node.id) {
|
|
155
|
+
scope.declare(node.id, 'normal', 'component');
|
|
156
|
+
}
|
|
154
157
|
|
|
155
158
|
add_params(scope, node.params);
|
|
156
159
|
next({ scope });
|
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
/** @import {
|
|
1
|
+
/** @import { Tracked } from '#client' */
|
|
2
2
|
|
|
3
3
|
import { effect, render } from './blocks.js';
|
|
4
4
|
import { on } from './events.js';
|
|
5
|
-
import {
|
|
5
|
+
import { get, set, tick, untrack } from './runtime.js';
|
|
6
6
|
import { is_array, is_tracked_object } from './utils.js';
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} name
|
|
10
|
+
* @returns {TypeError}
|
|
11
|
+
*/
|
|
12
|
+
function not_tracked_type_error(name) {
|
|
13
|
+
return new TypeError(`${name} argument is not a tracked object`);
|
|
14
|
+
}
|
|
15
|
+
|
|
8
16
|
/**
|
|
9
17
|
* Resize observer singleton.
|
|
10
18
|
* One listener per element only!
|
|
@@ -145,10 +153,9 @@ function select_option(select, value, mounting = false) {
|
|
|
145
153
|
*/
|
|
146
154
|
export function bindValue(maybe_tracked) {
|
|
147
155
|
if (!is_tracked_object(maybe_tracked)) {
|
|
148
|
-
throw
|
|
156
|
+
throw not_tracked_type_error('bindValue()');
|
|
149
157
|
}
|
|
150
158
|
|
|
151
|
-
var block = /** @type {Block} */ (active_block);
|
|
152
159
|
var tracked = /** @type {Tracked} */ (maybe_tracked);
|
|
153
160
|
|
|
154
161
|
return (node) => {
|
|
@@ -246,10 +253,9 @@ export function bindValue(maybe_tracked) {
|
|
|
246
253
|
*/
|
|
247
254
|
export function bindChecked(maybe_tracked) {
|
|
248
255
|
if (!is_tracked_object(maybe_tracked)) {
|
|
249
|
-
throw
|
|
256
|
+
throw not_tracked_type_error('bindChecked()');
|
|
250
257
|
}
|
|
251
258
|
|
|
252
|
-
const block = /** @type {any} */ (active_block);
|
|
253
259
|
const tracked = /** @type {Tracked} */ (maybe_tracked);
|
|
254
260
|
|
|
255
261
|
return (input) => {
|
|
@@ -267,12 +273,9 @@ export function bindChecked(maybe_tracked) {
|
|
|
267
273
|
*/
|
|
268
274
|
function bind_element_size(maybe_tracked, type) {
|
|
269
275
|
if (!is_tracked_object(maybe_tracked)) {
|
|
270
|
-
throw
|
|
271
|
-
`bind${type.charAt(0).toUpperCase() + type.slice(1)}() argument is not a tracked object`,
|
|
272
|
-
);
|
|
276
|
+
throw not_tracked_type_error(`bind${type.charAt(0).toUpperCase() + type.slice(1)}()`);
|
|
273
277
|
}
|
|
274
278
|
|
|
275
|
-
var block = /** @type {any} */ (active_block);
|
|
276
279
|
var tracked = /** @type {Tracked<any>} */ (maybe_tracked);
|
|
277
280
|
|
|
278
281
|
return (/** @type {HTMLElement} */ element) => {
|
|
@@ -325,12 +328,9 @@ export function bindOffsetHeight(maybe_tracked) {
|
|
|
325
328
|
*/
|
|
326
329
|
function bind_element_rect(maybe_tracked, type) {
|
|
327
330
|
if (!is_tracked_object(maybe_tracked)) {
|
|
328
|
-
throw
|
|
329
|
-
`bind${type.charAt(0).toUpperCase() + type.slice(1)}() argument is not a tracked object`,
|
|
330
|
-
);
|
|
331
|
+
throw not_tracked_type_error(`bind${type.charAt(0).toUpperCase() + type.slice(1)}()`);
|
|
331
332
|
}
|
|
332
333
|
|
|
333
|
-
var block = /** @type {any} */ (active_block);
|
|
334
334
|
var tracked = /** @type {Tracked<any>} */ (maybe_tracked);
|
|
335
335
|
var observer =
|
|
336
336
|
type === 'contentRect' || type === 'contentBoxSize'
|
|
@@ -388,12 +388,9 @@ export function bindDevicePixelContentBoxSize(maybe_tracked) {
|
|
|
388
388
|
*/
|
|
389
389
|
export function bind_content_editable(maybe_tracked, property) {
|
|
390
390
|
if (!is_tracked_object(maybe_tracked)) {
|
|
391
|
-
throw
|
|
392
|
-
`bind${property.charAt(0).toUpperCase() + property.slice(1)}() argument is not a tracked object`,
|
|
393
|
-
);
|
|
391
|
+
throw not_tracked_type_error(`bind${property.charAt(0).toUpperCase() + property.slice(1)}()`);
|
|
394
392
|
}
|
|
395
393
|
|
|
396
|
-
const block = /** @type {any} */ (active_block);
|
|
397
394
|
const tracked = /** @type {Tracked} */ (maybe_tracked);
|
|
398
395
|
|
|
399
396
|
return (element) => {
|
|
@@ -443,3 +440,21 @@ export function bindInnerText(maybe_tracked) {
|
|
|
443
440
|
export function bindTextContent(maybe_tracked) {
|
|
444
441
|
return bind_content_editable(maybe_tracked, 'textContent');
|
|
445
442
|
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Syntactic sugar for binding a HTMLElement with {ref fn}
|
|
446
|
+
* @param {unknown} maybe_tracked
|
|
447
|
+
* @returns {(node: HTMLElement) => void}
|
|
448
|
+
*/
|
|
449
|
+
export function bindNode(maybe_tracked) {
|
|
450
|
+
if (!is_tracked_object(maybe_tracked)) {
|
|
451
|
+
throw not_tracked_type_error('bindNode()');
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const tracked = /** @type {Tracked} */ (maybe_tracked);
|
|
455
|
+
|
|
456
|
+
/** @param {HTMLElement} node */
|
|
457
|
+
return (node) => {
|
|
458
|
+
set(tracked, node);
|
|
459
|
+
};
|
|
460
|
+
}
|
|
@@ -232,4 +232,40 @@ describe('basic client > components & composition', () => {
|
|
|
232
232
|
flushSync();
|
|
233
233
|
expect(countSpan.textContent).toBe('1');
|
|
234
234
|
});
|
|
235
|
+
|
|
236
|
+
it('renders components as named and anonymous properties', () => {
|
|
237
|
+
const UI = {
|
|
238
|
+
span: component Span(){
|
|
239
|
+
<span>{'Hello from Span'}</span>
|
|
240
|
+
},
|
|
241
|
+
button: component({ children }) {
|
|
242
|
+
<button>
|
|
243
|
+
<children />
|
|
244
|
+
</button>
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
component App(){
|
|
249
|
+
<div>
|
|
250
|
+
<h1>{'Component as Property Test'}</h1>
|
|
251
|
+
<UI.span />
|
|
252
|
+
<UI.button>
|
|
253
|
+
component children() {
|
|
254
|
+
<span>{'Click me!'}</span>
|
|
255
|
+
}
|
|
256
|
+
</UI.button>
|
|
257
|
+
</div>
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
render(App);
|
|
261
|
+
|
|
262
|
+
const heading = container.querySelector('h1');
|
|
263
|
+
const span = container.querySelector('span');
|
|
264
|
+
const button = container.querySelector('button');
|
|
265
|
+
const buttonSpan = button.querySelector('span');
|
|
266
|
+
|
|
267
|
+
expect(heading.textContent).toBe('Component as Property Test');
|
|
268
|
+
expect(span.textContent).toBe('Hello from Span');
|
|
269
|
+
expect(buttonSpan.textContent).toBe('Click me!');
|
|
270
|
+
});
|
|
235
271
|
});
|
package/types/index.d.ts
CHANGED
|
@@ -287,3 +287,5 @@ export declare function bindInnerHTML<V>(tracked: Tracked<V>): (node: HTMLElemen
|
|
|
287
287
|
export declare function bindInnerText<V>(tracked: Tracked<V>): (node: HTMLElement) => void;
|
|
288
288
|
|
|
289
289
|
export declare function bindTextContent<V>(tracked: Tracked<V>): (node: HTMLElement) => void;
|
|
290
|
+
|
|
291
|
+
export declare function bindNode<V>(tracked: Tracked<V>): (node: HTMLElement) => void;
|