ripple 0.4.2 → 0.4.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.
Files changed (98) hide show
  1. package/CHANGELOG.md +164 -0
  2. package/package.json +10 -4
  3. package/src/constants.js +6 -0
  4. package/src/jsx-runtime.d.ts +2 -1
  5. package/src/runtime/array.js +15 -9
  6. package/src/runtime/index-client.js +25 -7
  7. package/src/runtime/index-server.js +1 -0
  8. package/src/runtime/internal/client/attributes.js +439 -0
  9. package/src/runtime/internal/client/blocks.js +20 -20
  10. package/src/runtime/internal/client/component.js +2 -5
  11. package/src/runtime/internal/client/composite.js +295 -95
  12. package/src/runtime/internal/client/constants.js +14 -6
  13. package/src/runtime/internal/client/context.js +3 -2
  14. package/src/runtime/internal/client/css.js +11 -26
  15. package/src/runtime/internal/client/errors.js +188 -0
  16. package/src/runtime/internal/client/events.js +79 -34
  17. package/src/runtime/internal/client/expression.js +25 -120
  18. package/src/runtime/internal/client/for.js +113 -262
  19. package/src/runtime/internal/client/head.js +4 -3
  20. package/src/runtime/internal/client/html.js +5 -3
  21. package/src/runtime/internal/client/hydrate.js +484 -0
  22. package/src/runtime/internal/client/hydration-enabled.js +7 -0
  23. package/src/runtime/internal/client/hydration.js +16 -81
  24. package/src/runtime/internal/client/if.js +69 -10
  25. package/src/runtime/internal/client/index.js +18 -10
  26. package/src/runtime/internal/client/operations.js +11 -7
  27. package/src/runtime/internal/client/portal.js +3 -2
  28. package/src/runtime/internal/client/render.js +42 -325
  29. package/src/runtime/internal/client/root-boundary-enabled.js +7 -0
  30. package/src/runtime/internal/client/runtime.js +157 -371
  31. package/src/runtime/internal/client/template-ns.js +88 -0
  32. package/src/runtime/internal/client/template.js +118 -139
  33. package/src/runtime/internal/client/track-async.js +356 -0
  34. package/src/runtime/internal/client/try.js +133 -319
  35. package/src/runtime/internal/client/types.d.ts +48 -24
  36. package/src/runtime/internal/server/index.js +31 -29
  37. package/src/runtime/internal/server/prerender.js +27 -0
  38. package/src/server/index.js +1 -0
  39. package/src/utils/class-name.js +28 -0
  40. package/tests/client/basic/basic.errors.test.tsrx +3 -3
  41. package/tests/client/compiler/__snapshots__/compiler.typescript.test.tsrx.snap +0 -2
  42. package/tests/client/compiler/compiler.basic.test.tsrx +2 -2
  43. package/tests/client/compiler/compiler.tracked-access.test.tsrx +3 -3
  44. package/tests/client/composite/composite.dynamic-components.test.tsrx +40 -0
  45. package/tests/client/composite/composite.props.test.tsrx +10 -10
  46. package/tests/client/css/scoped-styles.test.tsrx +3 -1
  47. package/tests/client/derived-release.test.tsrx +3 -3
  48. package/tests/client/for-item.test.tsrx +39 -0
  49. package/tests/client/for-selector.test.tsrx +95 -0
  50. package/tests/client/svg.test.tsrx +273 -1
  51. package/tests/hydration/compiled/client/basic.js +88 -87
  52. package/tests/hydration/compiled/client/composite.js +15 -6
  53. package/tests/hydration/compiled/client/events.js +40 -48
  54. package/tests/hydration/compiled/client/for.js +191 -199
  55. package/tests/hydration/compiled/client/fragment-cursor.js +633 -334
  56. package/tests/hydration/compiled/client/head.js +22 -22
  57. package/tests/hydration/compiled/client/hmr.js +14 -5
  58. package/tests/hydration/compiled/client/html-in-template.js +5 -5
  59. package/tests/hydration/compiled/client/html.js +195 -177
  60. package/tests/hydration/compiled/client/if-children.js +136 -63
  61. package/tests/hydration/compiled/client/if-fragment-controlflow.js +63 -43
  62. package/tests/hydration/compiled/client/if.js +78 -30
  63. package/tests/hydration/compiled/client/mixed-control-flow.js +90 -72
  64. package/tests/hydration/compiled/client/nested-control-flow.js +266 -307
  65. package/tests/hydration/compiled/client/portal.js +20 -11
  66. package/tests/hydration/compiled/client/reactivity.js +29 -29
  67. package/tests/hydration/compiled/client/return.js +2 -2
  68. package/tests/hydration/compiled/client/streaming.js +33 -35
  69. package/tests/hydration/compiled/client/switch.js +25 -25
  70. package/tests/hydration/compiled/client/track-async-serialization.js +41 -41
  71. package/tests/hydration/compiled/client/try.js +15 -15
  72. package/tests/hydration/compiled/client/typed-text.js +8 -7
  73. package/tests/hydration/compiled/server/basic.js +6 -6
  74. package/tests/hydration/compiled/server/events.js +8 -8
  75. package/tests/hydration/compiled/server/for.js +22 -22
  76. package/tests/hydration/compiled/server/fragment-cursor.js +188 -52
  77. package/tests/hydration/compiled/server/head.js +6 -6
  78. package/tests/hydration/compiled/server/hmr.js +1 -1
  79. package/tests/hydration/compiled/server/html.js +1 -1
  80. package/tests/hydration/compiled/server/if-children.js +9 -9
  81. package/tests/hydration/compiled/server/if.js +5 -5
  82. package/tests/hydration/compiled/server/mixed-control-flow.js +4 -4
  83. package/tests/hydration/compiled/server/portal.js +1 -1
  84. package/tests/hydration/compiled/server/reactivity.js +9 -9
  85. package/tests/hydration/compiled/server/streaming.js +9 -9
  86. package/tests/hydration/compiled/server/switch.js +4 -4
  87. package/tests/hydration/compiled/server/track-async-serialization.js +16 -16
  88. package/tests/hydration/compiled/server/try.js +4 -4
  89. package/tests/hydration/compiled/server/typed-text.js +1 -1
  90. package/tests/hydration/components/fragment-cursor.tsrx +60 -0
  91. package/tests/hydration/components/track-async-serialization.tsrx +2 -2
  92. package/tests/hydration/fragment-cursor.test.js +34 -1
  93. package/tests/hydration/track-async-serialization.test.js +1 -1
  94. package/tests/server/basic.components.test.tsrx +3 -3
  95. package/tests/server/track-async-serialization.test.tsrx +6 -6
  96. package/tests/utils/tracked-types.test.js +3 -3
  97. package/types/index.d.ts +8 -8
  98. package/types/server.d.ts +17 -0
