ripple 0.2.189 → 0.2.191

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 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.189",
6
+ "version": "0.2.191",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -41,8 +41,8 @@
41
41
  "./compiler/internal/import": {
42
42
  "types": "./src/compiler/types/import.d.ts"
43
43
  },
44
- "./compiler/internal/import/utils": {
45
- "default": "./src/compiler/import-utils.js"
44
+ "./compiler/internal/identifier/utils": {
45
+ "default": "./src/compiler/identifier-utils.js"
46
46
  },
47
47
  "./validator": {
48
48
  "types": "./types/index.d.ts",
@@ -92,6 +92,6 @@
92
92
  "vscode-languageserver-types": "^3.17.5"
93
93
  },
94
94
  "peerDependencies": {
95
- "ripple": "0.2.189"
95
+ "ripple": "0.2.191"
96
96
  }
97
97
  }
@@ -0,0 +1,75 @@
1
+ export const IDENTIFIER_OBFUSCATION_PREFIX = '_$_';
2
+ export const STYLE_IDENTIFIER = IDENTIFIER_OBFUSCATION_PREFIX + encode_utf16_char('#') + 'style';
3
+ export const SERVER_IDENTIFIER = IDENTIFIER_OBFUSCATION_PREFIX + encode_utf16_char('#') + 'server';
4
+ export const CSS_HASH_IDENTIFIER = IDENTIFIER_OBFUSCATION_PREFIX + 'hash';
5
+
6
+ const DECODE_UTF16_REGEX = /_u([0-9a-fA-F]{4})_/g;
7
+
8
+ /**
9
+ * @param {string} char
10
+ * @returns {string}
11
+ */
12
+ function encode_utf16_char(char) {
13
+ return `_u${('0000' + char.charCodeAt(0).toString(16)).slice(-4)}_`;
14
+ }
15
+
16
+ /**
17
+ * @param {string} encoded
18
+ * @returns {string}
19
+ */
20
+ function decoded_utf16_string(encoded) {
21
+ return encoded.replace(DECODE_UTF16_REGEX, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
22
+ }
23
+
24
+ /**
25
+ * @param {string} name
26
+ * @returns {string}
27
+ */
28
+ export function obfuscate_identifier(name) {
29
+ let start = 0;
30
+ if (name[0] === name[0].toUpperCase()) {
31
+ start = 1;
32
+ }
33
+ const index = find_next_uppercase(name, start);
34
+
35
+ const first_part = name.slice(0, index);
36
+ const second_part = name.slice(index);
37
+
38
+ return (
39
+ IDENTIFIER_OBFUSCATION_PREFIX +
40
+ (second_part ? second_part + '__' + first_part : first_part + '__')
41
+ );
42
+ }
43
+
44
+ /**
45
+ * @param {string} name
46
+ * @returns {boolean}
47
+ */
48
+ export function is_identifier_obfuscated(name) {
49
+ return name.startsWith(IDENTIFIER_OBFUSCATION_PREFIX);
50
+ }
51
+
52
+ /**
53
+ * @param {string} name
54
+ * @returns {string}
55
+ */
56
+ export function deobfuscate_identifier(name) {
57
+ name = name.replaceAll(IDENTIFIER_OBFUSCATION_PREFIX, '');
58
+ const parts = name.split('__');
59
+ return decoded_utf16_string((parts[1] ? parts[1] : '') + parts[0]);
60
+ }
61
+
62
+ /**
63
+ * Finds the next uppercase character or returns name.length
64
+ * @param {string} name
65
+ * @param {number} start
66
+ * @returns {number}
67
+ */
68
+ function find_next_uppercase(name, start) {
69
+ for (let i = start; i < name.length; i++) {
70
+ if (name[i] === name[i].toUpperCase()) {
71
+ return i;
72
+ }
73
+ }
74
+ return name.length;
75
+ }
@@ -386,8 +386,27 @@ function RipplePlugin(config) {
386
386
  }
387
387
  }
388
388
 
389
+ if (this.input.slice(this.pos, this.pos + 6) === '#style') {
390
+ // Check that next char after 'style' is . (dot), [, whitespace, or EOF
391
+ const charAfter =
392
+ this.pos + 6 < this.input.length ? this.input.charCodeAt(this.pos + 6) : -1;
393
+ if (
394
+ charAfter === 46 || // . (dot)
395
+ charAfter === 91 || // [
396
+ charAfter === 32 || // space
397
+ charAfter === 9 || // tab
398
+ charAfter === 10 || // newline
399
+ charAfter === 13 || // carriage return
400
+ charAfter === -1 // EOF
401
+ ) {
402
+ // { or . or whitespace or EOF
403
+ this.pos += 6; // consume '#style'
404
+ return this.finishToken(tt.name, '#style');
405
+ }
406
+ }
407
+
389
408
  // Check if this is an invalid #Identifier pattern
