ripple 0.3.13 → 0.3.14

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 (66) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/package.json +5 -30
  3. package/src/runtime/array.js +38 -38
  4. package/src/runtime/create-subscriber.js +2 -2
  5. package/src/runtime/internal/client/bindings.js +4 -6
  6. package/src/runtime/internal/client/events.js +8 -3
  7. package/src/runtime/internal/client/hmr.js +5 -17
  8. package/src/runtime/internal/client/runtime.js +1 -0
  9. package/src/runtime/internal/server/blocks.js +7 -9
  10. package/src/runtime/internal/server/index.js +14 -22
  11. package/src/runtime/media-query.js +34 -33
  12. package/src/runtime/object.js +7 -10
  13. package/src/runtime/proxy.js +2 -3
  14. package/src/runtime/reactive-value.js +23 -21
  15. package/src/utils/ast.js +1 -1
  16. package/src/utils/attributes.js +43 -0
  17. package/src/utils/builders.js +2 -2
  18. package/tests/client/basic/basic.errors.test.rsrx +1 -1
  19. package/tests/client/basic/basic.styling.test.rsrx +1 -1
  20. package/tests/client/compiler/compiler.assignments.test.rsrx +1 -1
  21. package/tests/client/compiler/compiler.attributes.test.rsrx +1 -1
  22. package/tests/client/compiler/compiler.basic.test.rsrx +13 -13
  23. package/tests/client/compiler/compiler.tracked-access.test.rsrx +1 -1
  24. package/tests/client/compiler/compiler.try-in-function.test.rsrx +1 -1
  25. package/tests/client/compiler/compiler.typescript.test.rsrx +1 -1
  26. package/tests/client/css/global-additional-cases.test.rsrx +1 -1
  27. package/tests/client/css/global-advanced-selectors.test.rsrx +1 -1
  28. package/tests/client/css/global-at-rules.test.rsrx +1 -1
  29. package/tests/client/css/global-basic.test.rsrx +1 -1
  30. package/tests/client/css/global-classes-ids.test.rsrx +1 -1
  31. package/tests/client/css/global-combinators.test.rsrx +1 -1
  32. package/tests/client/css/global-complex-nesting.test.rsrx +1 -1
  33. package/tests/client/css/global-edge-cases.test.rsrx +1 -1
  34. package/tests/client/css/global-keyframes.test.rsrx +1 -1
  35. package/tests/client/css/global-nested.test.rsrx +1 -1
  36. package/tests/client/css/global-pseudo.test.rsrx +1 -1
  37. package/tests/client/css/global-scoping.test.rsrx +1 -1
  38. package/tests/client/css/style-identifier.test.rsrx +1 -1
  39. package/tests/client/return.test.rsrx +1 -1
  40. package/tests/hydration/build-components.js +1 -1
  41. package/tests/server/style-identifier.test.rsrx +1 -1
  42. package/tests/setup-server.js +1 -1
  43. package/tests/utils/compiler-compat-config.test.js +1 -1
  44. package/src/compiler/comment-utils.js +0 -91
  45. package/src/compiler/errors.js +0 -77
  46. package/src/compiler/identifier-utils.js +0 -80
  47. package/src/compiler/index.d.ts +0 -127
  48. package/src/compiler/index.js +0 -89
  49. package/src/compiler/phases/1-parse/index.js +0 -3007
  50. package/src/compiler/phases/1-parse/style.js +0 -704
  51. package/src/compiler/phases/2-analyze/css-analyze.js +0 -160
  52. package/src/compiler/phases/2-analyze/index.js +0 -2208
  53. package/src/compiler/phases/2-analyze/prune.js +0 -1131
  54. package/src/compiler/phases/2-analyze/validation.js +0 -168
  55. package/src/compiler/phases/3-transform/client/index.js +0 -5264
  56. package/src/compiler/phases/3-transform/segments.js +0 -2125
  57. package/src/compiler/phases/3-transform/server/index.js +0 -1749
  58. package/src/compiler/phases/3-transform/stylesheet.js +0 -545
  59. package/src/compiler/scope.js +0 -476
  60. package/src/compiler/source-map-utils.js +0 -358
  61. package/src/compiler/types/acorn.d.ts +0 -11
  62. package/src/compiler/types/estree-jsx.d.ts +0 -11
  63. package/src/compiler/types/estree.d.ts +0 -11
  64. package/src/compiler/types/index.d.ts +0 -1411
  65. package/src/compiler/types/parse.d.ts +0 -1723
  66. package/src/compiler/utils.js +0 -1258
