ripple 0.2.75 → 0.2.77

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 (29) hide show
  1. package/package.json +8 -1
  2. package/src/compiler/index.js +29 -12
  3. package/src/compiler/phases/1-parse/index.js +0 -5
  4. package/src/compiler/phases/3-transform/client/index.js +1667 -0
  5. package/src/compiler/phases/3-transform/server/index.js +240 -0
  6. package/src/compiler/utils.js +60 -0
  7. package/src/runtime/index.js +6 -0
  8. package/src/runtime/internal/client/for.js +3 -4
  9. package/src/runtime/internal/client/runtime.js +4 -0
  10. package/src/runtime/internal/server/index.js +70 -0
  11. package/src/server/index.js +1 -0
  12. package/src/utils/escaping.js +26 -0
  13. package/tests/{__snapshots__ → client/__snapshots__}/basic.test.ripple.snap +9 -0
  14. package/tests/{array.test.ripple → client/array.test.ripple} +1 -1
  15. package/tests/{basic.test.ripple → client/basic.test.ripple} +13 -0
  16. package/types/server.d.ts +0 -0
  17. package/src/compiler/phases/3-transform/index.js +0 -1721
  18. /package/tests/{__snapshots__ → client/__snapshots__}/composite.test.ripple.snap +0 -0
  19. /package/tests/{__snapshots__ → client/__snapshots__}/for.test.ripple.snap +0 -0
  20. /package/tests/{accessors-props.test.ripple → client/accessors-props.test.ripple} +0 -0
  21. /package/tests/{boundaries.test.ripple → client/boundaries.test.ripple} +0 -0
  22. /package/tests/{compiler.test.ripple → client/compiler.test.ripple} +0 -0
  23. /package/tests/{composite.test.ripple → client/composite.test.ripple} +0 -0
  24. /package/tests/{context.test.ripple → client/context.test.ripple} +0 -0
  25. /package/tests/{for.test.ripple → client/for.test.ripple} +0 -0
  26. /package/tests/{map.test.ripple → client/map.test.ripple} +0 -0
  27. /package/tests/{ref.test.ripple → client/ref.test.ripple} +0 -0
  28. /package/tests/{set.test.ripple → client/set.test.ripple} +0 -0
  29. /package/tests/{svg.test.ripple → client/svg.test.ripple} +0 -0