390
- // Valid patterns: #[, #{, #Map(, #Map<, #Set(, #Set<, #server
409
+ // Valid patterns: #[, #{, #Map(, #Map<, #Set(, #Set<, #server, #style
391
410
  // If we see # followed by an uppercase letter that isn't Map or Set, it's an error
392
411
  // In loose mode, allow incomplete identifiers like #M, #Ma, #S, #Se for autocomplete
393
412
  if (nextChar >= 65 && nextChar <= 90) {
@@ -466,20 +485,43 @@ function RipplePlugin(config) {
466
485
  // In JSX expressions, inside parentheses, assignments, etc.
467
486
  // we want to treat @ as an identifier prefix rather than decorator
468
487
  const currentType = this.type;
469
- const inExpression =
470
- this.exprAllowed ||
471
- currentType === tt.braceL || // Inside { }
472
- currentType === tt.parenL || // Inside ( )
473
- currentType === tt.eq || // After =
474
- currentType === tt.comma || // After ,
475
- currentType === tt.colon || // After :
476
- currentType === tt.question || // After ?
477
- currentType === tt.logicalOR || // After ||
478
- currentType === tt.logicalAND || // After &&
479
- currentType === tt.dot || // After . (for member expressions like obj.@prop)
480
- currentType === tt.questionDot; // After ?. (for optional chaining like obj?.@prop)
481
-
482
- if (inExpression) {
488
+ /**
489
+ * @param {Parse.TokenType} type
490
+ * @param {Parse.Parser} parser
491
+ * @param {Parse.TokTypes} tt
492
+ * @returns {boolean}
493
+ */
494
+ function inExpression(type, parser, tt) {
495
+ return (
496
+ parser.exprAllowed ||
497
+ type === tt.braceL || // Inside { }
498
+ type === tt.parenL || // Inside ( )
499
+ type === tt.eq || // After =
500
+ type === tt.comma || // After ,
501
+ type === tt.colon || // After :
502
+ type === tt.question || // After ?
503
+ type === tt.logicalOR || // After ||
504
+ type === tt.logicalAND || // After &&
505
+ type === tt.dot || // After . (for member expressions like obj.@prop)
506
+ type === tt.questionDot // After ?. (for optional chaining like obj?.@prop)
507
+ );
508
+ }
509
+
510
+ /**
511
+ * @param {Parse.Parser} parser
512
+ * @param {Parse.TokTypes} tt
513
+ * @returns {boolean}
514
+ */
515
+ function inAwait(parser, tt) {
516
+ return currentType === tt.name &&
517
+ parser.value === 'await' &&
518
+ parser.canAwait &&
519
+ parser.preToken
520
+ ? inExpression(parser.preToken, parser, tt)
521
+ : false;
522
+ }
523
+
524
+ if (inExpression(currentType, this, tt) || inAwait(this, tt)) {
483
525
  return this.readAtIdentifier();
484
526
  }
485
527
  }
@@ -635,6 +677,12 @@ function RipplePlugin(config) {
635
677
  return /** @type {AST.ServerIdentifier} */ (this.finishNode(node, 'ServerIdentifier'));
636
678
  }
637
679
 
680
+ if (this.type === tt.name && this.value === '#style') {
681
+ const node = this.startNode();
682
+ this.next();
683
+ return /** @type {AST.StyleIdentifier} */ (this.finishNode(node, 'StyleIdentifier'));
684
+ }
685
+
638
686
  // Check if this is #Map( or #Set(
639
687
  if (this.type === tt.name && (this.value === '#Map' || this.value === '#Set')) {
640
688
  const type = this.value === '#Map' ? 'TrackedMapExpression' : 'TrackedSetExpression';
@@ -721,7 +769,7 @@ function RipplePlugin(config) {
721
769
  const node = /** @type {AST.ServerBlock} */ (this.startNode());
722
770
  this.next();
723
771
 
724
- const body = /** @type {AST.BlockStatement} */ (this.startNode());
772
+ const body = /** @type {AST.ServerBlockStatement} */ (this.startNode());
725
773
  node.body = body;
726
774
  body.body = [];
727
775
 
@@ -6,6 +6,8 @@
6
6
  AnalysisContext,
7
7
  ScopeInterface,
8
8
  Visitors,
9
+ TopScopedClasses,
10
+ StyleClasses,
9
11
  } from '#compiler';
10
12
  */
11
13
  /** @import * as AST from 'estree' */
@@ -200,6 +202,51 @@ const visitors = {
200
202
  context.state.metadata.tracking = true;
201
203
  }
202
204
 
205
+ // Track #style.className or #style['className'] references
206
+ if (node.object.type === 'StyleIdentifier') {
207
+ const component = is_inside_component(context, true);
208
+
209
+ if (!component) {
210
+ return error(
211
+ '`#style` can only be used within a component',
212
+ context.state.analysis.module.filename,
213
+ node,
214
+ );
215
+ } else {
216
+ component.metadata.styleIdentifierPresent = true;
217
+ }
218
+
219
+ /** @type {string | null} */
220
+ let className = null;
221
+
222
+ if (!node.computed && node.property.type === 'Identifier') {
223
+ // #style.test
224
+ className = node.property.name;
225
+ } else if (
226
+ node.computed &&
227
+ node.property.type === 'Literal' &&
228
+ typeof node.property.value === 'string'
229
+ ) {
230
+ // #style['test']
231
+ className = node.property.value;
232
+ } else {
233
+ // #style[expression] - dynamic, not allowed
234
+ error(
235
+ '`#style` property access must use a dot property or static string for css class name, not a dynamic expression',
236
+ context.state.analysis.module.filename,
237
+ node.property,
238
+ );
239
+ }
240
+
241
+ if (className !== null) {
242
+ context.state.metadata.styleClasses?.set(className, node.property);
243
+ }
244
+
245
+ return context.next();
246
+ } else if (node.object.type === 'ServerIdentifier') {
247
+ context.state.analysis.metadata.serverIdentifierPresent = true;
248
+ }
249
+
203
250
  if (node.object.type === 'Identifier' && !node.object.tracked) {
204
251
  const binding = context.state.scope.get(node.object.name);
205
252
 
@@ -372,7 +419,13 @@ const visitors = {
372
419
  const elements = [];
373
420
 
374
421
  // Track metadata for this component
375
- const metadata = { await: false };
422
+ const metadata = {
423
+ await: false,
424
+ styleClasses: /** @type {StyleClasses} */ (new Map()),
425
+ };
426
+
427
+ /** @type {TopScopedClasses} */
428
+ const topScopedClasses = new Map();
376
429
 
377
430
  context.next({
378
431
  ...context.state,
@@ -388,7 +441,27 @@ const visitors = {
388
441
  analyze_css(css);
389
442
 
390
443
  for (const node of elements) {
391
- prune_css(css, node);
444
+ prune_css(css, node, metadata.styleClasses, topScopedClasses);
445
+ }
446
+
447
+ if (topScopedClasses.size > 0) {
448
+ node.metadata.topScopedClasses = topScopedClasses;
449
+ }
450
+
451
+ if (metadata.styleClasses.size > 0) {
452
+ node.metadata.styleClasses = metadata.styleClasses;
453
+
454
+ if (!context.state.loose) {
455
+ for (const [className, property] of metadata.styleClasses) {
456
+ if (!topScopedClasses?.has(className)) {
457
+ return error(
458
+ `CSS class ".${className}" does not exist in ${node.id?.name ? node.id.name : "this component's"} <style> block`,
459
+ context.state.analysis.module.filename,
460
+ property,
461
+ );
462
+ }
463
+ }
464
+ }
392
465
  }
393
466
  }
394
467
 
@@ -677,13 +750,13 @@ const visitors = {
677
750
 
678
751
  // Store capitalized name for dynamic components/elements
679
752
  if (node.id.tracked) {
680
- const original_name = node.id.name;
681
- const capitalized_name = original_name.charAt(0).toUpperCase() + original_name.slice(1);
753
+ const source_name = node.id.name;
754
+ const capitalized_name = source_name.charAt(0).toUpperCase() + source_name.slice(1);
682
755
  node.metadata.ts_name = capitalized_name;
683
- node.metadata.original_name = original_name;
756
+ node.metadata.source_name = source_name;
684
757
 
685
758
  // Mark the binding as a dynamic component so we can capitalize it everywhere
686
- const binding = context.state.scope.get(original_name);
759
+ const binding = context.state.scope.get(source_name);
687
760
  if (binding) {
688
761
  if (!binding.metadata) {
689
762
  binding.metadata = {};
@@ -889,13 +962,16 @@ export function analyze(ast, filename, options = {}) {
889
962
 
890
963
  const { scope, scopes } = create_scopes(ast, scope_root, null);
891
964
 
892
- const analysis = {
965
+ const analysis = /** @type {AnalysisResult} */ ({
893
966
  module: { ast, scope, scopes, filename },
894
967
  ast,
895
968
  scope,
896
969
  scopes,
897
970
  component_metadata: [],
898
- };
971
+ metadata: {
972
+ serverIdentifierPresent: false,
973
+ },
974
+ });
899
975
 
900
976
  walk(
901
977
  ast,
@@ -1,5 +1,5 @@
1
1
  /** @import * as AST from 'estree' */
2
- /** @import { Visitors } from '#compiler' */
2
+ /** @import { Visitors, TopScopedClasses, StyleClasses } from '#compiler' */
3
3
  /** @typedef {0 | 1} Direction */
4
4
 
5
5
  import { walk } from 'zimmerframe';
@@ -15,6 +15,10 @@ const BACKWARD = 1;
15
15
  // since the code is synchronous, this is safe
16
16
  /** @type {string} */
17
17
  let css_hash;
18
+ /** @type {StyleClasses} */
19
+ let style_identifier_classes;
20
+ /** @type {TopScopedClasses} */
21
+ let top_scoped_classes;
18
22
 
19
23
  // CSS selector constants
20
24
  /**
@@ -27,6 +31,15 @@ function create_descendant_combinator(start, end) {
27
31
  }
28
32
 
29
33
  /**
34
+ * @param {AST.CSS.RelativeSelector} relative_selector
35
+ * @param {AST.CSS.ClassSelector} selector
36
+ * @returns {boolean}
37
+ */
38
+ function is_standalone_class_selector(relative_selector, selector) {
39
+ return relative_selector.selectors.length === 1 && relative_selector.selectors[0] === selector;
40
+ }
41
+
42
+ /**`
30
43
  * @param {number} start
31
44
  * @param {number} end
32
45
  * @returns {AST.CSS.RelativeSelector}
@@ -211,7 +224,6 @@ function apply_selector(relative_selectors, rule, element, direction) {
211
224
  if (!element.metadata.css) {
212
225
  element.metadata.css = {
213
226
  scopedClasses: new Map(),
214
- topScopedClasses: new Map(),
215
227
  hash: css_hash,
216
228
  };
217
229
  }
@@ -225,14 +237,12 @@ function apply_selector(relative_selectors, rule, element, direction) {
225
237
  });
226
238
  }
227
239
 
228
- // Also store in topScopedClasses if standalone
229
- // Standalone = only this ClassSelector in the RelativeSelector, no pseudo-classes/elements
230
- const isStandalone =
231
- relative_selector.selectors.length === 1 &&
232
- relative_selector.selectors[0] === selector;
233
-
234
- if (isStandalone && !element.metadata.css.topScopedClasses.has(name)) {
235
- element.metadata.css.topScopedClasses.set(name, {
240
+ // Also store in top_scoped_classes if standalone selector
241
+ if (
242
+ is_standalone_class_selector(relative_selector, selector) &&
243
+ !top_scoped_classes.has(name)
244
+ ) {
245
+ top_scoped_classes.set(name, {
236
246
  start: selector.start,
237
247
  end: selector.end,
238
248
  selector: selector,
@@ -939,7 +949,11 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
939
949
  }
940
950
 
941
951
  case 'ClassSelector': {
942
- if (!attribute_matches(element, 'class', name, '~=', false)) {
952
+ if (
953
+ !attribute_matches(element, 'class', name, '~=', false) &&
954
+ (!style_identifier_classes.has(name) ||
955
+ !is_standalone_class_selector(relative_selector, selector))
956
+ ) {
943
957
  return false;
944
958
  }
945
959
 
@@ -1047,10 +1061,14 @@ function rule_has_animation(rule) {
1047
1061
  /**
1048
1062
  * @param {AST.CSS.StyleSheet} css
1049
1063
  * @param {AST.Element} element
1064
+ * @param {StyleClasses} styleClasses
1065
+ * @param {TopScopedClasses} topScopedClasses
1050
1066
  * @return {void}
1051
1067
  */
1052
- export function prune_css(css, element) {
1068
+ export function prune_css(css, element, styleClasses, topScopedClasses) {
1053
1069
  css_hash = css.hash;
1070
+ style_identifier_classes = styleClasses;
1071
+ top_scoped_classes = topScopedClasses;
1054
1072
 
1055
1073
  /** @type {Visitors<AST.CSS.Node, null>} */
1056
1074
  const visitors = {