@@ -1,1131 +0,0 @@
1
- /** @import * as AST from 'estree' */
2
- /** @import { Visitors, TopScopedClasses, StyleClasses } from '#compiler' */
3
- /** @typedef {0 | 1} Direction */
4
-
5
- import { walk } from 'zimmerframe';
6
- import { is_element_dom_element, is_element_dynamic } from '../../utils.js';
7
-
8
- const regex_backslash_and_following_character = /\\(.)/g;
9
- /** @type {Direction} */
10
- const FORWARD = 0;
11
- /** @type {Direction} */
12
- const BACKWARD = 1;
13
-
14
- // this will be set for every prune_css call
15
- // since the code is synchronous, this is safe
16
- /** @type {string} */
17
- let css_hash;
18
- /** @type {StyleClasses} */
19
- let style_identifier_classes;
20
- /** @type {TopScopedClasses} */
21
- let top_scoped_classes;
22
-
23
- // CSS selector constants
24
- /**
25
- * @param {number} start
26
- * @param {number} end
27
- * @returns {AST.CSS.Combinator}
28
- */
29
- function create_descendant_combinator(start, end) {
30
- return { name: ' ', type: 'Combinator', start, end };
31
- }
32
-
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
- /**`
43
- * @param {number} start
44
- * @param {number} end
45
- * @returns {AST.CSS.RelativeSelector}
46
- */
47
- function create_nesting_selector(start, end) {
48
- return {
49
- type: 'RelativeSelector',
50
- selectors: [{ type: 'NestingSelector', name: '&', start, end }],
51
- combinator: null,
52
- metadata: { is_global: false, is_global_like: false, scoped: false },
53
- start,
54
- end,
55
- };
56
- }
57
-
58
- /**
59
- * @param {number} start
60
- * @param {number} end
61
- * @returns {AST.CSS.RelativeSelector}
62
- */
63
- function create_any_selector(start, end) {
64
- return {
65
- type: 'RelativeSelector',
66
- selectors: [{ type: 'TypeSelector', name: '*', start, end }],
67
- combinator: null,
68
- metadata: { is_global: false, is_global_like: false, scoped: false },
69
- start,
70
- end,
71
- };
72
- }
73
-
74
- // Whitelist for attribute selectors on specific elements
75
- const whitelist_attribute_selector = new Map([
76
- ['details', ['open']],
77
- ['dialog', ['open']],
78
- ['form', ['novalidate']],
79
- ['iframe', ['allow', 'allowfullscreen', 'allowpaymentrequest', 'loading', 'referrerpolicy']],
80
- ['img', ['loading']],
81
- [
82
- 'input',
83
- [
84
- 'accept',
85
- 'autocomplete',
86
- 'capture',
87
- 'checked',
88
- 'disabled',
89
- 'max',
90
- 'maxlength',
91
- 'min',
92
- 'minlength',
93
- 'multiple',
94
- 'pattern',
95
- 'placeholder',
96
- 'readonly',
97
- 'required',
98
- 'size',
99
- 'step',
100
- ],
101
- ],
102
- ['object', ['typemustmatch']],
103
- ['ol', ['reversed', 'start', 'type']],
104
- ['optgroup', ['disabled']],
105
- ['option', ['disabled', 'selected']],
106
- ['script', ['async', 'defer', 'nomodule', 'type']],
107
- ['select', ['disabled', 'multiple', 'required', 'size']],
108
- [
109
- 'textarea',
110
- [
111
- 'autocomplete',
112
- 'disabled',
113
- 'maxlength',
114
- 'minlength',
115
- 'placeholder',
116
- 'readonly',
117
- 'required',
118
- 'rows',
119
- 'wrap',
120
- ],
121
- ],
122
- ['video', ['autoplay', 'controls', 'loop', 'muted', 'playsinline']],
123
- ]);
124
-
125
- /**
126
- * @param {AST.CSS.ComplexSelector} node
127
- */
128
- function get_relative_selectors(node) {
129
- const selectors = truncate(node);
130
-
131
- if (node.metadata.rule?.metadata.parent_rule && selectors.length > 0) {
132
- let has_explicit_nesting_selector = false;
133
-
134
- // nesting could be inside pseudo classes like :is, :has or :where
135
- for (let selector of selectors) {
136
- walk(
137
- selector,
138
- null,
139
- /** @type {Visitors<AST.CSS.Node, null>} */ ({
140
- NestingSelector() {
141
- has_explicit_nesting_selector = true;
142
- },
143
- }),
144
- );
145
-
146
- // if we found one we can break from the others
147
- if (has_explicit_nesting_selector) break;
148
- }
149
-
150
- if (!has_explicit_nesting_selector) {
151
- if (selectors[0].combinator === null) {
152
- selectors[0] = {
153
- ...selectors[0],
154
- combinator: create_descendant_combinator(selectors[0].start, selectors[0].end),
155
- };
156
- }
157
-
158
- selectors.unshift(create_nesting_selector(selectors[0].start, selectors[0].end));
159
- }
160
- }
161
-
162
- return selectors;
163
- }
164
-
165
- /**
166
- *
167
- * @param {AST.CSS.ComplexSelector} node
168
- * @returns {AST.CSS.RelativeSelector[]}
169
- */
170
- function truncate(node) {
171
- const i = node.children.findLastIndex(({ metadata, selectors }) => {
172
- const first = selectors[0];
173
- return (
174
- // not after a :global selector
175
- !metadata.is_global_like &&
176
- !(first.type === 'PseudoClassSelector' && first.name === 'global' && first.args === null) &&
177
- // not a :global(...) without a :has/is/where(...) modifier that is scoped
178
- !metadata.is_global
179
- );
180
- });
181
-
182
- return node.children.slice(0, i + 1).map((child) => {
183
- // In case of `:root.y:has(...)`, `y` is unscoped, but everything in `:has(...)` should be scoped (if not global).
184
- // To properly accomplish that, we gotta filter out all selector types except `:has`.
185
- const root = child.selectors.find((s) => s.type === 'PseudoClassSelector' && s.name === 'root');
186
- if (!root || child.metadata.is_global_like) return child;
187
-
188
- return {
189
- ...child,
190
- selectors: child.selectors.filter(
191
- (s) => s.type === 'PseudoClassSelector' && s.name === 'has',
192
- ),
193
- };
194
- });
195
- }
196
-
197
- /**
198
- * @param {AST.CSS.RelativeSelector[]} relative_selectors
199
- * @param {AST.CSS.Rule} rule
200
- * @param {AST.Element} element
201
- * @param {Direction} direction
202
- * @returns {boolean}
203
- */
204
- function apply_selector(relative_selectors, rule, element, direction) {
205
- const rest_selectors = relative_selectors.slice();
206
- const relative_selector = direction === FORWARD ? rest_selectors.shift() : rest_selectors.pop();
207
-
208
- const matched =
209
- !!relative_selector &&
210
- relative_selector_might_apply_to_node(relative_selector, rule, element, direction) &&
211
- apply_combinator(relative_selector, rest_selectors, rule, element, direction);
212
-
213
- if (matched) {
214
- if (!is_outer_global(relative_selector)) {
215
- relative_selector.metadata.scoped = true;
216
-
217
- // Store scoped class information on element for language server features
218
- if (!relative_selector.metadata.is_global && !relative_selector.metadata.is_global_like) {
219
- // Extract class selectors from the relative selector
220
- for (const selector of relative_selector.selectors) {
221
- if (selector.type === 'ClassSelector') {
222
- const name = selector.name.replace(regex_backslash_and_following_character, '$1');
223
-
224
- if (!element.metadata.css) {
225
- element.metadata.css = {
226
- scopedClasses: new Map(),
227
- hash: css_hash,
228
- };
229
- }
230
-
231
- // Store class name → CSS location in scopedClasses
232
- if (!element.metadata.css.scopedClasses.has(name)) {
233
- element.metadata.css.scopedClasses.set(name, {
234
- start: selector.start,
235
- end: selector.end,
236
- selector: selector,
237
- });
238
- }
239
- }
240
- }
241
- }
242
- }
243
-
244
- element.metadata.scoped = true;
245
- }
246
-
247
- return matched;
248
- }
249
-
250
- /**
251
- * @param {AST.Element} node
252
- * @param {boolean} adjacent_only
253
- * @returns {AST.Element[]}
254
- */
255
- function get_ancestor_elements(node, adjacent_only) {
256
- /** @type {AST.Element[]} */
257
- const ancestors = [];
258
-
259
- const path = node.metadata.path;
260
- let i = path.length;
261
-
262
- while (i--) {
263
- const parent = path[i];
264
-
265
- if (parent.type === 'Element') {
266
- ancestors.push(parent);
267
- if (adjacent_only) {
268
- break;
269
- }
270
- }
271
- }
272
-
273
- return ancestors;
274
- }
275
-
276
- /**
277
- * @param {AST.Element} node
278
- * @param {boolean} adjacent_only
279
- * @returns {AST.Element[]}
280
- */
281
- function get_descendant_elements(node, adjacent_only) {
282
- /** @type {AST.Element[]} */
283
- const descendants = [];
284
-
285
- /**
286
- * @param {AST.Node} current_node
287
- * @param {number} depth
288
- * @returns {void}
289
- */
290
- function visit(current_node, depth = 0) {
291
- if (current_node.type === 'Element' && current_node !== node) {
292
- descendants.push(current_node);
293
- if (adjacent_only) return; // Only direct children for '>' combinator
294
- }
295
-
296
- // Visit children based on Ripple's AST structure
297
- if (/** @type {AST.Element} */ (current_node).children) {
298
- for (const child of /** @type {AST.Element} */ (current_node).children) {
299
- visit(child, depth + 1);
300
- }
301
- }
302
-
303
- if (/** @type {AST.Component} */ (current_node).body) {
304
- for (const child of /** @type {AST.Component} */ (current_node).body) {
305
- visit(child, depth + 1);
306
- }
307
- }
308
-
309
- // For template nodes and interpolation expressions
310
- if (
311
- (current_node.type === 'RippleExpression' ||
312
- current_node.type === 'Text' ||
313
- current_node.type === 'Html') &&
314
- /** @type {AST.RippleExpression | AST.Html | AST.TextNode} */ (current_node).expression &&
315
- typeof (
316
- /** @type {AST.RippleExpression | AST.Html | AST.TextNode} */ (current_node).expression
317
- ) === 'object'
318
- ) {
319
- visit(
320
- /** @type {AST.RippleExpression | AST.Html | AST.TextNode} */ (current_node).expression,
321
- depth + 1,
322
- );
323
- }
324
- }
325
-
326
- // Start from node's children
327
- if (node.children) {
328
- for (const child of node.children) {
329
- visit(child);
330
- }
331
- }
332
-
333
- return descendants;
334
- }
335
-
336
- /**
337
- * Check if an element can render dynamic content that might affect CSS matching
338
- * @param {AST.Node} element
339
- * @param {boolean} check_classes - Whether to check for dynamic class attributes
340
- * @returns {boolean}
341
- */
342
- function can_render_dynamic_content(element, check_classes = false) {
343
- if (!is_element_dom_element(element)) {
344
- return true;
345
- }
346
-
347
- // Either a dynamic element or component (only can tell at runtime)
348
- // But dynamic elements should return false ideally
349
- if (is_element_dynamic(/** @type {AST.Element} */ (element))) {
350
- return true;
351
- }
352
-
353
- // Check for dynamic class attributes if requested (for class-based selectors)
354
- if (check_classes && /** @type {AST.Element} */ (element).attributes) {
355
- for (const attr of /** @type {AST.Element} */ (element).attributes) {
356
- if (attr.type === 'Attribute' && attr.name.name === 'class') {
357
- // Check if class value is an expression (not a static string)
358
- if (attr.value && typeof attr.value === 'object') {
359
- // If it's a CallExpression or other dynamic value, it's dynamic
360
- if (attr.value.type !== 'Literal' && attr.value.type !== 'Text') {
361
- return true;
362
- }
363
- }
364
- }
365
- }
366
- }
367
-
368
- return false;
369
- }
370
-
371
- /**
372
- * @param {AST.Node} node
373
- * @param {Direction} direction
374
- * @param {boolean} adjacent_only
375
- * @returns {Map<AST.Element, boolean>}
376
- */
377
- function get_possible_element_siblings(node, direction, adjacent_only) {
378
- const siblings = new Map();
379
- // Parent has to be an Element not a Component
380
- const parent = get_element_parent(node);
381
-
382
- if (!parent) {
383
- return siblings;
384
- }
385
-
386
- // Get the container that holds the siblings
387
- const container = parent.children || [];
388
- const node_index = container.indexOf(node);
389
-
390
- if (node_index === -1) return siblings;
391
-
392
- // Determine which siblings to check based on direction
393
- let start, end, step;
394
- if (direction === FORWARD) {
395
- start = node_index + 1;
396
- end = container.length;
397
- step = 1;
398
- } else {
399
- start = node_index - 1;
400
- end = -1;
401
- step = -1;
402
- }
403
-
404
- // Collect siblings
405
- for (let i = start; i !== end; i += step) {
406
- const sibling = container[i];
407
-
408
- if (sibling.type === 'Element' || sibling.type === 'Component') {
409
- siblings.set(sibling, true);
410
- // Don't break for dynamic elements (children, Components, dynamic components)
411
- // as they can render dynamic content or might render nothing
412
- const isDynamic = can_render_dynamic_content(sibling, false);
413
- if (adjacent_only && !isDynamic) {
414
- break; // Only immediate sibling for '+' combinator
415
- }
416
- }
417
- // Stop at non-whitespace text nodes for adjacent selectors
418
- else if (
419
- adjacent_only &&
420
- (sibling.type === 'RippleExpression' || sibling.type === 'Text') &&
421
- sibling.expression.type === 'Literal' &&
422
- typeof sibling.expression.value === 'string' &&
423
- sibling.expression.value.trim()
424
- ) {
425
- break;
426
- }
427
- }
428
-
429
- return siblings;
430
- }
431
-
432
- /**
433
- * @param {AST.CSS.RelativeSelector} relative_selector
434
- * @param {AST.CSS.RelativeSelector[]} rest_selectors
435
- * @param {AST.CSS.Rule} rule
436
- * @param {AST.Element} node
437
- * @param {Direction} direction
438
- * @returns {boolean}
439
- */
440
- function apply_combinator(relative_selector, rest_selectors, rule, node, direction) {
441
- const combinator =
442
- direction == FORWARD ? rest_selectors[0]?.combinator : relative_selector.combinator;
443
- if (!combinator) return true;
444
-
445
- switch (combinator.name) {
446
- case ' ':
447
- case '>': {
448
- const is_adjacent = combinator.name === '>';
449
- const parents =
450
- direction === FORWARD
451
- ? get_descendant_elements(node, is_adjacent)
452
- : get_ancestor_elements(node, is_adjacent);
453
- let parent_matched = false;
454
-
455
- for (const parent of parents) {
456
- if (apply_selector(rest_selectors, rule, parent, direction)) {
457
- parent_matched = true;
458
- }
459
- }
460
-
461
- return (
462
- parent_matched ||
463
- (direction === BACKWARD &&
464
- (!is_adjacent || parents.length === 0) &&
465
- rest_selectors.every((selector) => is_global(selector, rule)))
466
- );
467
- }
468
-
469
- case '+':
470
- case '~': {
471
- const siblings = get_possible_element_siblings(node, direction, combinator.name === '+');
472
-
473
- let sibling_matched = false;
474
-
475
- for (const possible_sibling of siblings.keys()) {
476
- // Check if this sibling can render dynamic content
477
- // For class selectors, also check if element has dynamic classes
478
- const has_class_selector = rest_selectors.some((sel) =>
479
- sel.selectors?.some((s) => s.type === 'ClassSelector'),
480
- );
481
- const is_dynamic = can_render_dynamic_content(possible_sibling, has_class_selector);
482
-
483
- if (is_dynamic) {
484
- if (rest_selectors.length > 0) {
485
- // Check if the first selector in the rest is global
486
- const first_rest_selector = rest_selectors[0];
487
- if (is_global(first_rest_selector, rule)) {
488
- // Global selector followed by possibly more selectors
489
- // Check if remaining selectors could match elements after this component
490
- const remaining = rest_selectors.slice(1);
491
- if (remaining.length === 0) {
492
- // Just a global selector, mark as matched
493
- sibling_matched = true;
494
- } else {
495
- // Check if there are any elements after this component that could match the remaining selectors
496
- const parent = get_element_parent(node);
497
- if (parent) {
498
- const container = parent.children || [];
499
- const component_index = container.indexOf(possible_sibling);
500
-
501
- // For adjacent combinator, only check immediate next element
502
- // For general sibling, check all following elements
503
- const search_start = component_index + 1;
504
- const search_end = combinator.name === '+' ? search_start + 1 : container.length;
505
-
506
- for (let i = search_start; i < search_end; i++) {
507
- const subsequent = container[i];
508
- if (subsequent.type === 'Element') {
509
- if (apply_selector(remaining, rule, subsequent, direction)) {
510
- sibling_matched = true;
511
- break;
512
- }
513
- if (combinator.name === '+') break; // For adjacent, only check first element
514
- } else if (subsequent.type === 'Component') {
515
- // Skip components when looking for the target element
516
- if (combinator.name === '+') {
517
- // For adjacent, continue looking
518
- continue;
519
- }
520
- }
521
- }
522
- }
523
- }
524
- }
525
- } else if (rest_selectors.length === 1 && rest_selectors[0].metadata.is_global) {
526
- // Single global selector always matches
527
- sibling_matched = true;
528
- }
529
- // Don't apply_selector for dynamic elements - they won't match regular element selectors
530
- } else if (
531
- possible_sibling.type === 'Element' &&
532
- apply_selector(rest_selectors, rule, possible_sibling, direction)
533
- ) {
534
- sibling_matched = true;
535
- }
536
- }
537
-
538
- return (
539
- sibling_matched ||
540
- (direction === BACKWARD &&
541
- get_element_parent(node) === null &&
542
- rest_selectors.every((selector) => is_global(selector, rule)))
543
- );
544
- }
545
-
546
- default:
547
- // TODO other combinators
548
- return true;
549
- }
550
- }
551
- /**
552
- * @param {AST.Node} node
553
- * @returns {AST.Element | null}
554
- */
555
- function get_element_parent(node) {
556
- // Check if metadata and path exist
557
- if (!node.metadata || !node.metadata.path) {
558
- return null;
559
- }
560
-
561
- let path = node.metadata.path;
562
- let i = path.length;
563
-
564
- while (i--) {
565
- const parent = path[i];
566
-
567
- if (parent.type === 'Element') {
568
- return parent;
569
- }
570
- }
571
-
572
- return null;
573
- }
574
-
575
- /**
576
- * `true` if is a pseudo class that cannot be or is not scoped
577
- * @param {AST.CSS.SimpleSelector} selector
578
- * @returns {boolean}
579
- */
580
- function is_unscoped_pseudo_class(selector) {
581
- return (
582
- selector.type === 'PseudoClassSelector' &&
583
- // These make the selector scoped
584
- ((selector.name !== 'has' &&
585
- selector.name !== 'is' &&
586
- selector.name !== 'where' &&
587
- // :not is special because we want to scope as specific as possible, but because :not
588
- // inverses the result, we want to leave the unscoped, too. The exception is more than
589
- // one selector in the :not (.e.g :not(.x .y)), then .x and .y should be scoped
590
- (selector.name !== 'not' ||
591
- selector.args === null ||
592
- selector.args.children.every((c) => c.children.length === 1))) ||
593
- // selectors with has/is/where/not can also be global if all their children are global
594
- selector.args === null ||
595
- selector.args.children.every((c) => c.children.every((r) => is_global_simple(r))))
596
- );
597
- }
598
-
599
- /**
600
- * True if is `:global(...)` or `:global` and no pseudo class that is scoped.
601
- * @param {AST.CSS.RelativeSelector} relative_selector
602
- */
603
- function is_global_simple(relative_selector) {
604
- const first = relative_selector.selectors[0];
605
-
606
- return (
607
- first.type === 'PseudoClassSelector' &&
608
- first.name === 'global' &&
609
- (first.args === null ||
610
- // Only these two selector types keep the whole selector global, because e.g.
611
- // :global(button).x means that the selector is still scoped because of the .x
612
- relative_selector.selectors.every(
613
- (selector) =>
614
- is_unscoped_pseudo_class(selector) || selector.type === 'PseudoElementSelector',
615
- ))
616
- );
617
- }
618
-
619
- /**
620
- * @param {AST.CSS.RelativeSelector} selector
621
- * @param {AST.CSS.Rule} rule
622
- * @return {boolean}
623
- */
624
- function is_global(selector, rule) {
625
- if (selector.metadata.is_global || selector.metadata.is_global_like) {
626
- return true;
627
- }
628
-
629
- let explicitly_global = false;
630
-
631
- for (const s of selector.selectors) {
632
- /** @type {AST.CSS.SelectorList | null} */
633
- let selector_list = null;
634
- let can_be_global = false;
635
- let owner = rule;
636
-
637
- if (s.type === 'PseudoClassSelector') {
638
- if ((s.name === 'is' || s.name === 'where') && s.args) {
639
- selector_list = s.args;
640
- } else {
641
- can_be_global = is_unscoped_pseudo_class(s);
642
- }
643
- }
644
-
645
- if (s.type === 'NestingSelector') {
646
- owner = /** @type {AST.CSS.Rule} */ (rule.metadata.parent_rule);
647
- selector_list = owner.prelude;
648
- }
649
-
650
- const has_global_selectors = !!selector_list?.children.some((complex_selector) => {
651
- return complex_selector.children.every((relative_selector) =>
652
- is_global(relative_selector, owner),
653
- );
654
- });
655
- explicitly_global ||= has_global_selectors;
656
-
657
- if (!has_global_selectors && !can_be_global) {
658
- return false;
659
- }
660
- }
661
-
662
- return explicitly_global || selector.selectors.length === 0;
663
- }
664
-
665
- /**
666
- * @param {AST.Attribute} attribute
667
- * @returns {attribute is AST.Attribute & { value: AST.Literal & { value: string } }}
668
- */
669
- function is_text_attribute(attribute) {
670
- return attribute.value?.type === 'Literal' && typeof attribute.value.value === 'string';
671
- }
672
-
673
- /**
674
- * @param {string | null} operator
675
- * @param {string} expected_value
676
- * @param {boolean} case_insensitive
677
- * @param {string} value
678
- * @returns {boolean}
679
- */
680
- function test_attribute(operator, expected_value, case_insensitive, value) {
681
- if (case_insensitive) {
682
- expected_value = expected_value.toLowerCase();
683
- value = value.toLowerCase();
684
- }
685
- switch (operator) {
686
- case '=':
687
- return value === expected_value;
688
- case '~=':
689
- return value.split(/\s/).includes(expected_value);
690
- case '|=':
691
- return `${value}-`.startsWith(`${expected_value}-`);
692
- case '^=':
693
- return value.startsWith(expected_value);
694
- case '$=':
695
- return value.endsWith(expected_value);
696
- case '*=':
697
- return value.includes(expected_value);
698
- default:
699
- throw new Error("this shouldn't happen");
700
- }
701
- }
702
-
703
- /**
704
- * @param {AST.Element} node
705
- * @param {string} name
706
- * @param {string | null} expected_value
707
- * @param {string | null} operator
708
- * @param {boolean} case_insensitive
709
- * @returns {boolean}
710
- */
711
- function attribute_matches(node, name, expected_value, operator, case_insensitive) {
712
- for (const attribute of node.attributes) {
713
- if (attribute.type === 'SpreadAttribute') return true;
714
-
715
- if (attribute.type !== 'Attribute') continue;
716
-
717
- const lowerCaseName = name.toLowerCase();
718
- if (![lowerCaseName, `$${lowerCaseName}`].includes(attribute.name.name.toLowerCase())) continue;
719
-
720
- if (expected_value === null) return true;
721
-
722
- if (is_text_attribute(attribute)) {
723
- return test_attribute(operator, expected_value, case_insensitive, attribute.value.value);
724
- } else {
725
- return true;
726
- }
727
- }
728
-
729
- return false;
730
- }
731
-
732
- /**
733
- * @param {AST.CSS.RelativeSelector} relative_selector
734
- * @returns {boolean}
735
- */
736
- function is_outer_global(relative_selector) {
737
- const first = relative_selector.selectors[0];
738
-
739
- return (
740
- first &&
741
- first.type === 'PseudoClassSelector' &&
742
- first.name === 'global' &&
743
- (first.args === null ||
744
- // Only these two selector types can keep the whole selector global, because e.g.
745
- // :global(button).x means that the selector is still scoped because of the .x
746
- relative_selector.selectors.every(
747
- (selector) =>
748
- selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector',
749
- ))
750
- );
751
- }
752
-
753
- /**
754
- * @param {AST.CSS.RelativeSelector} relative_selector
755
- * @param {AST.CSS.Rule} rule
756
- * @param {AST.Element} element
757
- * @param {Direction} direction
758
- * @return {boolean}
759
- */
760
- function relative_selector_might_apply_to_node(relative_selector, rule, element, direction) {
761
- // Sort :has(...) selectors in one bucket and everything else into another
762
- const has_selectors = [];
763
- const other_selectors = [];
764
-
765
- for (const selector of relative_selector.selectors) {
766
- if (selector.type === 'PseudoClassSelector' && selector.name === 'has' && selector.args) {
767
- has_selectors.push(selector);
768
- } else {
769
- other_selectors.push(selector);
770
- }
771
- }
772
-
773
- // If we're called recursively from a :has(...) selector, we're on the way of checking if the other selectors match.
774
- // In that case ignore this check (because we just came from this) to avoid an infinite loop.
775
- if (has_selectors.length > 0) {
776
- // If this is a :has inside a global selector, we gotta include the element itself, too,
777
- // because the global selector might be for an element that's outside the component,
778
- // e.g. :root:has(.scoped), :global(.foo):has(.scoped), or :root { &:has(.scoped) {} }
779
- const rules = get_parent_rules(rule);
780
- const include_self =
781
- rules.some((r) => r.prelude.children.some((c) => c.children.some((s) => is_global(s, r)))) ||
782
- rules[rules.length - 1].prelude.children.some((c) =>
783
- c.children.some((r) =>
784
- r.selectors.some(
785
- (s) =>
786
- s.type === 'PseudoClassSelector' &&
787
- (s.name === 'root' || (s.name === 'global' && s.args)),
788
- ),
789
- ),
790
- );
791
-
792
- // :has(...) is special in that it means "look downwards in the CSS tree". Since our matching algorithm goes
793
- // upwards and back-to-front, we need to first check the selectors inside :has(...), then check the rest of the
794
- // selector in a way that is similar to ancestor matching. In a sense, we're treating `.x:has(.y)` as `.x .y`.
795
- for (const has_selector of has_selectors) {
796
- const complex_selectors = /** @type {AST.CSS.SelectorList} */ (has_selector.args).children;
797
- let matched = false;
798
-
799
- for (const complex_selector of complex_selectors) {
800
- const [first, ...rest] = truncate(complex_selector);
801
- // if it was just a :global(...)
802
- if (!first) {
803
- complex_selector.metadata.used = true;
804
- matched = true;
805
- continue;
806
- }
807
-
808
- if (include_self) {
809
- const selector_including_self = [
810
- first.combinator ? { ...first, combinator: null } : first,
811
- ...rest,
812
- ];
813
- if (apply_selector(selector_including_self, rule, element, FORWARD)) {
814
- complex_selector.metadata.used = true;
815
- matched = true;
816
- }
817
- }
818
-
819
- const selector_excluding_self = [
820
- create_any_selector(first.start, first.end),
821
- first.combinator
822
- ? first
823
- : { ...first, combinator: create_descendant_combinator(first.start, first.end) },
824
- ...rest,
825
- ];
826
- if (apply_selector(selector_excluding_self, rule, element, FORWARD)) {
827
- complex_selector.metadata.used = true;
828
- matched = true;
829
- }
830
- }
831
-
832
- if (!matched) {
833
- return false;
834
- }
835
- }
836
- }
837
-
838
- for (const selector of other_selectors) {
839
- if (selector.type === 'Percentage' || selector.type === 'Nth') continue;
840
-
841
- const name = selector.name.replace(regex_backslash_and_following_character, '$1');
842
-
843
- switch (selector.type) {
844
- case 'PseudoClassSelector': {
845
- if (name === 'host' || name === 'root') return false;
846
-
847
- if (
848
- name === 'global' &&
849
- selector.args !== null &&
850
- relative_selector.selectors.length === 1
851
- ) {
852
- const args = selector.args;
853
- const complex_selector = args.children[0];
854
- return apply_selector(complex_selector.children, rule, element, BACKWARD);
855
- }
856
-
857
- // We came across a :global, everything beyond it is global and therefore a potential match
858
- if (name === 'global' && selector.args === null) return true;
859
-
860
- // :not(...) contents should stay unscoped. Scoping them would achieve the opposite of what we want,
861
- // because they are then _more_ likely to bleed out of the component. The exception is complex selectors
862
- // with descendants, in which case we scope them all.
863
- if (name === 'not' && selector.args) {
864
- for (const complex_selector of selector.args.children) {
865
- walk(complex_selector, null, {
866
- ComplexSelector(node, context) {
867
- node.metadata.used = true;
868
- context.next();
869
- },
870
- });
871
- const relative = truncate(complex_selector);
872
-
873
- if (complex_selector.children.length > 1) {
874
- // foo:not(bar foo) means that bar is an ancestor of foo (side note: ending with foo is the only way the selector make sense).
875
- // We can't fully check if that actually matches with our current algorithm, so we just assume it does.
876
- // The result may not match a real element, so the only drawback is the missing prune.
877
- for (const selector of relative) {
878
- selector.metadata.scoped = true;
879
- }
880
-
881
- /** @type {AST.Element | null} */
882
- let el = element;
883
- while (el) {
884
- el.metadata.scoped = true;
885
- el = get_element_parent(el);
886
- }
887
- }
888
- }
889
-
890
- break;
891
- }
892
-
893
- if ((name === 'is' || name === 'where') && selector.args) {
894
- let matched = false;
895
-
896
- for (const complex_selector of selector.args.children) {
897
- const relative = truncate(complex_selector);
898
- const is_global = relative.length === 0;
899
-
900
- if (is_global) {
901
- complex_selector.metadata.used = true;
902
- matched = true;
903
- } else if (apply_selector(relative, rule, element, BACKWARD)) {
904
- complex_selector.metadata.used = true;
905
- matched = true;
906
- } else if (complex_selector.children.length > 1 && (name == 'is' || name == 'where')) {
907
- // foo :is(bar baz) can also mean that bar is an ancestor of foo, and baz a descendant.
908
- // We can't fully check if that actually matches with our current algorithm, so we just assume it does.
909
- // The result may not match a real element, so the only drawback is the missing prune.
910
- complex_selector.metadata.used = true;
911
- matched = true;
912
- for (const selector of relative) {
913
- selector.metadata.scoped = true;
914
- }
915
- }
916
- }
917
-
918
- if (!matched) {
919
- return false;
920
- }
921
- }
922
-
923
- break;
924
- }
925
-
926
- case 'PseudoElementSelector': {
927
- break;
928
- }
929
-
930
- case 'AttributeSelector': {
931
- const whitelisted = whitelist_attribute_selector.get(
932
- /** @type {AST.Identifier} */ (element.id).name.toLowerCase(),
933
- );
934
- if (
935
- !whitelisted?.includes(selector.name.toLowerCase()) &&
936
- !attribute_matches(
937
- element,
938
- selector.name,
939
- selector.value && unquote(selector.value),
940
- selector.matcher,
941
- selector.flags?.includes('i') ?? false,
942
- )
943
- ) {
944
- return false;
945
- }
946
- break;
947
- }
948
-
949
- case 'ClassSelector': {
950
- if (
951
- !attribute_matches(element, 'class', name, '~=', false) &&
952
- (!style_identifier_classes.has(name) ||
953
- !is_standalone_class_selector(relative_selector, selector))
954
- ) {
955
- return false;
956
- }
957
-
958
- break;
959
- }
960
-
961
- case 'IdSelector': {
962
- if (!attribute_matches(element, 'id', name, '=', false)) {
963
- return false;
964
- }
965
-
966
- break;
967
- }
968
-
969
- case 'TypeSelector': {
970
- if (
971
- element.id.type === 'Identifier' &&
972
- element.id.name.toLowerCase() !== name.toLowerCase() &&
973
- name !== '*'
974
- ) {
975
- return false;
976
- }
977
-
978
- break;
979
- }
980
-
981
- case 'NestingSelector': {
982
- let matched = false;
983
-
984
- const parent = /** @type {AST.CSS.Rule} */ (rule.metadata.parent_rule);
985
-
986
- for (const complex_selector of parent.prelude.children) {
987
- if (
988
- apply_selector(get_relative_selectors(complex_selector), parent, element, direction) ||
989
- complex_selector.children.every((s) => is_global(s, parent))
990
- ) {
991
- complex_selector.metadata.used = true;
992
- matched = true;
993
- }
994
- }
995
-
996
- if (!matched) {
997
- return false;
998
- }
999
-
1000
- break;
1001
- }
1002
- }
1003
- }
1004
-
1005
- // possible match
1006
- return true;
1007
- }
1008
-
1009
- /**
1010
- * @param {string} str
1011
- * @returns {string}
1012
- */
1013
- function unquote(str) {
1014
- if (
1015
- (str[0] === '"' && str[str.length - 1] === '"') ||
1016
- (str[0] === "'" && str[str.length - 1] === "'")
1017
- ) {
1018
- return str.slice(1, -1);
1019
- }
1020
- return str;
1021
- }
1022
-
1023
- /**
1024
- * @param {AST.CSS.Rule} rule
1025
- * @returns {AST.CSS.Rule[]}
1026
- */
1027
- function get_parent_rules(rule) {
1028
- const rules = [rule];
1029
- let current = rule;
1030
-
1031
- while (current.metadata.parent_rule) {
1032
- current = current.metadata.parent_rule;
1033
- rules.unshift(current);
1034
- }
1035
-
1036
- return rules;
1037
- }
1038
-
1039
- /**
1040
- * Check if a CSS rule contains animation or animation-name properties
1041
- * @param {AST.CSS.Rule} rule
1042
- * @returns {boolean}
1043
- */
1044
- function rule_has_animation(rule) {
1045
- if (!rule.block) return false;
1046
-
1047
- for (const child of rule.block.children) {
1048
- if (child.type === 'Declaration') {
1049
- const prop = child.property?.toLowerCase();
1050
- if (prop === 'animation' || prop === 'animation-name') {
1051
- return true;
1052
- }
1053
- }
1054
- }
1055
-
1056
- return false;
1057
- }
1058
-
1059
- /**
1060
- * @param {AST.CSS.StyleSheet} css
1061
- * @param {AST.Element} element
1062
- * @param {StyleClasses} styleClasses
1063
- * @param {TopScopedClasses} topScopedClasses
1064
- * @return {void}
1065
- */
1066
- export function prune_css(css, element, styleClasses, topScopedClasses) {
1067
- css_hash = css.hash;
1068
- style_identifier_classes = styleClasses;
1069
- top_scoped_classes = topScopedClasses;
1070
-
1071
- /** @type {Visitors<AST.CSS.Node, null>} */
1072
- const visitors = {
1073
- Rule(node, context) {
1074
- if (node.metadata.is_global_block) {
1075
- context.visit(node.prelude);
1076
- } else {
1077
- context.next();
1078
- }
1079
- },
1080
- ComplexSelector(node, context) {
1081
- const selectors = get_relative_selectors(node);
1082
-
1083
- const rule = /** @type {AST.CSS.Rule} */ (node.metadata.rule);
1084
-
1085
- if (apply_selector(selectors, rule, element, BACKWARD) || rule_has_animation(rule)) {
1086
- node.metadata.used = true;
1087
- }
1088
-
1089
- // Populate top_scoped_classes for truly standalone class selectors (for #style support).
1090
- // A class is standalone only when the entire effective selector chain (after resolving
1091
- // nesting and stripping :global) is a single RelativeSelector with a single ClassSelector.
1092
- // This prevents classes from compound selectors like `.wrapper .nested` or selectors
1093
- // inside :global() from being treated as valid #style targets.
1094
- if (selectors.length === 1) {
1095
- const sole_selector = selectors[0];
1096
- if (
1097
- !sole_selector.metadata.is_global &&
1098
- !sole_selector.metadata.is_global_like &&
1099
- sole_selector.selectors.length === 1 &&
1100
- sole_selector.selectors[0].type === 'ClassSelector'
1101
- ) {
1102
- const class_selector = sole_selector.selectors[0];
1103
- const name = class_selector.name.replace(regex_backslash_and_following_character, '$1');
1104
- if (!top_scoped_classes.has(name)) {
1105
- top_scoped_classes.set(name, {
1106
- start: class_selector.start,
1107
- end: class_selector.end,
1108
- selector: class_selector,
1109
- });
1110
- }
1111
- }
1112
- }
1113
-
1114
- context.next();
1115
- },
1116
- PseudoClassSelector(node, context) {
1117
- // Visit nested selectors inside :has(), :is(), :where(), and :not()
1118
- if (
1119
- (node.name === 'has' ||
1120
- node.name === 'is' ||
1121
- node.name === 'where' ||
1122
- node.name === 'not') &&
1123
- node.args
1124
- ) {
1125
- context.next();
1126
- }
1127
- },
1128
- };
1129
-
1130
- walk(css, null, visitors);
1131
- }