@@ -1,7 +1,279 @@
1
- import { track } from 'ripple';
1
+ import { track, flushSync } from 'ripple';
2
2
  import type { Component, PropsWithChildren, PropsWithExtras } from 'ripple';
3
3
 
4
+ const SVG_NS = 'http://www.w3.org/2000/svg';
5
+ const HTML_NS = 'http://www.w3.org/1999/xhtml';
6
+
7
+ // The namespace is only known at runtime here: the component's children are
8
+ // compiled without it and rendered into an `<svg>` the component owns, so
9
+ // every block must keep the namespace it was created in when it reruns.
10
+ describe('SVG namespace in runtime-only contexts', () => {
11
+ function SVG({ children }: PropsWithChildren<{}>) @{
12
+ <svg>{children}</svg>
13
+ }
14
+
15
+ it('parses a nested template in the SVG namespace', () => {
16
+ function App() @{
17
+ <SVG>
18
+ <g class="group">
19
+ <circle r="1" />
20
+ </g>
21
+ </SVG>
22
+ }
23
+
24
+ render(App);
25
+ expect(container.querySelector('.group').namespaceURI).toBe(SVG_NS);
26
+ expect(container.querySelector('circle').namespaceURI).toBe(SVG_NS);
27
+ });
28
+
29
+ it('keeps the namespace when an @if in component children toggles', () => {
30
+ let setFlag: ((value: boolean) => void) | undefined;
31
+
32
+ function App() @{
33
+ const flag = track(true);
34
+ setFlag = (value) => {
35
+ flag.value = value;
36
+ };
37
+ <SVG>
38
+ @if (flag.value) {
39
+ <polygon class="a" points="0,0 1,1" />
40
+ } @else {
41
+ <g class="b">
42
+ <rect width="1" height="1" />
43
+ </g>
44
+ }
45
+ </SVG>
46
+ }
47
+
48
+ render(App);
49
+ expect(container.querySelector('.a').namespaceURI).toBe(SVG_NS);
50
+ flushSync(() => setFlag?.(false));
51
+ expect(container.querySelector('.b').namespaceURI).toBe(SVG_NS);
52
+ expect(container.querySelector('rect').namespaceURI).toBe(SVG_NS);
53
+ flushSync(() => setFlag?.(true));
54
+ expect(container.querySelector('.a').namespaceURI).toBe(SVG_NS);
55
+ });
56
+
57
+ it('keeps the namespace when an @if in a leaf component toggles', () => {
58
+ let setFlag: ((value: boolean) => void) | undefined;
59
+
60
+ function Shape() @{
61
+ const flag = track(true);
62
+ setFlag = (value) => {
63
+ flag.value = value;
64
+ };
65
+ @if (flag.value) {
66
+ <polygon class="a" points="0,0 1,1" />
67
+ } @else {
68
+ <rect class="b" width="1" height="1" />
69
+ }
70
+ }
71
+
72
+ function App() @{
73
+ <svg>
74
+ <Shape />
75
+ </svg>
76
+ }
77
+
78
+ render(App);
79
+ expect(container.querySelector('.a').namespaceURI).toBe(SVG_NS);
80
+ flushSync(() => setFlag?.(false));
81
+ expect(container.querySelector('.b').namespaceURI).toBe(SVG_NS);
82
+ });
83
+
84
+ it('keeps the namespace when a dynamic tag in component children changes', () => {
85
+ let setTag: ((value: string) => void) | undefined;
86
+
87
+ function App() @{
88
+ const tag = track('polygon');
89
+ setTag = (value) => {
90
+ tag.value = value;
91
+ };
92
+ <SVG>
93
+ <{tag.value} class="shape" />
94
+ </SVG>
95
+ }
96
+
97
+ render(App);
98
+ expect(container.querySelector('.shape').namespaceURI).toBe(SVG_NS);
99
+ flushSync(() => setTag?.('rect'));
100
+ const shape = container.querySelector('.shape');
101
+ expect(shape.localName).toBe('rect');
102
+ expect(shape.namespaceURI).toBe(SVG_NS);
103
+ });
104
+
105
+ it('keeps the namespace for items added to an @for in component children', () => {
106
+ let setItems: ((value: string[]) => void) | undefined;
107
+
108
+ function App() @{
109
+ const items = track(['a']);
110
+ setItems = (value) => {
111
+ items.value = value;
112
+ };
113
+ <SVG>
114
+ @for (const id of items.value) {
115
+ <circle data-id={id} r="1" />
116
+ }
117
+ </SVG>
118
+ }
119
+
120
+ render(App);
121
+ expect(container.querySelector('[data-id=a]').namespaceURI).toBe(SVG_NS);
122
+ flushSync(() => setItems?.(['a', 'b']));
123
+ expect(container.querySelector('[data-id=b]').namespaceURI).toBe(SVG_NS);
124
+ });
125
+
126
+ it('renders HTML inside a foreignObject in component children', () => {
127
+ let setTag: ((value: string) => void) | undefined;
128
+
129
+ function App() @{
130
+ const tag = track('div');
131
+ setTag = (value) => {
132
+ tag.value = value;
133
+ };
134
+ <SVG>
135
+ <foreignObject>
136
+ @if (tag.value !== 'none') {
137
+ <div class="static-label">
138
+ <span>hi</span>
139
+ </div>
140
+ }
141
+ <{tag.value} class="html-label">Label</{tag.value}>
142
+ </foreignObject>
143
+ <circle class="after" r="1" />
144
+ </SVG>
145
+ }
146
+
147
+ render(App);
148
+ expect(container.querySelector('.static-label').namespaceURI).toBe(HTML_NS);
149
+ expect(container.querySelector('span').namespaceURI).toBe(HTML_NS);
150
+ expect(container.querySelector('.html-label').namespaceURI).toBe(HTML_NS);
151
+ expect(container.querySelector('.after').namespaceURI).toBe(SVG_NS);
152
+ flushSync(() => setTag?.('span'));
153
+ const label = container.querySelector('.html-label');
154
+ expect(label.localName).toBe('span');
155
+ expect(label.namespaceURI).toBe(HTML_NS);
156
+ });
157
+
158
+ it('sets a dynamic class on SVG elements in component children', () => {
159
+ let setName: ((value: string) => void) | undefined;
160
+
161
+ function App() @{
162
+ const name = track('one');
163
+ setName = (value) => {
164
+ name.value = value;
165
+ };
166
+ <SVG>
167
+ <circle class={name.value} r="1" />
168
+ <a class={name.value} href="#link">
169
+ <text>link</text>
170
+ </a>
171
+ </SVG>
172
+ }
173
+
174
+ render(App);
175
+ expect(container.querySelector('circle').getAttribute('class')).toBe('one');
176
+ expect(container.querySelector('a').getAttribute('class')).toBe('one');
177
+ flushSync(() => setName?.('two'));
178
+ expect(container.querySelector('circle').getAttribute('class')).toBe('two');
179
+ expect(container.querySelector('a').getAttribute('class')).toBe('two');
180
+ });
181
+ });
182
+
183
+ describe('dynamic element namespace changes', () => {
184
+ it('switches a root between HTML and SVG with its children', () => {
185
+ let setTag: ((value: string) => void) | undefined;
186
+
187
+ function App() @{
188
+ const tag = track('div');
189
+ setTag = (value) => {
190
+ tag.value = value;
191
+ };
192
+ <{tag.value} class="root">
193
+ <g class="inner">
194
+ <title>t</title>
195
+ </g>
196
+ </{tag.value}>
197
+ }
198
+
199
+ render(App);
200
+ expect(container.querySelector('.root').namespaceURI).toBe(HTML_NS);
201
+ expect(container.querySelector('.inner').namespaceURI).toBe(HTML_NS);
202
+ flushSync(() => setTag?.('svg'));
203
+ expect(container.querySelector('.root').namespaceURI).toBe(SVG_NS);
204
+ expect(container.querySelector('.inner').namespaceURI).toBe(SVG_NS);
205
+ expect(container.querySelector('title').namespaceURI).toBe(SVG_NS);
206
+ flushSync(() => setTag?.('div'));
207
+ expect(container.querySelector('.root').namespaceURI).toBe(HTML_NS);
208
+ expect(container.querySelector('.inner').namespaceURI).toBe(HTML_NS);
209
+ });
210
+
211
+ it('renders HTML children once the tag becomes foreignObject', () => {
212
+ let setTag: ((value: string) => void) | undefined;
213
+
214
+ function App() @{
215
+ const tag = track('g');
216
+ setTag = (value) => {
217
+ tag.value = value;
218
+ };
219
+ <svg>
220
+ <{tag.value} class="host">
221
+ <a class="content">hi</a>
222
+ </{tag.value}>
223
+ </svg>
224
+ }
225
+
226
+ render(App);
227
+ expect(container.querySelector('.host').namespaceURI).toBe(SVG_NS);
228
+ expect(container.querySelector('.content').namespaceURI).toBe(SVG_NS);
229
+ flushSync(() => setTag?.('foreignObject'));
230
+ const host = container.querySelector('.host');
231
+ expect(host.localName).toBe('foreignObject');
232
+ expect(host.namespaceURI).toBe(SVG_NS);
233
+ expect(container.querySelector('.content').namespaceURI).toBe(HTML_NS);
234
+ flushSync(() => setTag?.('g'));
235
+ expect(container.querySelector('.content').namespaceURI).toBe(SVG_NS);
236
+ });
237
+ });
238
+
4
239
  describe('SVG namespace handling', () => {
240
+ it('preserves SVG and foreignObject namespaces when dynamic tags change', () => {
241
+ let setTags: ((svgTag: string, htmlTag: string) => void) | undefined;
242
+
243
+ function App() @{
244
+ const shape = track('circle');
245
+ const html = track('div');
246
+ setTags = (svgTag, htmlTag) => {
247
+ shape.value = svgTag;
248
+ html.value = htmlTag;
249
+ };
250
+ <svg>
251
+ <{shape.value} class="shape">
252
+ <title>Dynamic shape</title>
253
+ </{shape.value}>
254
+ <foreignObject>
255
+ <{html.value} class="html-label">Label</{html.value}>
256
+ </foreignObject>
257
+ </svg>
258
+ }
259
+
260
+ render(App);
261
+ for (const [svgTag, htmlTag] of [['circle', 'div'], ['path', 'span'], ['line', 'div']]) {
262
+ flushSync(() => {
263
+ setTags?.(svgTag, htmlTag);
264
+ });
265
+ const element = container.querySelector('.shape');
266
+ expect(element.localName).toBe(svgTag);
267
+ expect(element.namespaceURI).toBe('http://www.w3.org/2000/svg');
268
+ expect(element.querySelector('title').namespaceURI).toBe('http://www.w3.org/2000/svg');
269
+ expect(element.textContent).toBe('Dynamic shape');
270
+ const label = container.querySelector('.html-label');
271
+ expect(label.localName).toBe(htmlTag);
272
+ expect(label.namespaceURI).toBe('http://www.w3.org/1999/xhtml');
273
+ expect(label.textContent).toBe('Label');
274
+ }
275
+ });
276
+
5
277
  it('should render static SVG elements with correct namespace', () => {
6
278
  function App() @{
7
279
  <svg