@wix/zero-config-implementation 1.75.0 → 1.77.0

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.
@@ -3,7 +3,14 @@ import { camelCase } from 'case-anything'
3
3
  import { generate, lexer, parse, walk } from 'css-tree'
4
4
  import type { Atrule, CssNode, Declaration, FunctionNode, Rule, Selector, Value } from 'css-tree'
5
5
  import { stateNameFromModifier } from '../../utils/css-class'
6
- import type { CSSParserAPI, CSSProperty, NativePseudoClass, StateClassRule } from './types'
6
+ import type {
7
+ CSSParserAPI,
8
+ CSSProperty,
9
+ CssDataType,
10
+ NativePseudoClass,
11
+ RegisteredCustomProperty,
12
+ StateClassRule,
13
+ } from './types'
7
14
 
8
15
  type WalkContext = {
9
16
  atrule?: Atrule | null
@@ -22,6 +29,7 @@ export function parseCss(cssString: string): CSSParserAPI {
22
29
  const parsedSelectors = new Map<string, Selector>()
23
30
  const { propertiesMap: allProperties, varUsagesByProperty } = parseAllProperties(cssString, parsedSelectors)
24
31
  const stateClasses = parseStateClasses(cssString)
32
+ const registeredProperties = parseRegisteredProperties(cssString)
25
33
 
26
34
  return {
27
35
  getStateClasses(): StateClassRule[] {
@@ -77,6 +85,11 @@ export function parseCss(cssString: string): CSSParserAPI {
77
85
  },
78
86
 
79
87
  getVarPropertyType(varName: string, defaultValue: string): string | undefined {
88
+ // A `@property` registration is the declared type and wins over usage inference.
89
+ const normalizedVarName = varName.startsWith('--') ? varName : `--${varName}`
90
+ const registered = registeredProperties.get(normalizedVarName)
91
+ if (registered) return registered.cssPropertyType
92
+
80
93
  const usages = this.getVarUsages(varName)
81
94
  if (usages.length === 0) return undefined
82
95
  const uniqueProperties = [...new Set(usages)]
@@ -87,6 +100,10 @@ export function parseCss(cssString: string): CSSParserAPI {
87
100
  }
88
101
  return inferCssDataType(defaultValue)
89
102
  },
103
+
104
+ getRegisteredCustomProperties(): Map<string, RegisteredCustomProperty> {
105
+ return new Map(registeredProperties)
106
+ },
90
107
  }
91
108
  }
92
109
 
@@ -329,6 +346,150 @@ function extractVarNames(valueNode: CssNode): string[] {
329
346
  return varNames
330
347
  }
331
348
 
349
+ // ─────────────────────────────────────────────────────────────────────────────
350
+ // @property at-rule parsing (CSS Properties and Values API)
351
+ // ─────────────────────────────────────────────────────────────────────────────
352
+
353
+ /**
354
+ * A single `syntax` component (a CSS type reference) → manifest `cssPropertyType`.
355
+ *
356
+ * Only types with a matching schema `cssPropertyType` appear here. `<integer>` has no
357
+ * dedicated schema type, so it maps to `number`. Types with no schema equivalent —
358
+ * `<image>`, `<url>`, `<resolution>`, `<transform-function>`, `<transform-list>`,
359
+ * `<custom-ident>` — are deliberately absent; see {@link cssPropertyTypeFromSyntax}.
360
+ */
361
+ const SYNTAX_TYPE_BY_COMPONENT: Record<string, CssDataType> = {
362
+ '<color>': CSS_PROPERTIES.CSS_DATA_TYPE.color,
363
+ '<number>': CSS_PROPERTIES.CSS_DATA_TYPE.number,
364
+ '<integer>': CSS_PROPERTIES.CSS_DATA_TYPE.number,
365
+ '<length>': CSS_PROPERTIES.CSS_DATA_TYPE.length,
366
+ '<percentage>': CSS_PROPERTIES.CSS_DATA_TYPE.percentage,
367
+ '<length-percentage>': CSS_PROPERTIES.CSS_DATA_TYPE.lengthPercentage,
368
+ '<angle>': CSS_PROPERTIES.CSS_DATA_TYPE.angle,
369
+ '<time>': CSS_PROPERTIES.CSS_DATA_TYPE.time,
370
+ }
371
+
372
+ // A CSS custom-ident: a bare keyword (no `<...>` type reference), e.g. `space-between`.
373
+ const CUSTOM_IDENT_PATTERN = /^-?[A-Za-z_][\w-]*$/
374
+
375
+ /**
376
+ * Derives the manifest type from a `@property` `syntax` descriptor, or `undefined`
377
+ * when the declared type has no matching schema `cssPropertyType` and the registration
378
+ * should be dropped rather than surfaced.
379
+ *
380
+ * Resolution order:
381
+ * - a pipe-separated ident list (≥2 bare keywords) → `customEnum` with its options;
382
+ * - otherwise the first recognized `<type>` component (list multipliers `+`/`#` stripped);
383
+ * - anything else — the universal `*`, a lone keyword, or an unsupported `<type>` such as
384
+ * `<image>` / `<url>` / `<resolution>` / `<transform-function>` / `<custom-ident>` —
385
+ * has no schema type, so returns `undefined` (dropped).
386
+ */
387
+ function cssPropertyTypeFromSyntax(
388
+ rawSyntax: string,
389
+ ): { cssPropertyType: CssDataType; enumOptions?: string[] } | undefined {
390
+ const { CSS_DATA_TYPE } = CSS_PROPERTIES
391
+ const syntax = rawSyntax
392
+ .trim()
393
+ .replace(/^["']|["']$/g, '')
394
+ .trim()
395
+ if (syntax === '' || syntax === '*') return undefined
396
+
397
+ const components = syntax
398
+ .split('|')
399
+ .map((component) => component.trim())
400
+ .filter((component) => component.length > 0)
401
+
402
+ const identComponents = components.filter((component) => CUSTOM_IDENT_PATTERN.test(component))
403
+ if (identComponents.length === components.length && components.length >= 2) {
404
+ return { cssPropertyType: CSS_DATA_TYPE.customEnum, enumOptions: components }
405
+ }
406
+
407
+ for (const component of components) {
408
+ const baseType = component.replace(/[+#]$/, '')
409
+ const mappedType = SYNTAX_TYPE_BY_COMPONENT[baseType]
410
+ if (mappedType) return { cssPropertyType: mappedType }
411
+ }
412
+
413
+ return undefined
414
+ }
415
+
416
+ /**
417
+ * Collects the descriptor declarations (`syntax`, `initial-value`) from a `@property`
418
+ * block. Re-parses the raw block text when css-tree yields a Raw block, so parsing
419
+ * doesn't depend on css-tree's at-rule support.
420
+ */
421
+ function extractAtruleDescriptors(block: CssNode): Map<string, string> {
422
+ const descriptors = new Map<string, string>()
423
+ const declarations: Declaration[] = []
424
+
425
+ walk(block, {
426
+ visit: 'Declaration',
427
+ enter(declaration: Declaration) {
428
+ declarations.push(declaration)
429
+ },
430
+ })
431
+
432
+ if (declarations.length === 0) {
433
+ const innerText = generate(block)
434
+ .replace(/^\s*\{/, '')
435
+ .replace(/\}\s*$/, '')
436
+ try {
437
+ const parsedBlock = parse(innerText, { context: 'declarationList' })
438
+ walk(parsedBlock, {
439
+ visit: 'Declaration',
440
+ enter(declaration: Declaration) {
441
+ declarations.push(declaration)
442
+ },
443
+ })
444
+ } catch {
445
+ // Leave descriptors empty if the raw block can't be re-parsed.
446
+ }
447
+ }
448
+
449
+ for (const declaration of declarations) {
450
+ descriptors.set(declaration.property, generate(declaration.value).trim())
451
+ }
452
+ return descriptors
453
+ }
454
+
455
+ /**
456
+ * Walks the stylesheet for `@property` at-rules and builds a map of registered custom
457
+ * properties keyed by the `--`-prefixed variable name.
458
+ */
459
+ function parseRegisteredProperties(cssString: string): Map<string, RegisteredCustomProperty> {
460
+ const registered = new Map<string, RegisteredCustomProperty>()
461
+
462
+ try {
463
+ const ast = parse(cssString)
464
+
465
+ walk(ast, {
466
+ visit: 'Atrule',
467
+ enter(atrule: Atrule) {
468
+ if (atrule.name !== 'property' || !atrule.prelude || !atrule.block) return
469
+
470
+ const varName = generate(atrule.prelude).trim()
471
+ if (!varName.startsWith('--')) return
472
+
473
+ const descriptors = extractAtruleDescriptors(atrule.block)
474
+ const resolved = cssPropertyTypeFromSyntax(descriptors.get('syntax') ?? '*')
475
+ if (!resolved) return
476
+ const { cssPropertyType, enumOptions } = resolved
477
+ const initialValue = descriptors.get('initial-value')
478
+
479
+ registered.set(varName, {
480
+ cssPropertyType,
481
+ ...(enumOptions && { enumOptions }),
482
+ ...(initialValue !== undefined && { defaultValue: initialValue }),
483
+ })
484
+ },
485
+ })
486
+ } catch (error) {
487
+ console.error('CSS @property parsing error:', error)
488
+ }
489
+
490
+ return registered
491
+ }
492
+
332
493
  // Native pseudo-classes mapped to their design-state key. `:focus-visible` and
333
494
  // `:focus-within` both collapse to `focus` (the schema has a single focus state).
334
495
  const NATIVE_PSEUDO_BY_NAME: Record<string, NativePseudoClass> = {
@@ -1,5 +1,13 @@
1
+ import type { CSS_PROPERTIES } from '@wix/react-component-schema'
1
2
  import type { CssNode } from 'css-tree'
2
3
 
4
+ /**
5
+ * The subset of manifest `cssPropertyType` values a `@property` `syntax` can resolve to —
6
+ * the schema's CSS data types (`color`, `number`, `length`, …, `customEnum`). Derived from
7
+ * the schema so it stays in sync with `CssCustomPropertyItem['cssPropertyType']`.
8
+ */
9
+ export type CssDataType = (typeof CSS_PROPERTIES.CSS_DATA_TYPE)[keyof typeof CSS_PROPERTIES.CSS_DATA_TYPE]
10
+
3
11
  export interface CSSProperty {
4
12
  name: string
5
13
  value: string
@@ -20,6 +28,27 @@ export interface MatchedCssData {
20
28
  customProperties: Record<string, string>
21
29
  }
22
30
 
31
+ /**
32
+ * A CSS custom property registered via the standard `@property` at-rule.
33
+ * The `syntax` descriptor gives the type (and, for a pipe-separated ident list, a
34
+ * finite set of options); the `initial-value` descriptor gives the default. Unlike
35
+ * usage-inferred types, this is a declared source of truth and takes precedence.
36
+ */
37
+ export interface RegisteredCustomProperty {
38
+ /**
39
+ * The manifest `cssPropertyType` derived from the `@property` `syntax` descriptor
40
+ * (e.g. `color`, `number`, `length`, or `customEnum` for a pipe-separated ident list).
41
+ */
42
+ cssPropertyType: CssDataType
43
+ /** The default value from the `@property` `initial-value` descriptor, if declared. */
44
+ defaultValue?: string
45
+ /**
46
+ * The custom-ident options when `syntax` is a pipe-separated ident list
47
+ * (`a | b | c`). Present only when `cssPropertyType` is `customEnum`.
48
+ */
49
+ enumOptions?: string[]
50
+ }
51
+
23
52
  export type NativePseudoClass = 'hover' | 'focus' | 'disabled' | 'invalid'
24
53
 
25
54
  /**
@@ -88,16 +117,25 @@ export interface CSSParserAPI {
88
117
  getSelectorSpecificity: (selector: string) => [number, number, number] | null
89
118
 
90
119
  /**
91
- * Determines the CSS property type for a custom property based on how it is used.
92
- * If all usages of varName are within the same CSS property, returns that property name.
93
- * If usages differ, returns the CSS data type inferred from the initial value
94
- * ('color', 'length', 'number', or 'string').
95
- * Returns undefined if the variable is never used via var().
120
+ * Determines the CSS property type for a custom property.
121
+ * A `@property` registration is the top-priority source (declared type). Otherwise
122
+ * falls back to usage: if all usages of varName are within the same CSS property,
123
+ * returns that property name; if usages differ, returns the CSS data type inferred
124
+ * from the initial value ('color', 'length', 'number', or 'string').
125
+ * Returns undefined if the variable is neither registered nor used via var().
96
126
  * @param varName - The CSS variable name (with or without --)
97
127
  * @param defaultValue - The initial value string of the custom property
98
128
  */
99
129
  getVarPropertyType: (varName: string, defaultValue: string) => string | undefined
100
130
 
131
+ /**
132
+ * Returns every custom property registered via a `@property` at-rule, keyed by the
133
+ * variable name including its leading `--`. Each entry carries the type derived from
134
+ * `syntax`, the default from `initial-value`, and (for a pipe-separated ident list)
135
+ * the enum options.
136
+ */
137
+ getRegisteredCustomProperties: () => Map<string, RegisteredCustomProperty>
138
+
101
139
  /**
102
140
  * Returns every `:global(.modifier)` design-state class found in the CSS, paired
103
141
  * with any native pseudo-class on the same base element within the same rule.