@@ -0,0 +1,240 @@
1
+ import * as b from '../../../../utils/builders.js';
2
+ import { walk } from 'zimmerframe';
3
+ import ts from 'esrap/languages/ts';
4
+ import path from 'node:path';
5
+ import { print } from 'esrap';
6
+ import {
7
+ build_getter,
8
+ is_element_dom_element,
9
+ is_inside_component,
10
+ is_void_element,
11
+ normalize_children,
12
+ } from '../../../utils.js';
13
+ import is_reference from 'is-reference';
14
+ import { escape } from '../../../../utils/escaping.js';
15
+
16
+ function add_ripple_internal_import(context) {
17
+ if (!context.state.to_ts) {
18
+ if (!context.state.imports.has(`import * as _$_ from 'ripple/internal/server'`)) {
19
+ context.state.imports.add(`import * as _$_ from 'ripple/internal/server'`);
20
+ }
21
+ }
22
+ }
23
+
24
+ function transform_children(children, context) {
25
+ const { visit, state, root } = context;
26
+ const normalized = normalize_children(children);
27
+
28
+ for (const node of normalized) {
29
+ if (
30
+ node.type === 'VariableDeclaration' ||
31
+ node.type === 'ExpressionStatement' ||
32
+ node.type === 'ThrowStatement' ||
33
+ node.type === 'FunctionDeclaration' ||
34
+ node.type === 'DebuggerStatement' ||
35
+ node.type === 'ClassDeclaration' ||
36
+ node.type === 'TSTypeAliasDeclaration' ||
37
+ node.type === 'TSInterfaceDeclaration' ||
38
+ node.type === 'Component'
39
+ ) {
40
+ const metadata = { await: false };
41
+ state.init.push(visit(node, { ...state, metadata }));
42
+ if (metadata.await) {
43
+ state.init.push(b.if(b.call('_$_.aborted'), b.return(null)));
44
+ if (state.metadata?.await === false) {
45
+ state.metadata.await = true;
46
+ }
47
+ }
48
+ } else {
49
+ visit(node, { ...state, root: false });
50
+ }
51
+ }
52
+ }
53
+
54
+ function transform_body(body, { visit, state }) {
55
+ const body_state = {
56
+ ...state,
57
+ init: [],
58
+ metadata: state.metadata,
59
+ };
60
+
61
+ transform_children(body, { visit, state: body_state, root: true });
62
+
63
+ return body_state.init;
64
+ }
65
+
66
+ const visitors = {
67
+ _: function set_scope(node, { next, state }) {
68
+ const scope = state.scopes.get(node);
69
+
70
+ if (scope && scope !== state.scope) {
71
+ return next({ ...state, scope });
72
+ } else {
73
+ return next();
74
+ }
75
+ },
76
+
77
+ Component(node, context) {
78
+ add_ripple_internal_import(context);
79
+
80
+ const metadata = { await: false };
81
+ const body_statements = [
82
+ b.stmt(b.call('_$_.push_component')),
83
+ ...transform_body(node.body, {
84
+ ...context,
85
+ state: { ...context.state, component: node, metadata },
86
+ }),
87
+ b.stmt(b.call('_$_.pop_component')),
88
+ ];
89
+
90
+ if (node.css !== null && node.css) {
91
+ context.state.stylesheets.push(node.css);
92
+ }
93
+
94
+ return b.function(
95
+ node.id,
96
+ node.params.length > 0 ? [b.id('__output'), node.params[0]] : [b.id('__output')],
97
+ b.block([
98
+ ...(metadata.await
99
+ ? [b.stmt(b.call('_$_.async', b.thunk(b.block(body_statements), true)))]
100
+ : body_statements),
101
+ ]),
102
+ );
103
+ },
104
+
105
+ Element(node, context) {
106
+ const { state, visit } = context;
107
+
108
+ const is_dom_element = is_element_dom_element(node);
109
+
110
+ if (is_dom_element) {
111
+ const is_void = is_void_element(node.id.name);
112
+
113
+ state.init.push(
114
+ b.stmt(b.call(b.member(b.id('__output'), b.id('push')), b.literal(`<${node.id.name}`))),
115
+ );
116
+ let class_attribute = null;
117
+
118
+ for (const attr of node.attributes) {
119
+ }
120
+
121
+ if (class_attribute !== null) {
122
+ debugger;
123
+ } else if (node.metadata.scoped && state.component.css) {
124
+ debugger;
125
+ }
126
+
127
+ state.init.push(b.stmt(b.call(b.member(b.id('__output'), b.id('push')), b.literal(`>`))));
128
+
129
+ if (!is_void) {
130
+ transform_children(node.children, { visit, state: { ...state, root: false } });
131
+
132
+ state.init.push(
133
+ b.stmt(b.call(b.member(b.id('__output'), b.id('push')), b.literal(`</${node.id.name}>`))),
134
+ );
135
+ }
136
+ } else {
137
+ const props = [];
138
+ state.init.push(b.stmt(b.call(node.id.name, b.call('__output.component'), b.object(props))));
139
+ }
140
+ },
141
+
142
+ ForOfStatement(node, context) {
143
+ if (!is_inside_component(context)) {
144
+ context.next();
145
+ return;
146
+ }
147
+ const body_scope = context.state.scopes.get(node.body);
148
+
149
+ context.state.init.push(
150
+ b.for_of(
151
+ context.visit(node.left),
152
+ context.visit(node.right),
153
+ b.block(
154
+ transform_body(node.body.body, {
155
+ ...context,
156
+ state: { ...context.state, scope: body_scope },
157
+ }),
158
+ ),
159
+ ),
160
+ );
161
+ },
162
+
163
+ IfStatement(node, context) {
164
+ if (!is_inside_component(context)) {
165
+ context.next();
166
+ return;
167
+ }
168
+
169
+ // TODO: alternative (else if / else)
170
+ context.state.init.push(
171
+ b.if(
172
+ context.visit(node.test),
173
+ b.block(
174
+ transform_body(node.consequent.body, {
175
+ ...context,
176
+ state: { ...context.state, scope: context.state.scopes.get(node.consequent) },
177
+ }),
178
+ ),
179
+ ),
180
+ );
181
+ },
182
+
183
+ Identifier(node, context) {
184
+ const parent = /** @type {Node} */ (context.path.at(-1));
185
+
186
+ if (is_reference(node, parent) && node.tracked) {
187
+ add_ripple_internal_import(context);
188
+ return b.call('_$_.get', build_getter(node, context));
189
+ }
190
+ },
191
+
192
+ Text(node, { visit, state }) {
193
+ const metadata = { await: false };
194
+ const expression = visit(node.expression, { ...state, metadata });
195
+
196
+ if (expression.type === 'Literal') {
197
+ state.init.push(
198
+ b.stmt(
199
+ b.call(b.member(b.id('__output'), b.id('push')), b.literal(escape(expression.value))),
200
+ ),
201
+ );
202
+ } else {
203
+ state.init.push(
204
+ b.stmt(b.call(b.member(b.id('__output'), b.id('push')), b.call('_$_.escape', expression))),
205
+ );
206
+ }
207
+ },
208
+ };
209
+
210
+ export function transform_server(filename, source, analysis) {
211
+ const state = {
212
+ imports: new Set(),
213
+ init: null,
214
+ scope: analysis.scope,
215
+ scopes: analysis.scopes,
216
+ stylesheets: [],
217
+ };
218
+
219
+ const program = /** @type {ESTree.Program} */ (
220
+ walk(analysis.ast, { ...state, namespace: 'html' }, visitors)
221
+ );
222
+
223
+ for (const import_node of state.imports) {
224
+ program.body.unshift(b.stmt(b.id(import_node)));
225
+ }
226
+
227
+ const js = print(program, ts(), {
228
+ sourceMapContent: source,
229
+ sourceMapSource: path.basename(filename),
230
+ });
231
+
232
+ // TODO: extract css
233
+ const css = '';
234
+
235
+ return {
236
+ ast: program,
237
+ js,
238
+ css,
239
+ };
240
+ }
@@ -647,3 +647,63 @@ export function is_element_dom_element(node) {
647
647
  !node.id.tracked
648
648
  );
