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.
Files changed (55) hide show
  1. package/package.json +5 -4
  2. package/src/compiler/index.d.ts +1 -5
  3. package/src/compiler/phases/1-parse/index.js +145 -11
  4. package/src/compiler/phases/2-analyze/index.js +24 -8
  5. package/src/compiler/phases/2-analyze/prune.js +5 -3
  6. package/src/compiler/phases/3-transform/client/index.js +312 -165
  7. package/src/compiler/phases/3-transform/segments.js +220 -70
  8. package/src/compiler/phases/3-transform/server/index.js +227 -77
  9. package/src/compiler/source-map-utils.js +74 -10
  10. package/src/compiler/types/index.d.ts +63 -21
  11. package/src/compiler/types/parse.d.ts +3 -1
  12. package/src/compiler/utils.js +34 -0
  13. package/src/helpers.d.ts +5 -0
  14. package/src/runtime/index-server.js +27 -47
  15. package/src/runtime/internal/client/composite.js +5 -0
  16. package/src/runtime/internal/client/events.js +1 -9
  17. package/src/runtime/internal/client/for.js +6 -4
  18. package/src/runtime/internal/client/hydration.js +2 -2
  19. package/src/runtime/internal/client/index.js +1 -1
  20. package/src/runtime/internal/client/operations.js +4 -4
  21. package/src/runtime/internal/client/render.js +0 -2
  22. package/src/runtime/internal/client/template.js +9 -1
  23. package/src/runtime/internal/client/types.d.ts +18 -0
  24. package/src/runtime/internal/client/utils.js +1 -1
  25. package/src/runtime/internal/server/index.js +106 -3
  26. package/src/utils/builders.js +25 -5
  27. package/tests/client/basic/basic.attributes.test.ripple +1 -1
  28. package/tests/client/basic/basic.components.test.ripple +47 -0
  29. package/tests/client/basic/basic.rendering.test.ripple +1 -1
  30. package/tests/client/composite/composite.props.test.ripple +49 -4
  31. package/tests/client/dynamic-elements.test.ripple +44 -0
  32. package/tests/client/switch.test.ripple +40 -0
  33. package/tests/client/tsconfig.json +11 -0
  34. package/tests/client.d.ts +5 -22
  35. package/tests/common.d.ts +24 -0
  36. package/tests/hydration/compiled/server/basic.js +109 -24
  37. package/tests/hydration/compiled/server/events.js +161 -72
  38. package/tests/hydration/compiled/server/for.js +202 -102
  39. package/tests/hydration/compiled/server/if.js +130 -50
  40. package/tests/hydration/compiled/server/reactivity.js +51 -12
  41. package/tests/server/__snapshots__/compiler.test.ripple.snap +11 -4
  42. package/tests/server/basic.attributes.test.ripple +459 -0
  43. package/tests/server/basic.components.test.ripple +237 -0
  44. package/tests/server/basic.test.ripple +25 -0
  45. package/tests/server/compiler.test.ripple +2 -3
  46. package/tests/server/composite.props.test.ripple +161 -0
  47. package/tests/server/dynamic-elements.test.ripple +438 -0
  48. package/tests/server/head.test.ripple +102 -0
  49. package/tests/server/switch.test.ripple +40 -0
  50. package/tests/server/tsconfig.json +11 -0
  51. package/tests/server.d.ts +7 -0
  52. package/tests/setup-client.js +6 -2
  53. package/tests/setup-server.js +16 -0
  54. package/types/index.d.ts +2 -2
  55. 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.199",
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.23",
96
+ "@volar/language-core": "~2.4.28",
96
97
  "vscode-languageserver-types": "^3.17.5"
97
98
  },
98
99
  "peerDependencies": {
99
- "ripple": "0.2.199"
100
+ "ripple": "0.2.201"
100
101
  }
101
102
  }
@@ -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({ requireName = false, isDefault = false, declareName = false } = {}) {
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
- this.next(); // consume 'component'
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 (requireName) {
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
- this.raise(
1353
- ref.start,
1354
- '"component" is a Ripple keyword and cannot be used as an identifier',
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
- this.next();
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
- if (node.id.tracked) {
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(`<${node.id.name}> cannot be used in <head>`, state.analysis.module.filename, node);
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).id.tracked) {
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(element.id.name.toLowerCase());
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(