ripple 0.2.199 → 0.2.201
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 +5 -4
- package/src/compiler/index.d.ts +1 -5
- package/src/compiler/phases/1-parse/index.js +145 -11
- package/src/compiler/phases/2-analyze/index.js +24 -8
- package/src/compiler/phases/2-analyze/prune.js +5 -3
- package/src/compiler/phases/3-transform/client/index.js +312 -165
- package/src/compiler/phases/3-transform/segments.js +220 -70
- package/src/compiler/phases/3-transform/server/index.js +227 -77
- package/src/compiler/source-map-utils.js +74 -10
- package/src/compiler/types/index.d.ts +63 -21
- package/src/compiler/types/parse.d.ts +3 -1
- package/src/compiler/utils.js +34 -0
- package/src/helpers.d.ts +5 -0
- package/src/runtime/index-server.js +27 -47
- package/src/runtime/internal/client/composite.js +5 -0
- package/src/runtime/internal/client/events.js +1 -9
- package/src/runtime/internal/client/for.js +6 -4
- package/src/runtime/internal/client/hydration.js +2 -2
- package/src/runtime/internal/client/index.js +1 -1
- package/src/runtime/internal/client/operations.js +4 -4
- package/src/runtime/internal/client/render.js +0 -2
- package/src/runtime/internal/client/template.js +9 -1
- package/src/runtime/internal/client/types.d.ts +18 -0
- package/src/runtime/internal/client/utils.js +1 -1
- package/src/runtime/internal/server/index.js +106 -3
- package/src/utils/builders.js +25 -5
- package/tests/client/basic/basic.attributes.test.ripple +1 -1
- package/tests/client/basic/basic.components.test.ripple +47 -0
- package/tests/client/basic/basic.rendering.test.ripple +1 -1
- package/tests/client/composite/composite.props.test.ripple +49 -4
- package/tests/client/dynamic-elements.test.ripple +44 -0
- package/tests/client/switch.test.ripple +40 -0
- package/tests/client/tsconfig.json +11 -0
- package/tests/client.d.ts +5 -22
- package/tests/common.d.ts +24 -0
- package/tests/hydration/compiled/server/basic.js +109 -24
- package/tests/hydration/compiled/server/events.js +161 -72
- package/tests/hydration/compiled/server/for.js +202 -102
- package/tests/hydration/compiled/server/if.js +130 -50
- package/tests/hydration/compiled/server/reactivity.js +51 -12
- package/tests/server/__snapshots__/compiler.test.ripple.snap +11 -4
- package/tests/server/basic.attributes.test.ripple +459 -0
- package/tests/server/basic.components.test.ripple +237 -0
- package/tests/server/basic.test.ripple +25 -0
- package/tests/server/compiler.test.ripple +2 -3
- package/tests/server/composite.props.test.ripple +161 -0
- package/tests/server/dynamic-elements.test.ripple +438 -0
- package/tests/server/head.test.ripple +102 -0
- package/tests/server/switch.test.ripple +40 -0
- package/tests/server/tsconfig.json +11 -0
- package/tests/server.d.ts +7 -0
- package/tests/setup-client.js +6 -2
- package/tests/setup-server.js +16 -0
- package/types/index.d.ts +2 -2
- package/types/server.d.ts +4 -3
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.2.
|
|
6
|
+
"version": "0.2.201",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"module": "src/runtime/index-client.js",
|
|
9
9
|
"main": "src/runtime/index-client.js",
|
|
@@ -71,7 +71,8 @@
|
|
|
71
71
|
"#server": "./src/runtime/internal/server/types.d.ts",
|
|
72
72
|
"#compiler": "./src/compiler/types/index.d.ts",
|
|
73
73
|
"#parser": "./src/compiler/types/parse.d.ts",
|
|
74
|
-
"#public": "./types/index.d.ts"
|
|
74
|
+
"#public": "./types/index.d.ts",
|
|
75
|
+
"#helpers": "./src/helpers.d.ts"
|
|
75
76
|
},
|
|
76
77
|
"dependencies": {
|
|
77
78
|
"@jridgewell/sourcemap-codec": "^1.5.5",
|
|
@@ -92,10 +93,10 @@
|
|
|
92
93
|
"@types/node": "^24.3.0",
|
|
93
94
|
"@typescript-eslint/types": "^8.40.0",
|
|
94
95
|
"typescript": "^5.9.2",
|
|
95
|
-
"@volar/language-core": "~2.4.
|
|
96
|
+
"@volar/language-core": "~2.4.28",
|
|
96
97
|
"vscode-languageserver-types": "^3.17.5"
|
|
97
98
|
},
|
|
98
99
|
"peerDependencies": {
|
|
99
|
-
"ripple": "0.2.
|
|
100
|
+
"ripple": "0.2.201"
|
|
100
101
|
}
|
|
101
102
|
}
|
package/src/compiler/index.d.ts
CHANGED
|
@@ -38,11 +38,7 @@ export interface PluginActionOverrides {
|
|
|
38
38
|
/** TypeScript diagnostic codes to suppress for this mapping */
|
|
39
39
|
suppressedDiagnostics?: number[];
|
|
40
40
|
/** Custom hover documentation for this mapping, false to disable */
|
|
41
|
-
hover?:
|
|
42
|
-
| {
|
|
43
|
-
contents: string;
|
|
44
|
-
}
|
|
45
|
-
| false;
|
|
41
|
+
hover?: string | false | ((content: string) => string);
|
|
46
42
|
/** Custom definition info for this mapping, false to disable */
|
|
47
43
|
definition?:
|
|
48
44
|
| {
|
|
@@ -220,6 +220,123 @@ function RipplePlugin(config) {
|
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Override parseProperty to support component methods in object literals.
|
|
225
|
+
* Handles syntax like `{ component something() { <div /> } }`
|
|
226
|
+
* Also supports computed names: `{ component ['something']() { <div /> } }`
|
|
227
|
+
* @type {Parse.Parser['parseProperty']}
|
|
228
|
+
*/
|
|
229
|
+
parseProperty(isPattern, refDestructuringErrors) {
|
|
230
|
+
// Check if this is a component method: component name( ... ) { ... }
|
|
231
|
+
if (!isPattern && this.type === tt.name && this.value === 'component') {
|
|
232
|
+
// Look ahead to see if this is "component identifier(", "component identifier<", "component [", or "component 'string'"
|
|
233
|
+
const lookahead = this.input.slice(this.pos).match(/^\s*(?:(\w+)\s*[(<]|\[|['"])/);
|
|
234
|
+
if (lookahead) {
|
|
235
|
+
// This is a component method definition
|
|
236
|
+
const prop = /** @type {AST.Property} */ (this.startNode());
|
|
237
|
+
const isComputed = lookahead[0].trim().startsWith('[');
|
|
238
|
+
const isStringLiteral = /^['"]/.test(lookahead[0].trim());
|
|
239
|
+
|
|
240
|
+
if (isComputed) {
|
|
241
|
+
// For computed names, consume 'component'
|
|
242
|
+
// parse the key, then parse component without name
|
|
243
|
+
this.next(); // consume 'component'
|
|
244
|
+
this.next(); // consume '['
|
|
245
|
+
prop.key = this.parseExpression();
|
|
246
|
+
this.expect(tt.bracketR);
|
|
247
|
+
prop.computed = true;
|
|
248
|
+
|
|
249
|
+
// Parse component without name (skipName: true)
|
|
250
|
+
const component_node = this.parseComponent({ skipName: true });
|
|
251
|
+
/** @type {AST.RippleProperty} */ (prop).value = component_node;
|
|
252
|
+
} else if (isStringLiteral) {
|
|
253
|
+
// For string literal names, consume 'component'
|
|
254
|
+
// parse the string key, then parse component without name
|
|
255
|
+
this.next(); // consume 'component'
|
|
256
|
+
prop.key = /** @type {AST.Literal} */ (this.parseExprAtom());
|
|
257
|
+
prop.computed = false;
|
|
258
|
+
|
|
259
|
+
// Parse component without name (skipName: true)
|
|
260
|
+
const component_node = this.parseComponent({ skipName: true });
|
|
261
|
+
/** @type {AST.RippleProperty} */ (prop).value = component_node;
|
|
262
|
+
} else {
|
|
263
|
+
const component_node = this.parseComponent({ requireName: true });
|
|
264
|
+
|
|
265
|
+
prop.key = /** @type {AST.Identifier} */ (component_node.id);
|
|
266
|
+
/** @type {AST.RippleProperty} */ (prop).value = component_node;
|
|
267
|
+
prop.computed = false;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
prop.shorthand = false;
|
|
271
|
+
prop.method = true;
|
|
272
|
+
prop.kind = 'init';
|
|
273
|
+
|
|
274
|
+
return this.finishNode(prop, 'Property');
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return super.parseProperty(isPattern, refDestructuringErrors);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Override parseClassElement to support component methods in classes.
|
|
283
|
+
* Handles syntax like `class Foo { component something() { <div /> } }`
|
|
284
|
+
* Also supports computed names: `class Foo { component ['something']() { <div /> } }`
|
|
285
|
+
* @type {Parse.Parser['parseClassElement']}
|
|
286
|
+
*/
|
|
287
|
+
parseClassElement(constructorAllowsSuper) {
|
|
288
|
+
// Check if this is a component method: component name( ... ) { ... }
|
|
289
|
+
if (this.type === tt.name && this.value === 'component') {
|
|
290
|
+
// Look ahead to see if this is "component identifier(",
|
|
291
|
+
// "component identifier<", "component [", or "component 'string'"
|
|
292
|
+
const lookahead = this.input.slice(this.pos).match(/^\s*(?:(\w+)\s*[(<]|\[|['"])/);
|
|
293
|
+
if (lookahead) {
|
|
294
|
+
// This is a component method definition
|
|
295
|
+
const node = /** @type {AST.MethodDefinition} */ (this.startNode());
|
|
296
|
+
const isComputed = lookahead[0].trim().startsWith('[');
|
|
297
|
+
const isStringLiteral = /^['"]/.test(lookahead[0].trim());
|
|
298
|
+
|
|
299
|
+
if (isComputed) {
|
|
300
|
+
// For computed names, consume 'component'
|
|
301
|
+
// parse the key, then parse component without name
|
|
302
|
+
this.next(); // consume 'component'
|
|
303
|
+
this.next(); // consume '['
|
|
304
|
+
node.key = this.parseExpression();
|
|
305
|
+
this.expect(tt.bracketR);
|
|
306
|
+
node.computed = true;
|
|
307
|
+
|
|
308
|
+
// Parse component without name (skipName: true)
|
|
309
|
+
const component_node = this.parseComponent({ skipName: true });
|
|
310
|
+
/** @type {AST.RippleMethodDefinition} */ (node).value = component_node;
|
|
311
|
+
} else if (isStringLiteral) {
|
|
312
|
+
// For string literal names, consume 'component'
|
|
313
|
+
// parse the string key, then parse component without name
|
|
314
|
+
this.next(); // consume 'component'
|
|
315
|
+
node.key = /** @type {AST.Literal} */ (this.parseExprAtom());
|
|
316
|
+
node.computed = false;
|
|
317
|
+
|
|
318
|
+
// Parse component without name (skipName: true)
|
|
319
|
+
const component_node = this.parseComponent({ skipName: true });
|
|
320
|
+
/** @type {AST.RippleMethodDefinition} */ (node).value = component_node;
|
|
321
|
+
} else {
|
|
322
|
+
// Use parseComponent which handles consuming 'component', parsing name, params, and body
|
|
323
|
+
const component_node = this.parseComponent({ requireName: true });
|
|
324
|
+
|
|
325
|
+
node.key = /** @type {AST.Identifier} */ (component_node.id);
|
|
326
|
+
/** @type {AST.RippleMethodDefinition} */ (node).value = component_node;
|
|
327
|
+
node.computed = false;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
node.static = false;
|
|
331
|
+
node.kind = 'method';
|
|
332
|
+
|
|
333
|
+
return this.finishNode(node, 'MethodDefinition');
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return super.parseClassElement(constructorAllowsSuper);
|
|
338
|
+
}
|
|
339
|
+
|
|
223
340
|
/**
|
|
224
341
|
* Override parsePropertyValue to support TypeScript generic methods in object literals.
|
|
225
342
|
* By default, acorn-typescript doesn't handle `{ method<T>() {} }` syntax.
|
|
@@ -1097,15 +1214,28 @@ function RipplePlugin(config) {
|
|
|
1097
1214
|
* Parse a component - common implementation used by statements, expressions, and export defaults
|
|
1098
1215
|
* @type {Parse.Parser['parseComponent']}
|
|
1099
1216
|
*/
|
|
1100
|
-
parseComponent({
|
|
1217
|
+
parseComponent({
|
|
1218
|
+
requireName = false,
|
|
1219
|
+
isDefault = false,
|
|
1220
|
+
declareName = false,
|
|
1221
|
+
skipName = false,
|
|
1222
|
+
} = {}) {
|
|
1101
1223
|
const node = /** @type {AST.Component} */ (this.startNode());
|
|
1102
1224
|
node.type = 'Component';
|
|
1103
1225
|
node.css = null;
|
|
1104
1226
|
node.default = isDefault;
|
|
1105
|
-
|
|
1227
|
+
|
|
1228
|
+
// skipName is used for computed property names where 'component' and the key
|
|
1229
|
+
// have already been consumed before calling parseComponent
|
|
1230
|
+
if (!skipName) {
|
|
1231
|
+
this.next(); // consume 'component'
|
|
1232
|
+
}
|
|
1106
1233
|
this.enterScope(0);
|
|
1107
1234
|
|
|
1108
|
-
if (
|
|
1235
|
+
if (skipName) {
|
|
1236
|
+
// For computed names, the key is parsed separately, so id is null
|
|
1237
|
+
node.id = null;
|
|
1238
|
+
} else if (requireName) {
|
|
1109
1239
|
node.id = this.parseIdent();
|
|
1110
1240
|
if (declareName) {
|
|
1111
1241
|
this.declareName(
|
|
@@ -1349,10 +1479,17 @@ function RipplePlugin(config) {
|
|
|
1349
1479
|
*/
|
|
1350
1480
|
checkUnreserved(ref) {
|
|
1351
1481
|
if (ref.name === 'component') {
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
)
|
|
1482
|
+
// Allow 'component' when it's followed by an identifier and '(' or '<' (component method in object literal or class)
|
|
1483
|
+
// e.g., { component something() { ... } } or class Foo { component something<T>() { ... } }
|
|
1484
|
+
// Also allow computed names: { component ['name']() { ... } }
|
|
1485
|
+
// Also allow string literal names: { component 'name'() { ... } }
|
|
1486
|
+
const nextChars = this.input.slice(this.pos).match(/^\s*(?:(\w+)\s*[(<]|\[|['"])/);
|
|
1487
|
+
if (!nextChars) {
|
|
1488
|
+
this.raise(
|
|
1489
|
+
ref.start,
|
|
1490
|
+
'"component" is a Ripple keyword and cannot be used as an identifier',
|
|
1491
|
+
);
|
|
1492
|
+
}
|
|
1356
1493
|
}
|
|
1357
1494
|
return super.checkUnreserved(ref);
|
|
1358
1495
|
}
|
|
@@ -2347,15 +2484,12 @@ function RipplePlugin(config) {
|
|
|
2347
2484
|
this.type === tt.braceL &&
|
|
2348
2485
|
this.context.some((c) => c === tstc.tc_expr)
|
|
2349
2486
|
) {
|
|
2350
|
-
this.next();
|
|
2351
2487
|
const node = this.jsx_parseExpressionContainer();
|
|
2352
2488
|
// Keep JSXEmptyExpression as-is (don't convert to Text)
|
|
2353
2489
|
if (node.expression.type !== 'JSXEmptyExpression') {
|
|
2354
2490
|
/** @type {AST.TextNode} */ (/** @type {unknown} */ (node)).type = 'Text';
|
|
2355
2491
|
}
|
|
2356
|
-
|
|
2357
|
-
this.context.pop();
|
|
2358
|
-
this.context.pop();
|
|
2492
|
+
|
|
2359
2493
|
return /** @type {ESTreeJSX.JSXEmptyExpression | AST.TextNode | ESTreeJSX.JSXExpressionContainer} */ (
|
|
2360
2494
|
/** @type {unknown} */ (node)
|
|
2361
2495
|
);
|
|
@@ -844,6 +844,14 @@ const visitors = {
|
|
|
844
844
|
},
|
|
845
845
|
|
|
846
846
|
Element(node, context) {
|
|
847
|
+
if (!is_inside_component(context)) {
|
|
848
|
+
error(
|
|
849
|
+
'Elements cannot be used outside of components',
|
|
850
|
+
context.state.analysis.module.filename,
|
|
851
|
+
node,
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
|
|
847
855
|
const { state, visit, path } = context;
|
|
848
856
|
const is_dom_element = is_element_dom_element(node);
|
|
849
857
|
const attribute_names = new Set();
|
|
@@ -853,7 +861,11 @@ const visitors = {
|
|
|
853
861
|
validate_nesting(node, context);
|
|
854
862
|
|
|
855
863
|
// Store capitalized name for dynamic components/elements
|
|
856
|
-
|
|
864
|
+
// TODO: this is not quite right as the node.id could be a member expression
|
|
865
|
+
// so, we'd need to identify dynamic based on that too
|
|
866
|
+
// However, we're going to get rid of capitalization in favor of jsx()
|
|
867
|
+
// so, this will be need to be redone.
|
|
868
|
+
if (node.id.type === 'Identifier' && node.id.tracked) {
|
|
857
869
|
const source_name = node.id.name;
|
|
858
870
|
const capitalized_name = source_name.charAt(0).toUpperCase() + source_name.slice(1);
|
|
859
871
|
node.metadata.ts_name = capitalized_name;
|
|
@@ -878,7 +890,7 @@ const visitors = {
|
|
|
878
890
|
}
|
|
879
891
|
|
|
880
892
|
if (is_dom_element) {
|
|
881
|
-
if (node.id.name === 'head') {
|
|
893
|
+
if (/** @type {AST.Identifier} */ (node.id).name === 'head') {
|
|
882
894
|
// head validation
|
|
883
895
|
if (node.attributes.length > 0) {
|
|
884
896
|
// TODO: could transform attributes as something, e.g. Text Node, and avoid a fatal error
|
|
@@ -896,7 +908,7 @@ const visitors = {
|
|
|
896
908
|
return;
|
|
897
909
|
}
|
|
898
910
|
if (state.inside_head) {
|
|
899
|
-
if (node.id.name === 'title') {
|
|
911
|
+
if (/** @type {AST.Identifier} */ (node.id).name === 'title') {
|
|
900
912
|
const children = normalize_children(node.children, context);
|
|
901
913
|
|
|
902
914
|
if (children.length !== 1 || children[0].type !== 'Text') {
|
|
@@ -910,12 +922,16 @@ const visitors = {
|
|
|
910
922
|
}
|
|
911
923
|
|
|
912
924
|
// check for invalid elements in head
|
|
913
|
-
if (!valid_in_head.has(node.id.name)) {
|
|
925
|
+
if (!valid_in_head.has(/** @type {AST.Identifier} */ (node.id).name)) {
|
|
914
926
|
// TODO: could transform invalid elements as something, e.g. Text Node, and avoid a fatal error
|
|
915
|
-
error(
|
|
927
|
+
error(
|
|
928
|
+
`<${/** @type {AST.Identifier} */ (node.id).name}> cannot be used in <head>`,
|
|
929
|
+
state.analysis.module.filename,
|
|
930
|
+
node,
|
|
931
|
+
);
|
|
916
932
|
}
|
|
917
933
|
} else {
|
|
918
|
-
if (node.id.name === 'script') {
|
|
934
|
+
if (/** @type {AST.Identifier} */ (node.id).name === 'script') {
|
|
919
935
|
const err_msg = '<script> cannot be used outside of <head>.';
|
|
920
936
|
error(
|
|
921
937
|
err_msg,
|
|
@@ -935,7 +951,7 @@ const visitors = {
|
|
|
935
951
|
}
|
|
936
952
|
}
|
|
937
953
|
|
|
938
|
-
const is_void = is_void_element(node.id.name);
|
|
954
|
+
const is_void = is_void_element(/** @type {AST.Identifier} */ (node.id).name);
|
|
939
955
|
|
|
940
956
|
if (state.elements) {
|
|
941
957
|
state.elements.push(node);
|
|
@@ -975,7 +991,7 @@ const visitors = {
|
|
|
975
991
|
|
|
976
992
|
if (is_void && node.children.length > 0) {
|
|
977
993
|
error(
|
|
978
|
-
`The <${node.id.name}> element is a void element and cannot have children`,
|
|
994
|
+
`The <${/** @type {AST.Identifier} */ (node.id).name}> element is a void element and cannot have children`,
|
|
979
995
|
state.analysis.module.filename,
|
|
980
996
|
node,
|
|
981
997
|
context.state.loose ? context.state.analysis.errors : undefined,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/** @typedef {0 | 1} Direction */
|
|
4
4
|
|
|
5
5
|
import { walk } from 'zimmerframe';
|
|
6
|
-
import { is_element_dom_element } from '../../utils.js';
|
|
6
|
+
import { is_element_dom_element, is_element_dynamic } from '../../utils.js';
|
|
7
7
|
|
|
8
8
|
const regex_backslash_and_following_character = /\\(.)/g;
|
|
9
9
|
/** @type {Direction} */
|
|
@@ -350,7 +350,7 @@ function can_render_dynamic_content(element, check_classes = false) {
|
|
|
350
350
|
|
|
351
351
|
// Either a dynamic element or component (only can tell at runtime)
|
|
352
352
|
// But dynamic elements should return false ideally
|
|
353
|
-
if (/** @type {AST.Element} */ (element)
|
|
353
|
+
if (is_element_dynamic(/** @type {AST.Element} */ (element))) {
|
|
354
354
|
return true;
|
|
355
355
|
}
|
|
356
356
|
|
|
@@ -932,7 +932,9 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
|
|
|
932
932
|
}
|
|
933
933
|
|
|
934
934
|
case 'AttributeSelector': {
|
|
935
|
-
const whitelisted = whitelist_attribute_selector.get(
|
|
935
|
+
const whitelisted = whitelist_attribute_selector.get(
|
|
936
|
+
/** @type {AST.Identifier} */ (element.id).name.toLowerCase(),
|
|
937
|
+
);
|
|
936
938
|
if (
|
|
937
939
|
!whitelisted?.includes(selector.name.toLowerCase()) &&
|
|
938
940
|
!attribute_matches(
|