649
649
  }
650
+
651
+ export function normalize_children(children) {
652
+ const normalized = [];
653
+
654
+ for (const node of children) {
655
+ normalize_child(node, normalized);
656
+ }
657
+
658
+ for (let i = normalized.length - 1; i >= 0; i--) {
659
+ const child = normalized[i];
660
+ const prev_child = normalized[i - 1];
661
+
662
+ if (child.type === 'Text' && prev_child?.type === 'Text') {
663
+ if (child.expression.type === 'Literal' && prev_child.expression.type === 'Literal') {
664
+ prev_child.expression = b.literal(
665
+ prev_child.expression.value + String(child.expression.value),
666
+ );
667
+ } else {
668
+ prev_child.expression = b.binary(
669
+ '+',
670
+ prev_child.expression,
671
+ b.call('String', child.expression),
672
+ );
673
+ }
674
+ normalized.splice(i, 1);
675
+ }
676
+ }
677
+
678
+ return normalized;
679
+ }
680
+
681
+ function normalize_child(node, normalized) {
682
+ if (node.type === 'EmptyStatement') {
683
+ return;
684
+ } else if (node.type === 'Element' && node.id.type === 'Identifier' && node.id.name === 'style') {
685
+ return;
686
+ } else {
687
+ normalized.push(node);
688
+ }
689
+ }
690
+
691
+ export function build_getter(node, context) {
692
+ const state = context.state;
693
+
694
+ for (let i = context.path.length - 1; i >= 0; i -= 1) {
695
+ const binding = state.scope.get(node.name);
696
+ const transform = binding?.transform;
697
+
698
+ // don't transform the declaration itself
699
+ if (node !== binding?.node) {
700
+ const read_fn = transform?.read;
701
+
702
+ if (read_fn) {
703
+ return read_fn(node, context.state?.metadata?.spread, context.visit);
704
+ }
705
+ }
706
+ }
707
+
708
+ return node;
709
+ }
@@ -20,6 +20,12 @@ export function mount(component, options) {
20
20
  const props = options.props || {};
21
21
  const target = options.target;
22
22
  const anchor = create_anchor();
23
+
24
+ // Clear target content in case of SSR
25
+ if (target.firstChild) {
26
+ target.textContent = '';
27
+ }
28
+
23
29
  target.append(anchor);
24
30
 
25
31
  const cleanup_events = handle_root_events(target);
@@ -210,10 +210,9 @@ function reconcile(anchor, block, b, render_fn, is_controlled, is_indexed) {
210
210
  sources[j - b_start] = i + 1;
211
211
  if (fast_path_removal) {
212
212
  fast_path_removal = false;
213
- // while (a_start < i) {
214
- // debugger
215
- // destroy_block(a_blocks[a_start++]);
216
- // }
213
+ while (a_start < i) {
214
+ destroy_block(a_blocks[a_start++]);
215
+ }
217
216
  }
218
217
  if (pos > j) {
219
218
  moved = true;
@@ -762,6 +762,10 @@ export function set(tracked, value, block) {
762
762
  if (value !== old_value) {
763
763
  var tracked_block = tracked.b;
764
764
 
765
+ if (!tracked_block) {
766
+ debugger;
767
+ }
768
+
765
769
  if ((block.f & CONTAINS_TEARDOWN) !== 0) {
766
770
  if (teardown) {
767
771
  old_values.set(tracked, value);
@@ -0,0 +1,70 @@
1
+ import { DERIVED, UNINITIALIZED } from "../client/constants";
2
+ import { is_tracked_object } from "../client/utils";
3
+
4
+ export { escape } from '../../../utils/escaping.js';
5
+
6
+ class Output {
7
+ head = '';
8
+ body = '';
9
+ #parent = null;
10
+
11
+ constructor(parent) {
12
+ this.#parent = parent;
13
+ }
14
+
15
+ component() {
16
+ return new Output(this);
17
+ }
18
+
19
+ push(str) {
20
+ this.body += str;
21
+ }
22
+ }
23
+
24
+ export async function renderToString(component) {
25
+ const output = new Output(null);
26
+
27
+ // TODO add expando "async" property to component functions during SSR
28
+ if (component.async) {
29
+ await component(output, {});
30
+ } else {
31
+ component(output, {});
32
+ }
33
+
34
+ const { head, body } = output;
35
+
36
+ return { head, body };
37
+ }
38
+
39
+ export function push_component() {
40
+ // TODO
41
+ }
42
+
43
+ export function pop_component() {
44
+ // TODO
45
+ }
46
+
47
+ export async function async(fn) {
48
+ // TODO
49
+ }
50
+
51
+ function get_derived(tracked) {
52
+ let v = tracked.v;
53
+
54
+ if (v === UNINITIALIZED) {
55
+ v = tracked.fn();
56
+ tracked.v = v;
57
+ }
58
+ return v;
59
+ }
60
+
61
+ export function get(tracked) {
62
+ // reflect back the value if it's not boxed
63
+ if (!is_tracked_object(tracked)) {
64
+ return tracked;
65
+ }
66
+
67
+ return (tracked.f & DERIVED) !== 0
68
+ ? get_derived(/** @type {Derived} */ (tracked))
69
+ : tracked.v;
70
+ }
@@ -0,0 +1 @@
1
+ export { renderToString } from '../runtime/internal/server/index.js';
@@ -0,0 +1,26 @@
1
+ const ATTR_REGEX = /[&"<]/g;
2
+ const CONTENT_REGEX = /[&<]/g;
3
+
4
+ /**
5
+ * @template V
6
+ * @param {V} value
7
+ * @param {boolean} [is_attr]
8
+ */
9
+ export function escape(value, is_attr) {
10
+ const str = String(value ?? '');
11
+
12
+ const pattern = is_attr ? ATTR_REGEX : CONTENT_REGEX;
13
+ pattern.lastIndex = 0;
14
+
15
+ let escaped = '';
16
+ let last = 0;
17
+
18
+ while (pattern.test(str)) {
19
+ const i = pattern.lastIndex - 1;
20
+ const ch = str[i];
21
+ escaped += str.substring(last, i) + (ch === '&' ? '&amp;' : ch === '"' ? '&quot;' : '&lt;');
22
+ last = i + 1;
23
+ }
24
+
25
+ return escaped + str.substring(last);
26
+ }
@@ -111,3 +111,12 @@ exports[`basic > renders simple JS expression logic correctly 1`] = `
111
111
 
112
112
  </div>
113
113
  `;
114
+
115
+ exports[`basic > should handle lexical scopes correctly 1`] = `
116
+ <div>
117
+ <section>
118
+ Nested scope variable
119
+ </section>
120
+
121
+ </div>
122
+ `;
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
2
  import { mount, flushSync, effect, untrack, TrackedArray, track } from 'ripple';
3
- import { MAX_ARRAY_LENGTH } from '../src/runtime/internal/client/constants.js';
3
+ import { MAX_ARRAY_LENGTH } from '../../src/runtime/internal/client/constants.js';
4
4
 
5
5
  describe('TrackedArray', () => {
6
6
  let container;
@@ -1401,4 +1401,17 @@ describe('basic', () => {
1401
1401
  const pre = container.querySelectorAll('pre')[0];
1402
1402
  expect(pre.textContent).toBe('true');
1403
1403
  });
1404
+
1405
+ it('should handle lexical scopes correctly', () => {
1406
+ component App() {
1407
+ <section>
1408
+ let sectionData = 'Nested scope variable';
1409
+
1410
+ {sectionData}
1411
+ </section>
1412
+ }
1413
+
1414
+ render(App);
1415
+ expect(container).toMatchSnapshot();
1416
+ });
1404
1417
  });
File without changes