ripple 0.4.4 → 0.4.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.
Files changed (27) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/package.json +2 -2
  3. package/src/runtime/internal/client/attributes.js +8 -1
  4. package/src/runtime/internal/client/events.js +15 -1
  5. package/src/runtime/internal/client/index.js +2 -0
  6. package/src/runtime/internal/client/operations.js +9 -4
  7. package/src/runtime/internal/client/render.js +37 -0
  8. package/src/runtime/internal/client/runtime.js +21 -9
  9. package/src/runtime/internal/client/template.js +69 -22
  10. package/tests/hydration/compiled/client/basic.js +43 -16
  11. package/tests/hydration/compiled/client/composite.js +2 -2
  12. package/tests/hydration/compiled/client/events.js +21 -6
  13. package/tests/hydration/compiled/client/for.js +37 -9
  14. package/tests/hydration/compiled/client/fragment-cursor.js +160 -41
  15. package/tests/hydration/compiled/client/head.js +3 -3
  16. package/tests/hydration/compiled/client/hmr.js +5 -2
  17. package/tests/hydration/compiled/client/html.js +66 -37
  18. package/tests/hydration/compiled/client/if-children.js +24 -7
  19. package/tests/hydration/compiled/client/if-fragment-controlflow.js +7 -7
  20. package/tests/hydration/compiled/client/if.js +5 -1
  21. package/tests/hydration/compiled/client/nested-control-flow.js +3 -3
  22. package/tests/hydration/compiled/client/portal.js +15 -4
  23. package/tests/hydration/compiled/client/reactivity.js +6 -2
  24. package/tests/hydration/compiled/client/streaming.js +4 -1
  25. package/tests/hydration/compiled/client/track-async-serialization.js +10 -2
  26. package/tests/hydration/compiled/client/try.js +4 -1
  27. package/tests/utils/inline-drift.test.js +83 -0
@@ -0,0 +1,83 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ import ts from 'typescript';
4
+ import { describe, expect, it } from 'vitest';
5
+
6
+ // A helper inlined on a cold path must stay the helper: each inlined
7
+ // expression is compared structurally with the helper's return expression,
8
+ // parameters substituted, so a change to one side fails here until the
9
+ // other follows.
10
+
11
+ /** @param {string} file */
12
+ function parse(file) {
13
+ const path = fileURLToPath(new URL(`../../src/runtime/internal/client/${file}`, import.meta.url));
14
+ return ts.createSourceFile(path, readFileSync(path, 'utf8'), ts.ScriptTarget.Latest, true);
15
+ }
16
+
17
+ /**
18
+ * @param {ts.SourceFile} source
19
+ * @param {string} name
20
+ * @returns {ts.FunctionDeclaration}
21
+ */
22
+ function fn(source, name) {
23
+ const found = source.statements.find(
24
+ (statement) => ts.isFunctionDeclaration(statement) && statement.name?.text === name,
25
+ );
26
+ if (!found) throw new Error(`No function ${name}`);
27
+ return /** @type {ts.FunctionDeclaration} */ (found);
28
+ }
29
+
30
+ /** @param {ts.FunctionDeclaration} declaration */
31
+ function returned(declaration) {
32
+ const statement = declaration.body?.statements.find(ts.isReturnStatement);
33
+ if (!statement?.expression) throw new Error(`${declaration.name?.text} returns nothing`);
34
+ return statement.expression;
35
+ }
36
+
37
+ /**
38
+ * The structure of an expression as data: node kinds, identifiers and
39
+ * literal texts, without positions, parentheses or type casts.
40
+ * @param {ts.Node} node
41
+ * @param {Record<string, unknown>} [substitute] a shape per parameter name
42
+ * @returns {unknown}
43
+ */
44
+ function shape(node, substitute = {}) {
45
+ while (ts.isParenthesizedExpression(node) || ts.isAsExpression(node)) node = node.expression;
46
+ if (ts.isIdentifier(node)) {
47
+ return node.text in substitute ? substitute[node.text] : { id: node.text };
48
+ }
49
+ if (ts.isStringLiteral(node) || ts.isNumericLiteral(node)) return { literal: node.text };
50
+ /** @type {unknown[]} */
51
+ const children = [];
52
+ node.forEachChild((child) => {
53
+ children.push(shape(child, substitute));
54
+ });
55
+ return { kind: ts.SyntaxKind[node.kind], children };
56
+ }
57
+
58
+ describe('inlined helpers match their functions', () => {
59
+ it('track() inlines is_ripple_object()', () => {
60
+ const helper = returned(fn(parse('utils.js'), 'is_ripple_object'));
61
+ const track = fn(parse('runtime.js'), 'track');
62
+ const first = track.body?.statements[0];
63
+ if (!first || !ts.isIfStatement(first))
64
+ throw new Error('track() no longer starts with the check');
65
+ expect(shape(first.expression)).toEqual(shape(helper));
66
+ });
67
+
68
+ it('create_deferred_effects() inlines effect()', () => {
69
+ const helper = returned(fn(parse('blocks.js'), 'effect'));
70
+ const creator = fn(parse('runtime.js'), 'create_deferred_effects');
71
+ /** @type {ts.CallExpression | undefined} */
72
+ let inlined;
73
+ const visit = (/** @type {ts.Node} */ node) => {
74
+ if (ts.isCallExpression(node) && node.expression.getText() === 'block') inlined = node;
75
+ node.forEachChild(visit);
76
+ };
77
+ visit(creator);
78
+ if (!inlined) throw new Error('create_deferred_effects() no longer creates the block itself');
79
+ // The helper's `fn` stands for whatever the inlined call passes.
80
+ const fn_argument = shape(inlined.arguments[inlined.arguments.length - 1]);
81
+ expect(shape(inlined)).toEqual(shape(helper, { fn: fn_argument }));
82
+ });
83
+ });