ripple 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/package.json +2 -1
- package/src/jsx-runtime.d.ts +12 -2
- package/src/runtime/internal/client/render.js +5 -4
- package/src/runtime/internal/server/index.js +4 -2
- package/tests/client/basic/basic.attributes.test.tsrx +37 -0
- package/tests/server/basic.attributes.test.tsrx +28 -0
- package/tests/utils/jsx-runtime-types.test.js +67 -2
- package/tests/utils/tracked-types.test.js +117 -0
- package/types/index.d.ts +7 -8
- package/tests/client/compiler/__snapshots__/compiler.assignments.test.rsrx.snap +0 -12
- package/tests/client/compiler/__snapshots__/compiler.typescript.test.rsrx.snap +0 -46
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# ripple
|
|
2
2
|
|
|
3
|
+
## 0.4.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#1477](https://github.com/Ripple-TS/ripple/pull/1477)
|
|
8
|
+
[`670d669`](https://github.com/Ripple-TS/ripple/commit/670d66916a0b2d58254adb3248abc9d3e816fd8d)
|
|
9
|
+
Thanks [@leonidaz](https://github.com/leonidaz)! - Use property-specific CSS
|
|
10
|
+
types for HTML and SVG style objects and export `CSSProperties` from `ripple`
|
|
11
|
+
and `ripple/jsx-runtime`. Numeric lengths such as `width: 400` now produce a
|
|
12
|
+
type error; specify units with `width: '400px'` or `width: '24rem'`. Unitless
|
|
13
|
+
CSS properties, zero lengths, camelCase and kebab-case names, and CSS custom
|
|
14
|
+
properties remain supported.
|
|
15
|
+
|
|
16
|
+
Style properties set to `null` or `undefined` are omitted during server
|
|
17
|
+
rendering and removed on the client, matching the nullable and optional property
|
|
18
|
+
types and supporting conditional style values. Ripple does not infer or add CSS
|
|
19
|
+
units.
|
|
20
|
+
|
|
21
|
+
Re-export the `JSX` and `Ripple` namespaces as types from `ripple` so element
|
|
22
|
+
prop and event types can be imported from the main package. The
|
|
23
|
+
`ripple/jsx-runtime` exports remain available.
|
|
24
|
+
|
|
25
|
+
- [#1475](https://github.com/Ripple-TS/ripple/pull/1475)
|
|
26
|
+
[`e008b3f`](https://github.com/Ripple-TS/ripple/commit/e008b3f933a5a5b54ca8b74ae80aa98b6ddfb82f)
|
|
27
|
+
Thanks [@leonidaz](https://github.com/leonidaz)! - Show public Tracked, Derived,
|
|
28
|
+
and WritableDerived type names in value property hovers.
|
|
29
|
+
|
|
3
30
|
## 0.4.0
|
|
4
31
|
|
|
5
32
|
### Minor Changes
|
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.4.
|
|
6
|
+
"version": "0.4.1",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"module": "src/runtime/index-client.js",
|
|
9
9
|
"main": "src/runtime/index-client.js",
|
|
@@ -77,6 +77,7 @@
|
|
|
77
77
|
},
|
|
78
78
|
"dependencies": {
|
|
79
79
|
"clsx": "^2.1.1",
|
|
80
|
+
"csstype": "^3.2.3",
|
|
80
81
|
"devalue": "^5.9.2",
|
|
81
82
|
"esm-env": "^1.2.2",
|
|
82
83
|
"@tsrx/core": "^0.2.0",
|
package/src/jsx-runtime.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
TSRXElement,
|
|
8
8
|
} from '#public';
|
|
9
9
|
import type { Nullable } from '@tsrx/core/types/helpers';
|
|
10
|
+
import type * as CSS from 'csstype';
|
|
10
11
|
export type { RefValue } from '#public';
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -50,10 +51,18 @@ export function jsxs(
|
|
|
50
51
|
*/
|
|
51
52
|
export function Fragment(props: FragmentProps): TSRXElement;
|
|
52
53
|
|
|
54
|
+
type NullableStyleProperties<T> = { [Property in keyof T]: T[Property] | null };
|
|
55
|
+
|
|
53
56
|
/** JSX attributes live under Ripple so editor hovers identify their framework. */
|
|
54
57
|
declare namespace Ripple {
|
|
55
58
|
type ClassValue = string | import('clsx').ClassArray | import('clsx').ClassDictionary;
|
|
56
59
|
|
|
60
|
+
/** Inline CSS styles. Values keep their units; null and undefined remove a property. */
|
|
61
|
+
interface CSSProperties
|
|
62
|
+
extends NullableStyleProperties<CSS.Properties>, NullableStyleProperties<CSS.PropertiesHyphen> {
|
|
63
|
+
[property: `--${string}`]: string | number | null | undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
57
66
|
type Event<
|
|
58
67
|
Target extends globalThis.EventTarget,
|
|
59
68
|
EventType extends globalThis.Event,
|
|
@@ -493,7 +502,7 @@ declare namespace Ripple {
|
|
|
493
502
|
class?: ClassValue | undefined | null;
|
|
494
503
|
className?: Nullable<string>;
|
|
495
504
|
id?: Nullable<string>;
|
|
496
|
-
style?: Nullable<string> |
|
|
505
|
+
style?: Nullable<string> | CSSProperties;
|
|
497
506
|
title?: Nullable<string>;
|
|
498
507
|
lang?: Nullable<string>;
|
|
499
508
|
dir?: 'ltr' | 'rtl' | 'auto';
|
|
@@ -627,7 +636,7 @@ declare namespace Ripple {
|
|
|
627
636
|
// Styling
|
|
628
637
|
class?: ClassValue | undefined | null;
|
|
629
638
|
className?: Nullable<string>;
|
|
630
|
-
style?: Nullable<string> |
|
|
639
|
+
style?: Nullable<string> | CSSProperties;
|
|
631
640
|
|
|
632
641
|
// Presentation attributes
|
|
633
642
|
alignmentBaseline?:
|
|
@@ -1405,6 +1414,7 @@ declare namespace Ripple {
|
|
|
1405
1414
|
}
|
|
1406
1415
|
|
|
1407
1416
|
export import ClassValue = Ripple.ClassValue;
|
|
1417
|
+
export import CSSProperties = Ripple.CSSProperties;
|
|
1408
1418
|
export import JSX = Ripple.JSX;
|
|
1409
1419
|
export { Ripple };
|
|
1410
1420
|
|
|
@@ -68,7 +68,7 @@ function get_setters(element) {
|
|
|
68
68
|
/**
|
|
69
69
|
* @param {Element} element
|
|
70
70
|
* @param {any} value
|
|
71
|
-
* @param {Record<string, string> | undefined} prev
|
|
71
|
+
* @param {Record<string, string | number | null | undefined> | undefined} prev
|
|
72
72
|
* @returns {void}
|
|
73
73
|
*/
|
|
74
74
|
export function set_style(element, value, prev = {}) {
|
|
@@ -100,8 +100,8 @@ export function set_attribute(element, attribute, value) {
|
|
|
100
100
|
|
|
101
101
|
/**
|
|
102
102
|
* @param {HTMLElement} element
|
|
103
|
-
* @param {Record<string, string | number>} new_styles
|
|
104
|
-
* @param {Record<string, string>} prev
|
|
103
|
+
* @param {Record<string, string | number | null | undefined>} new_styles
|
|
104
|
+
* @param {Record<string, string | number | null | undefined>} prev
|
|
105
105
|
*/
|
|
106
106
|
function apply_styles(element, new_styles, prev) {
|
|
107
107
|
const style = element.style;
|
|
@@ -109,7 +109,8 @@ function apply_styles(element, new_styles, prev) {
|
|
|
109
109
|
// Apply new styles
|
|
110
110
|
for (const key in new_styles) {
|
|
111
111
|
const css_prop = normalize_css_property_name(key);
|
|
112
|
-
const
|
|
112
|
+
const raw_value = new_styles[key];
|
|
113
|
+
const value = raw_value == null ? null : String(raw_value);
|
|
113
114
|
|
|
114
115
|
if (!(key in prev) || prev[key] !== value) {
|
|
115
116
|
style.setProperty(css_prop, value);
|
|
@@ -1610,14 +1610,16 @@ export function attr(name, value, is_boolean = false) {
|
|
|
1610
1610
|
}
|
|
1611
1611
|
|
|
1612
1612
|
/**
|
|
1613
|
-
* @param {Record<string, string | number>} styles
|
|
1613
|
+
* @param {Record<string, string | number | null | undefined>} styles
|
|
1614
1614
|
* @returns {string}
|
|
1615
1615
|
*/
|
|
1616
1616
|
function get_styles(styles) {
|
|
1617
1617
|
var result = '';
|
|
1618
1618
|
for (const key in styles) {
|
|
1619
|
+
const raw_value = styles[key];
|
|
1620
|
+
if (raw_value == null) continue;
|
|
1619
1621
|
const css_prop = normalize_css_property_name(key);
|
|
1620
|
-
const value = String(
|
|
1622
|
+
const value = String(raw_value).trim();
|
|
1621
1623
|
result += `${css_prop}: ${value}; `;
|
|
1622
1624
|
}
|
|
1623
1625
|
return result.trim();
|
|
@@ -244,6 +244,43 @@ describe('basic client > attribute rendering', () => {
|
|
|
244
244
|
expect(div.style.fontWeight).toBe('bold');
|
|
245
245
|
});
|
|
246
246
|
|
|
247
|
+
it.each([undefined, null])('omits %s styles and removes and restores conditional values', (
|
|
248
|
+
empty,
|
|
249
|
+
) => {
|
|
250
|
+
function Basic() @{
|
|
251
|
+
const active = track(false);
|
|
252
|
+
<>
|
|
253
|
+
<button onClick={() => (active.value = !active.value)}>Toggle</button>
|
|
254
|
+
<div
|
|
255
|
+
style={{
|
|
256
|
+
width: active.value ? '400px' : empty,
|
|
257
|
+
'--scale': active.value ? 2 : empty,
|
|
258
|
+
margin: 0,
|
|
259
|
+
opacity: 0,
|
|
260
|
+
'--literal': 'undefined',
|
|
261
|
+
'--literal-null': 'null',
|
|
262
|
+
}}
|
|
263
|
+
/>
|
|
264
|
+
</>
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
render(Basic);
|
|
268
|
+
|
|
269
|
+
const button = container.querySelector('button');
|
|
270
|
+
const div = container.querySelector('div');
|
|
271
|
+
|
|
272
|
+
for (const active of [false, true, false, true]) {
|
|
273
|
+
expect(div.style.width).toBe(active ? '400px' : '');
|
|
274
|
+
expect(div.style.getPropertyValue('--scale')).toBe(active ? '2' : '');
|
|
275
|
+
expect(div.style.margin).toBe('0px');
|
|
276
|
+
expect(div.style.opacity).toBe('0');
|
|
277
|
+
expect(div.style.getPropertyValue('--literal')).toBe('undefined');
|
|
278
|
+
expect(div.style.getPropertyValue('--literal-null')).toBe('null');
|
|
279
|
+
button.click();
|
|
280
|
+
flushSync();
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
247
284
|
it('render tracked variable as style attribute', () => {
|
|
248
285
|
function Basic() @{
|
|
249
286
|
const style = track({ color: 'red', fontWeight: 'bold' });
|
|
@@ -235,6 +235,34 @@ describe('basic server > attribute rendering', () => {
|
|
|
235
235
|
expect(div.style.fontWeight).toBe('bold');
|
|
236
236
|
});
|
|
237
237
|
|
|
238
|
+
it.each([undefined, null])('omits %s style values from HTML and SVG attributes', async (
|
|
239
|
+
empty,
|
|
240
|
+
) => {
|
|
241
|
+
function Basic() @{
|
|
242
|
+
const styles = {
|
|
243
|
+
width: empty,
|
|
244
|
+
'--scale': empty,
|
|
245
|
+
margin: 0,
|
|
246
|
+
opacity: 0,
|
|
247
|
+
'--literal': 'undefined',
|
|
248
|
+
'--literal-null': 'null',
|
|
249
|
+
};
|
|
250
|
+
<>
|
|
251
|
+
<div style={styles} />
|
|
252
|
+
<svg {...{ style: styles }} />
|
|
253
|
+
</>
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const { body } = await render(Basic);
|
|
257
|
+
const { document } = parseHtml(body);
|
|
258
|
+
|
|
259
|
+
for (const element of document.querySelectorAll('div, svg')) {
|
|
260
|
+
expect(element.getAttribute('style')).toBe(
|
|
261
|
+
'margin: 0; opacity: 0; --literal: undefined; --literal-null: null;',
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
238
266
|
it('render tracked variable as style attribute', async () => {
|
|
239
267
|
function Basic() @{
|
|
240
268
|
const style = track({ color: 'red', fontWeight: 'bold' });
|
|
@@ -43,6 +43,68 @@ function create_service(source, automatic) {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
describe('Ripple JSX types', () => {
|
|
46
|
+
it.each([true, false])('checks CSS property values (automatic runtime: %s)', (automatic) => {
|
|
47
|
+
const source = `
|
|
48
|
+
import type { CSSProperties, Ripple } from 'ripple';
|
|
49
|
+
import type { CSSProperties as RuntimeCSSProperties } from 'ripple/jsx-runtime';
|
|
50
|
+
|
|
51
|
+
const styles = {
|
|
52
|
+
width: '24rem', height: '50%', margin: 0, padding: '1em',
|
|
53
|
+
maxWidth: 'calc(100% - 2rem)', minWidth: 'var(--min-width)',
|
|
54
|
+
opacity: 0.5, lineHeight: 1.5, zIndex: 2, flexGrow: 1,
|
|
55
|
+
fontSizeAdjust: 0.5, shapeImageThreshold: 0.5, mathDepth: 2, scale: 1.5,
|
|
56
|
+
'font-size': '1rem', 'line-height': 1.5, 'border-width': 0,
|
|
57
|
+
WebkitLineClamp: 2, '-webkit-line-clamp': 2,
|
|
58
|
+
transitionDuration: '200ms',
|
|
59
|
+
'--space': '1rem', '--scale': 2,
|
|
60
|
+
} satisfies CSSProperties;
|
|
61
|
+
const namespaced: Ripple.CSSProperties = styles;
|
|
62
|
+
const runtime_styles: RuntimeCSSProperties = styles;
|
|
63
|
+
const html = <div style={namespaced} />;
|
|
64
|
+
const svg = <svg style={styles}><circle style={{ strokeWidth: 2, 'fill-opacity': 0.5 }} /></svg>;
|
|
65
|
+
const zero = <div style={{ width: 0 }} />;
|
|
66
|
+
const css_text = <div style="width: 400px; opacity: 0.5" />;
|
|
67
|
+
const absent = <div style={null} />;
|
|
68
|
+
const omitted = <svg style={undefined} />;
|
|
69
|
+
const conditional = <div style={{ width: Math.random() ? '400px' : undefined, '--scale': undefined }} />;
|
|
70
|
+
const nullable = {
|
|
71
|
+
width: null, 'font-size': null, opacity: null, WebkitLineClamp: null,
|
|
72
|
+
'-webkit-line-clamp': null, '--scale': null,
|
|
73
|
+
} satisfies CSSProperties;
|
|
74
|
+
const nullable_html = <div style={nullable} />;
|
|
75
|
+
const nullable_svg = <svg style={nullable} />;
|
|
76
|
+
|
|
77
|
+
// @ts-expect-error Lengths require explicit units unless the value is zero.
|
|
78
|
+
const bad_styles = { width: 400 } satisfies CSSProperties;
|
|
79
|
+
// @ts-expect-error Lengths require explicit units unless the value is zero.
|
|
80
|
+
const bad_width = <div style={{ width: 400 }} />;
|
|
81
|
+
// @ts-expect-error Kebab-case lengths use the same rules.
|
|
82
|
+
const bad_font_size = <div style={{ 'font-size': 16 }} />;
|
|
83
|
+
// @ts-expect-error SVG style objects also require units for these lengths.
|
|
84
|
+
const bad_svg = <svg style={{ width: 400 }} />;
|
|
85
|
+
// @ts-expect-error Vendor-prefixed lengths require units too.
|
|
86
|
+
const bad_vendor_length = <div style={{ WebkitBorderRadius: 4 }} />;
|
|
87
|
+
// @ts-expect-error Durations require time units, even for zero.
|
|
88
|
+
const bad_duration = <div style={{ transitionDuration: 0 }} />;
|
|
89
|
+
// @ts-expect-error Unknown property names do not bypass CSS checking.
|
|
90
|
+
const bad_property = <div style={{ widht: '400px' }} />;
|
|
91
|
+
// @ts-expect-error Custom properties accept strings and numbers, not objects.
|
|
92
|
+
const bad_custom = <div style={{ '--theme': {} }} />;
|
|
93
|
+
// @ts-expect-error CSS fallback arrays are not supported by the runtime.
|
|
94
|
+
const bad_array = <div style={{ display: ['flex', 'block'] }} />;
|
|
95
|
+
`;
|
|
96
|
+
const { service, file_name } = create_service(source, automatic);
|
|
97
|
+
try {
|
|
98
|
+
expect(
|
|
99
|
+
service
|
|
100
|
+
.getSemanticDiagnostics(file_name)
|
|
101
|
+
.map((diagnostic) => ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')),
|
|
102
|
+
).toEqual([]);
|
|
103
|
+
} finally {
|
|
104
|
+
service.dispose();
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
46
108
|
it.each([true, false])('qualifies HTML tag hovers (automatic runtime: %s)', (automatic) => {
|
|
47
109
|
const source = `
|
|
48
110
|
import 'ripple/jsx-runtime';
|
|
@@ -68,7 +130,8 @@ describe('Ripple JSX types', () => {
|
|
|
68
130
|
|
|
69
131
|
it('preserves JSX imports, global types, native events, and DOM refs', () => {
|
|
70
132
|
const source = `
|
|
71
|
-
import type { ClassValue, JSX as RuntimeJSX, Ripple } from 'ripple/jsx-runtime';
|
|
133
|
+
import type { ClassValue, JSX as RuntimeJSX, Ripple as RuntimeRipple } from 'ripple/jsx-runtime';
|
|
134
|
+
import type { JSX as PublicJSX, Ripple } from 'ripple';
|
|
72
135
|
import { createRefKey, type RefKey } from 'ripple';
|
|
73
136
|
|
|
74
137
|
const ref_key: RefKey = createRefKey();
|
|
@@ -81,8 +144,10 @@ describe('Ripple JSX types', () => {
|
|
|
81
144
|
};
|
|
82
145
|
const global_props: JSX.IntrinsicElements['input'] = props;
|
|
83
146
|
const runtime_props: RuntimeJSX.IntrinsicElements['input'] = global_props;
|
|
147
|
+
const public_props: PublicJSX.IntrinsicElements['input'] = runtime_props;
|
|
148
|
+
const runtime_namespace_props: RuntimeRipple.InputHTMLAttributes<HTMLInputElement> = public_props;
|
|
84
149
|
const classes: ClassValue = ['one', { two: true }];
|
|
85
|
-
const input = <input {...
|
|
150
|
+
const input = <input {...public_props} class={classes} />;
|
|
86
151
|
const svg = <svg><circle cx={10} ref={(node) => { const circle: SVGCircleElement = node; }} /></svg>;
|
|
87
152
|
// @ts-expect-error Input values do not accept objects.
|
|
88
153
|
const bad_value = <input value={{}} />;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {string} source
|
|
6
|
+
*/
|
|
7
|
+
function create_service(source) {
|
|
8
|
+
const root = process.cwd();
|
|
9
|
+
const file_name = `${root}/packages/ripple/tracked-types-test.tsx`;
|
|
10
|
+
const options = {
|
|
11
|
+
strict: true,
|
|
12
|
+
target: ts.ScriptTarget.ESNext,
|
|
13
|
+
module: ts.ModuleKind.ESNext,
|
|
14
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
15
|
+
jsx: ts.JsxEmit.Preserve,
|
|
16
|
+
jsxImportSource: 'ripple',
|
|
17
|
+
skipLibCheck: true,
|
|
18
|
+
types: [],
|
|
19
|
+
paths: {
|
|
20
|
+
'#public': [`${root}/packages/ripple/types/index.d.ts`],
|
|
21
|
+
ripple: [`${root}/packages/ripple/types/index.d.ts`],
|
|
22
|
+
'ripple/jsx-runtime': [`${root}/packages/ripple/src/jsx-runtime.d.ts`],
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
const service = ts.createLanguageService({
|
|
26
|
+
getCompilationSettings: () => options,
|
|
27
|
+
getScriptFileNames: () => [file_name],
|
|
28
|
+
getScriptVersion: () => '0',
|
|
29
|
+
getScriptSnapshot: (name) => {
|
|
30
|
+
const text = name === file_name ? source : ts.sys.readFile(name);
|
|
31
|
+
return text === undefined ? undefined : ts.ScriptSnapshot.fromString(text);
|
|
32
|
+
},
|
|
33
|
+
getCurrentDirectory: () => root,
|
|
34
|
+
getDefaultLibFileName: ts.getDefaultLibFilePath,
|
|
35
|
+
realpath: ts.sys.realpath,
|
|
36
|
+
directoryExists: ts.sys.directoryExists,
|
|
37
|
+
getDirectories: ts.sys.getDirectories,
|
|
38
|
+
fileExists: (name) => name === file_name || ts.sys.fileExists(name),
|
|
39
|
+
readFile: (name) => (name === file_name ? source : ts.sys.readFile(name)),
|
|
40
|
+
});
|
|
41
|
+
return { service, file_name };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe('Ripple tracked types', () => {
|
|
45
|
+
it('shows public names in value property hovers', () => {
|
|
46
|
+
const source = `
|
|
47
|
+
import { track } from 'ripple';
|
|
48
|
+
const count = track(0);
|
|
49
|
+
const derived = track(() => count.value * 2);
|
|
50
|
+
const writable = track(() => count.value, undefined, true);
|
|
51
|
+
count.value;
|
|
52
|
+
derived.value;
|
|
53
|
+
writable.value;
|
|
54
|
+
`;
|
|
55
|
+
const { service, file_name } = create_service(source);
|
|
56
|
+
try {
|
|
57
|
+
expect(service.getSemanticDiagnostics(file_name)).toEqual([]);
|
|
58
|
+
for (const [name, type] of [
|
|
59
|
+
['count', 'Tracked'],
|
|
60
|
+
['derived', 'Derived'],
|
|
61
|
+
['writable', 'WritableDerived'],
|
|
62
|
+
]) {
|
|
63
|
+
const info = service.getQuickInfoAtPosition(
|
|
64
|
+
file_name,
|
|
65
|
+
source.lastIndexOf(`${name}.value`) + name.length + 1,
|
|
66
|
+
);
|
|
67
|
+
expect(ts.displayPartsToString(info?.displayParts)).toBe(
|
|
68
|
+
`(property) ${type}<number>.value: number`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
} finally {
|
|
72
|
+
service.dispose();
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('preserves writes, read-only views, and tracked component props', () => {
|
|
77
|
+
const source = `
|
|
78
|
+
import { track, type Component, type Derived, type Tracked } from 'ripple';
|
|
79
|
+
const count = track(0);
|
|
80
|
+
count.value = 1;
|
|
81
|
+
const tracked: Tracked<number> = track(count);
|
|
82
|
+
const derived: Derived<number> = tracked;
|
|
83
|
+
const view = count.readOnly();
|
|
84
|
+
// @ts-expect-error Read-only views reject writes.
|
|
85
|
+
view.value = 2;
|
|
86
|
+
const computed = track(() => count.value * 2);
|
|
87
|
+
// @ts-expect-error Computed values reject writes.
|
|
88
|
+
computed.value = 2;
|
|
89
|
+
const writable = track(() => count.value, undefined, true);
|
|
90
|
+
writable.value = 2;
|
|
91
|
+
const with_setter = track(() => count.value, undefined, (next) => next);
|
|
92
|
+
with_setter.value = 3;
|
|
93
|
+
// @ts-expect-error Tracked values preserve their value type.
|
|
94
|
+
count.value = 'wrong';
|
|
95
|
+
// @ts-expect-error Non-component tracked values are not callable.
|
|
96
|
+
count({});
|
|
97
|
+
declare const TrackedComponent: Tracked<Component<{ name: string }>>;
|
|
98
|
+
declare const DerivedComponent: Derived<Component<{ name: string }>>;
|
|
99
|
+
const valid = <TrackedComponent name="Ripple" />;
|
|
100
|
+
const valid_derived = <DerivedComponent name="Ripple" />;
|
|
101
|
+
// @ts-expect-error Tracked components preserve required props.
|
|
102
|
+
const missing = <TrackedComponent />;
|
|
103
|
+
// @ts-expect-error Derived components preserve prop types.
|
|
104
|
+
const invalid = <DerivedComponent name={123} />;
|
|
105
|
+
`;
|
|
106
|
+
const { service, file_name } = create_service(source);
|
|
107
|
+
try {
|
|
108
|
+
expect(
|
|
109
|
+
service
|
|
110
|
+
.getSemanticDiagnostics(file_name)
|
|
111
|
+
.map((diagnostic) => ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')),
|
|
112
|
+
).toEqual([]);
|
|
113
|
+
} finally {
|
|
114
|
+
service.dispose();
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
package/types/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtendedEventOptions } from '@tsrx/core/types';
|
|
2
|
+
export type { CSSProperties, JSX, Ripple } from '../src/jsx-runtime.js';
|
|
2
3
|
export { setTransport } from './transport.js';
|
|
3
4
|
export type { Transport, Transporter } from './transport.js';
|
|
4
5
|
export type { RefValue } from '@tsrx/core/runtime/ref';
|
|
@@ -220,8 +221,8 @@ export const TRACKED_UPDATED: unique symbol;
|
|
|
220
221
|
export const SUSPENSE_PENDING: unique symbol;
|
|
221
222
|
export const SUSPENSE_REJECTED: unique symbol;
|
|
222
223
|
|
|
223
|
-
//
|
|
224
|
-
interface
|
|
224
|
+
// A tracked value: `track(0)`. Read and write it through `.value`.
|
|
225
|
+
export interface Tracked<V> extends TrackedCallable<V> {
|
|
225
226
|
'#v': V;
|
|
226
227
|
value: V;
|
|
227
228
|
/**
|
|
@@ -236,22 +237,20 @@ interface TrackedBase<V> {
|
|
|
236
237
|
interface TrackedCallable<V> {
|
|
237
238
|
(props: V extends Component<infer P> ? P : never): V extends Component ? void : never;
|
|
238
239
|
}
|
|
239
|
-
// A tracked value: `track(0)`. Read and write it through `.value`.
|
|
240
|
-
export type Tracked<V> = TrackedBase<V> & TrackedCallable<V>;
|
|
241
|
-
|
|
242
240
|
// A computed value: `track(() => ...)`. Its `value` is read-only unless the
|
|
243
241
|
// call opted into writes (see `WritableDerived`). A `Tracked` satisfies it.
|
|
244
|
-
interface
|
|
242
|
+
export interface Derived<V> extends TrackedCallable<V> {
|
|
245
243
|
'#v': V;
|
|
246
244
|
readonly value: V;
|
|
247
245
|
/** A derived is already read-only: returns itself. */
|
|
248
246
|
readOnly(): Derived<V>;
|
|
249
247
|
}
|
|
250
|
-
export type Derived<V> = DerivedBase<V> & TrackedCallable<V>;
|
|
251
248
|
// A computed value created with a setter (`track(fn, get, set)`) or with
|
|
252
249
|
// `true` in the setter position: writes land as a temporary value until the
|
|
253
250
|
// next recompute.
|
|
254
|
-
export
|
|
251
|
+
export interface WritableDerived<V> extends Tracked<V> {
|
|
252
|
+
value: V;
|
|
253
|
+
}
|
|
255
254
|
|
|
256
255
|
// Helper type to infer component type from a function that returns a component
|
|
257
256
|
// If T is a function returning a Component, extract the Component type itself, not the return type (void)
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
-
|
|
3
|
-
exports[`compiler > assignments > compiles tracked values in effect with assignment expression 1`] = `"state.count = lazy.value;"`;
|
|
4
|
-
|
|
5
|
-
exports[`compiler > assignments > compiles tracked values in effect with update expressions 1`] = `
|
|
6
|
-
"_$_.untrack(() => {
|
|
7
|
-
state.preIncrement = _$_.update_pre(lazy);
|
|
8
|
-
state.postIncrement = _$_.update(lazy);
|
|
9
|
-
state.preDecrement = _$_.update_pre(lazy, -1);
|
|
10
|
-
state.postDecrement = _$_.update(lazy, -1);
|
|
11
|
-
});"
|
|
12
|
-
`;
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
-
|
|
3
|
-
exports[`compiler > typescript > compiles TSInstantiationExpression 1`] = `
|
|
4
|
-
"import * as _$_ from 'ripple/internal/client';
|
|
5
|
-
|
|
6
|
-
function makeBox(value) {
|
|
7
|
-
return { value };
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const makeStringBox = (makeBox);
|
|
11
|
-
const stringBox = makeStringBox('abc');
|
|
12
|
-
const ErrorMap = (Map);
|
|
13
|
-
const errorMap = new ErrorMap();"
|
|
14
|
-
`;
|
|
15
|
-
|
|
16
|
-
exports[`compiler > typescript > removes class TypeScript syntax from JS output 1`] = `
|
|
17
|
-
"import * as _$_ from 'ripple/internal/client';
|
|
18
|
-
|
|
19
|
-
class PrintEvent {
|
|
20
|
-
text;
|
|
21
|
-
|
|
22
|
-
constructor(text) {
|
|
23
|
-
this.text = text;
|
|
24
|
-
}
|
|
25
|
-
}"
|
|
26
|
-
`;
|
|
27
|
-
|
|
28
|
-
exports[`compiler > typescript > removes class extends type arguments from JS output 1`] = `
|
|
29
|
-
"import * as _$_ from 'ripple/internal/client';
|
|
30
|
-
|
|
31
|
-
class StringMap extends Map {
|
|
32
|
-
constructor() {
|
|
33
|
-
var __block = _$_.scope();
|
|
34
|
-
|
|
35
|
-
super();
|
|
36
|
-
}
|
|
37
|
-
}"
|
|
38
|
-
`;
|
|
39
|
-
|
|
40
|
-
exports[`compiler > typescript > removes type assertions from function parameters and leaves default values 1`] = `
|
|
41
|
-
"import * as _$_ from 'ripple/internal/client';
|
|
42
|
-
|
|
43
|
-
function getString(e = 'test') {
|
|
44
|
-
return e;
|
|
45
|
-
}"
|
|
46
|
-
`;
|