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.
- package/package.json +8 -1
- package/src/compiler/index.js +29 -12
- package/src/compiler/phases/1-parse/index.js +0 -5
- package/src/compiler/phases/3-transform/client/index.js +1667 -0
- package/src/compiler/phases/3-transform/server/index.js +240 -0
- package/src/compiler/utils.js +60 -0
- package/src/runtime/index.js +6 -0
- package/src/runtime/internal/client/for.js +3 -4
- package/src/runtime/internal/client/runtime.js +4 -0
- package/src/runtime/internal/server/index.js +70 -0
- package/src/server/index.js +1 -0
- package/src/utils/escaping.js +26 -0
- package/tests/{__snapshots__ → client/__snapshots__}/basic.test.ripple.snap +9 -0
- package/tests/{array.test.ripple → client/array.test.ripple} +1 -1
- package/tests/{basic.test.ripple → client/basic.test.ripple} +13 -0
- package/types/server.d.ts +0 -0
- package/src/compiler/phases/3-transform/index.js +0 -1721
- /package/tests/{__snapshots__ → client/__snapshots__}/composite.test.ripple.snap +0 -0
- /package/tests/{__snapshots__ → client/__snapshots__}/for.test.ripple.snap +0 -0
- /package/tests/{accessors-props.test.ripple → client/accessors-props.test.ripple} +0 -0
- /package/tests/{boundaries.test.ripple → client/boundaries.test.ripple} +0 -0
- /package/tests/{compiler.test.ripple → client/compiler.test.ripple} +0 -0
- /package/tests/{composite.test.ripple → client/composite.test.ripple} +0 -0
- /package/tests/{context.test.ripple → client/context.test.ripple} +0 -0
- /package/tests/{for.test.ripple → client/for.test.ripple} +0 -0
- /package/tests/{map.test.ripple → client/map.test.ripple} +0 -0
- /package/tests/{ref.test.ripple → client/ref.test.ripple} +0 -0
- /package/tests/{set.test.ripple → client/set.test.ripple} +0 -0
- /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
|
+
}
|
package/src/compiler/utils.js
CHANGED
|
@@ -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
|
+
}
|
package/src/runtime/index.js
CHANGED
|
@@ -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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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 === '&' ? '&' : ch === '"' ? '"' : '<');
|
|
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 '
|
|
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
|