@wix/zero-config-implementation 1.67.0 → 1.68.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.
@@ -1,4 +1,4 @@
1
- import { g as x } from "./index-Ps7eUbv5.js";
1
+ import { g as x } from "./index-BewAGOQY.js";
2
2
  function h(r, a) {
3
3
  for (var i = 0; i < a.length; i++) {
4
4
  const o = a[i];
package/dist/index.d.ts CHANGED
@@ -2364,6 +2364,7 @@ export declare interface PropInfo {
2364
2364
  resolvedType: ResolvedType;
2365
2365
  description?: string;
2366
2366
  deprecated?: boolean;
2367
+ isStateTrigger?: boolean;
2367
2368
  }
2368
2369
 
2369
2370
  export declare interface PropSpyMeta {
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { B as t, D as e, E as o, I as n, N as c, P as l, R as i, a as p, V as E, b as m, c as x, d as f, e as d, f as u, h as C, i as I, j as k, k as y, l as A, m as D, n as P, o as R, p as h, q as B, r as M, s as T, t as b, w } from "./index-Ps7eUbv5.js";
1
+ import { B as t, D as e, E as o, I as n, N as c, P as l, R as i, a as p, V as E, b as m, c as x, d as f, e as d, f as u, h as C, i as I, j as k, k as y, l as A, m as D, n as P, o as R, p as h, q as B, r as M, s as T, t as b, w } from "./index-BewAGOQY.js";
2
2
  import "react";
3
3
  export {
4
4
  t as BaseError,
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "registry": "https://registry.npmjs.org/",
5
5
  "access": "public"
6
6
  },
7
- "version": "1.67.0",
7
+ "version": "1.68.0",
8
8
  "description": "Core library for extracting component manifests from JS and CSS files",
9
9
  "type": "module",
10
10
  "main": "dist/index.js",
@@ -81,5 +81,5 @@
81
81
  ]
82
82
  }
83
83
  },
84
- "falconPackageHash": "98b2efba108de51cc5bc3678f7b32253ba1a26e9b875964b6a8e725e"
84
+ "falconPackageHash": "5848ef0051ff536666dd46c6264f765b40f280afa13b5d120028a1a2"
85
85
  }
@@ -1,11 +1,16 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
  import type { ExtractedElement } from '../information-extractors/react'
3
+ import type { CoupledProp } from '../information-extractors/react/types'
3
4
  import { type CustomClassTrigger, buildCustomStatesBlock } from './custom-states-builder'
4
5
 
5
6
  function elementWithClass(classAttribute: string): ExtractedElement {
6
7
  return { attributes: { class: classAttribute } } as unknown as ExtractedElement
7
8
  }
8
9
 
10
+ function booleanStateProp(name: string): CoupledProp {
11
+ return { name, type: 'boolean', isStateTrigger: true } as unknown as CoupledProp
12
+ }
13
+
9
14
  function classTrigger(baseSelector: string, modifier: string, isCssModule = false): CustomClassTrigger {
10
15
  return { baseSelector, modifier, isCssModule }
11
16
  }
@@ -14,6 +19,7 @@ describe('buildCustomStatesBlock', () => {
14
19
  it('emits a custom-class state with className only (no props, no pseudoClass)', () => {
15
20
  const states = buildCustomStatesBlock({
16
21
  element: elementWithClass('custom-states'),
22
+ propTriggers: [],
17
23
  classTriggers: [classTrigger('.custom-states', 'custom-states--featured')],
18
24
  })
19
25
 
@@ -23,6 +29,7 @@ describe('buildCustomStatesBlock', () => {
23
29
  it('derives a multi-word state name from the modifier and capital-cases the displayName', () => {
24
30
  const states = buildCustomStatesBlock({
25
31
  element: elementWithClass('widget'),
32
+ propTriggers: [],
26
33
  classTriggers: [classTrigger('.widget', 'widget--in-progress')],
27
34
  })
28
35
 
@@ -35,6 +42,7 @@ describe('buildCustomStatesBlock', () => {
35
42
  it('does not apply a block-base class trigger to a BEM child element (plain CSS, exact match)', () => {
36
43
  const states = buildCustomStatesBlock({
37
44
  element: elementWithClass('custom-states__label'),
45
+ propTriggers: [],
38
46
  classTriggers: [classTrigger('.custom-states', 'custom-states--featured')],
39
47
  })
40
48
 
@@ -46,6 +54,7 @@ describe('buildCustomStatesBlock', () => {
46
54
  // base element — not a BEM child. (Regression: this used to be dropped as a child.)
47
55
  const states = buildCustomStatesBlock({
48
56
  element: elementWithClass('root__IWjkn wixui-test-comp-card test-comp-card'),
57
+ propTriggers: [],
49
58
  classTriggers: [classTrigger('.root', 'test-comp-card--featured', true)],
50
59
  })
51
60
 
@@ -55,15 +64,71 @@ describe('buildCustomStatesBlock', () => {
55
64
  it('still excludes a genuine BEM child (`root__label`) from a block-base trigger in a module', () => {
56
65
  const states = buildCustomStatesBlock({
57
66
  element: elementWithClass('root__label'),
67
+ propTriggers: [],
58
68
  classTriggers: [classTrigger('.root', 'test-comp-card--featured', true)],
59
69
  })
60
70
 
61
71
  expect(states).toBeUndefined()
62
72
  })
63
73
 
64
- it('returns undefined when no class trigger targets the element', () => {
74
+ it('returns undefined when no trigger targets the element', () => {
75
+ const states = buildCustomStatesBlock({
76
+ element: elementWithClass('custom-states'),
77
+ propTriggers: [],
78
+ classTriggers: [],
79
+ })
80
+
81
+ expect(states).toBeUndefined()
82
+ })
83
+
84
+ it('emits a props-only state for a prop trigger with no matching class (no className synthesized)', () => {
85
+ const states = buildCustomStatesBlock({
86
+ element: elementWithClass('panel'),
87
+ propTriggers: [booleanStateProp('isExpanded')],
88
+ classTriggers: [],
89
+ })
90
+
91
+ expect(states).toEqual({
92
+ 'is-expanded': { displayName: 'Is Expanded', props: { isExpanded: true } },
93
+ })
94
+ })
95
+
96
+ it('keeps the full prop name in the state key and displayName (no is/has stripping)', () => {
97
+ const states = buildCustomStatesBlock({
98
+ element: elementWithClass('widget'),
99
+ propTriggers: [booleanStateProp('isLoading')],
100
+ classTriggers: [],
101
+ })
102
+
103
+ expect(states?.['is-loading']).toEqual({
104
+ displayName: 'Is Loading',
105
+ props: { isLoading: true },
106
+ })
107
+ })
108
+
109
+ it('merges a prop trigger with a class trigger that resolves to the same state name', () => {
110
+ const states = buildCustomStatesBlock({
111
+ element: elementWithClass('prop-states'),
112
+ propTriggers: [booleanStateProp('isLoading')],
113
+ classTriggers: [classTrigger('.prop-states', 'prop-states--is-loading')],
114
+ })
115
+
116
+ expect(states).toEqual({
117
+ 'is-loading': {
118
+ displayName: 'Is Loading',
119
+ className: 'prop-states--is-loading',
120
+ props: { isLoading: true },
121
+ },
122
+ })
123
+ })
124
+
125
+ it('ignores plain (non-ElementState) props and non-boolean state triggers', () => {
65
126
  const states = buildCustomStatesBlock({
66
127
  element: elementWithClass('custom-states'),
128
+ propTriggers: [
129
+ { name: 'showLabel', type: 'boolean' } as unknown as CoupledProp,
130
+ { name: 'variant', type: "'a' | 'b'", isStateTrigger: true } as unknown as CoupledProp,
131
+ ],
67
132
  classTriggers: [],
68
133
  })
69
134
 
@@ -1,20 +1,25 @@
1
1
  /**
2
2
  * Builds the custom (non-native) `states` entries for an element.
3
3
  *
4
- * The custom-class trigger feeds this: a `:global(.modifier)` class on the element
5
- * with no paired native pseudo (CSS extractor `getStateClasses`). It emits the
6
- * modifier as the state's editor-trigger `className`.
4
+ * Two triggers feed this, per the agreed design props are looked at first:
5
+ * - Prop — a boolean prop wrapped in `ElementState<>` (TS extractor sets
6
+ * `isStateTrigger`) IS a custom state, emitting
7
+ * `props: { <prop>: true }`. The CSS is then searched for a
8
+ * class trigger matching that state's name — when found it
9
+ * supplies the editor-trigger `className`; otherwise the entry
10
+ * is props-only (`className` is never synthesized).
11
+ * - Custom class — a `:global(.modifier)` class on the element with no paired
12
+ * native pseudo (CSS extractor `getStateClasses`) and not
13
+ * claimed by a prop trigger. Emits the modifier as a
14
+ * className-only state.
7
15
  *
8
16
  * Native states are built separately and take precedence on any key collision.
9
- *
10
- * Note: the prop trigger (`State<>` markers → `props` overrides) is intentionally
11
- * not handled here yet — it is deferred to a follow-up PR, gated on the upstream
12
- * marker-type agreement.
13
17
  */
14
18
 
15
19
  import type { States } from '@wix/react-component-schema'
20
+ import { kebabCase } from 'case-anything'
16
21
  import type { StateClassRule } from '../information-extractors/css/types'
17
- import type { ExtractedElement } from '../information-extractors/react'
22
+ import type { CoupledProp, ExtractedElement } from '../information-extractors/react'
18
23
  import { isGlobalSemanticClass, stateNameFromModifier } from '../utils/css-class'
19
24
  import { formatDisplayName } from './utils'
20
25
 
@@ -23,17 +28,44 @@ export type CustomClassTrigger = StateClassRule & { isCssModule: boolean }
23
28
 
24
29
  interface BuildCustomStatesParams {
25
30
  element: ExtractedElement
31
+ /** Prop triggers in scope for this element (root-level `ElementState<>` props; `[]` for inner elements). */
32
+ propTriggers: CoupledProp[]
26
33
  /** Custom-class triggers (modifiers with no paired native pseudo) across all CSS files. */
27
34
  classTriggers: CustomClassTrigger[]
28
35
  }
29
36
 
30
- export function buildCustomStatesBlock({ element, classTriggers }: BuildCustomStatesParams): States | undefined {
37
+ export function buildCustomStatesBlock({
38
+ element,
39
+ propTriggers,
40
+ classTriggers,
41
+ }: BuildCustomStatesParams): States | undefined {
31
42
  const states: States = {}
32
43
 
33
- // Custom-class triggers that target this element.
44
+ // Class triggers that target this element, keyed by their derived state name,
45
+ // so prop triggers can look up their matching class.
46
+ const elementClassTriggers = new Map<string, CustomClassTrigger>()
34
47
  for (const rule of classTriggers) {
35
48
  if (!elementMatchesBase(element, rule.baseSelector, rule.isCssModule)) continue
36
- const stateName = stateNameFromModifier(rule.modifier)
49
+ elementClassTriggers.set(stateNameFromModifier(rule.modifier), rule)
50
+ }
51
+
52
+ // Prop triggers first: each boolean `ElementState<>` prop is a custom state.
53
+ // Then search the CSS for the class trigger matching that state's name — when
54
+ // found it supplies the editor-trigger className; otherwise props-only.
55
+ for (const prop of propTriggers) {
56
+ if (!prop.isStateTrigger || prop.type !== 'boolean') continue
57
+ const stateName = stateNameFromProp(prop.name)
58
+ const matchingClassTrigger = elementClassTriggers.get(stateName)
59
+ states[stateName] = {
60
+ displayName: formatDisplayName(stateName),
61
+ ...(matchingClassTrigger && { className: matchingClassTrigger.modifier }),
62
+ props: { [prop.name]: true },
63
+ }
64
+ }
65
+
66
+ // Remaining class triggers — those not claimed by a prop — are className-only states.
67
+ for (const [stateName, rule] of elementClassTriggers) {
68
+ if (states[stateName] !== undefined) continue
37
69
  states[stateName] = {
38
70
  displayName: formatDisplayName(stateName),
39
71
  className: rule.modifier,
@@ -43,6 +75,16 @@ export function buildCustomStatesBlock({ element, classTriggers }: BuildCustomSt
43
75
  return Object.keys(states).length > 0 ? states : undefined
44
76
  }
45
77
 
78
+ /**
79
+ * Derives the state name from a boolean prop by kebab-casing the full prop name
80
+ * (`isLoading` → `is-loading`, so the displayName reads "Is Loading"). The prop
81
+ * name is kept whole — no `is`/`has` prefix stripping — so the state key mirrors
82
+ * the prop that triggers it.
83
+ */
84
+ function stateNameFromProp(propName: string): string {
85
+ return kebabCase(propName)
86
+ }
87
+
46
88
  /**
47
89
  * Checks whether an element's rendered class list matches a simple single-class
48
90
  * base selector (e.g. `.root`, `.custom-states`).
@@ -29,7 +29,7 @@ import { formatDisplayName } from './utils'
29
29
 
30
30
  /**
31
31
  * Merges native and custom state blocks. Native states take precedence on any
32
- * key collision (e.g. an author wrapping `isDisabled` in `State<>` does not
32
+ * key collision (e.g. an author wrapping `isDisabled` in `ElementState<>` does not
33
33
  * shadow the inferred native `disabled` state).
34
34
  */
35
35
  function mergeStates(native: States | undefined, custom: States | undefined): States | undefined {
@@ -75,7 +75,11 @@ function buildEditorElement(
75
75
  const rootStates = rootElement
76
76
  ? mergeStates(
77
77
  rootSemanticClass ? buildStatesBlock(rootElement, rootSemanticClass) : undefined,
78
- buildCustomStatesBlock({ element: rootElement, classTriggers: customClassTriggers }),
78
+ buildCustomStatesBlock({
79
+ element: rootElement,
80
+ propTriggers: Object.values(component.props).filter((prop) => prop.isStateTrigger),
81
+ classTriggers: customClassTriggers,
82
+ }),
79
83
  )
80
84
  : undefined
81
85
 
@@ -147,9 +151,11 @@ function buildElements(
147
151
  const cssCustomProps = nearestCommonAncestorCustomProps.get(element.traceId) ?? {}
148
152
 
149
153
  const semanticClass = getSemanticBlockName(element)
154
+ // Inner-element custom states come from custom-class triggers only; prop
155
+ // triggers (`ElementState<>`) are component-level and attach to the root.
150
156
  const states = mergeStates(
151
157
  semanticClass ? buildStatesBlock(element, semanticClass) : undefined,
152
- buildCustomStatesBlock({ element, classTriggers: customClassTriggers }),
158
+ buildCustomStatesBlock({ element, propTriggers: [], classTriggers: customClassTriggers }),
153
159
  )
154
160
 
155
161
  result[element.name] = {
@@ -0,0 +1,29 @@
1
+ import path from 'node:path'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { compileTsFile } from '../../ts-compiler'
4
+ import { extractAllComponentInfo } from './components'
5
+
6
+ const propTriggeredFixture = path.resolve(
7
+ __dirname,
8
+ '../../../../example-components/src/components/DesignStates/Custom/PropTriggered/WithMatchingClass/component.tsx',
9
+ )
10
+
11
+ describe('TS extractor — ElementState<> trigger detection', () => {
12
+ it('flags an ElementState<>-wrapped prop and unwraps its inner type', async () => {
13
+ const programResult = await compileTsFile(propTriggeredFixture)
14
+ expect(programResult.isOk()).toBe(true)
15
+ const program = programResult._unsafeUnwrap()
16
+
17
+ const components = extractAllComponentInfo(program, propTriggeredFixture)
18
+ const component = components.find((candidate) => candidate.componentName === 'PropStates')
19
+ expect(component).toBeDefined()
20
+
21
+ const isLoading = component!.props.isLoading
22
+ expect(isLoading.isStateTrigger).toBe(true)
23
+ // The `ElementState<boolean>` wrapper is unwrapped — the prop behaves as a plain boolean.
24
+ expect(isLoading.type).toBe('boolean')
25
+
26
+ // Plain booleans are never flagged as state triggers.
27
+ expect(component!.props.showLabel.isStateTrigger).toBeUndefined()
28
+ })
29
+ })
@@ -208,16 +208,27 @@ function convertComponentDoc(doc: ComponentDoc, program: ts.Program, checker: ts
208
208
 
209
209
  // Resolve the type deeply
210
210
  let declaredTypeInfo: DeclaredTypeInfo | undefined
211
+ let stateTrigger: StateTriggerInfo | undefined
211
212
  if (propSymbol) {
212
213
  const decl = propSymbol.getDeclarations()?.[0]
213
214
  if (decl) {
214
- declaredTypeInfo = getDeclaredTypeInfo(propSymbol, decl.getSourceFile(), checker)
215
+ const declSourceFile = decl.getSourceFile()
216
+ declaredTypeInfo = getDeclaredTypeInfo(propSymbol, declSourceFile, checker)
217
+ stateTrigger = detectStateTrigger(propSymbol, declSourceFile, checker)
215
218
  }
216
219
  }
217
220
 
218
- const typeString = declaredTypeInfo?.name ?? propItem.type.name
221
+ // An `ElementState<Inner>` prop should behave as its inner type everywhere except
222
+ // for the `isStateTrigger` flag, so unwrap the inner type for `type` and resolution
223
+ // (and drop the `ElementState` declared symbol so it isn't mistaken for a semantic type).
224
+ const typeString = stateTrigger?.innerTypeName ?? declaredTypeInfo?.name ?? propItem.type.name
219
225
  const resolvedType = propType
220
- ? resolveType({ type: propType, checker, typeString, declaredSymbol: declaredTypeInfo?.symbol })
226
+ ? resolveType({
227
+ type: propType,
228
+ checker,
229
+ typeString,
230
+ declaredSymbol: stateTrigger ? undefined : declaredTypeInfo?.symbol,
231
+ })
221
232
  : { kind: 'primitive' as const, value: propItem.type.name }
222
233
 
223
234
  // Convert default value
@@ -239,6 +250,7 @@ function convertComponentDoc(doc: ComponentDoc, program: ts.Program, checker: ts
239
250
  resolvedType,
240
251
  description: propItem.description || undefined,
241
252
  deprecated: tags?.deprecated !== undefined ? true : undefined,
253
+ isStateTrigger: stateTrigger ? true : undefined,
242
254
  }
243
255
  }
244
256
 
@@ -477,6 +489,63 @@ interface DeclaredTypeInfo {
477
489
  symbol?: ts.Symbol
478
490
  }
479
491
 
492
+ interface StateTriggerInfo {
493
+ /** Text of the inner type argument, e.g. `'boolean'`. */
494
+ innerTypeName: string
495
+ }
496
+
497
+ /**
498
+ * Detects when a prop is declared with the `ElementState<>` marker type from
499
+ * `@wix/react-component-utils` (e.g. `isLoading?: ElementState<boolean>`), which opts
500
+ * the prop in as the trigger for a custom design state. Returns the inner type
501
+ * argument so callers can treat the prop as its underlying type. Returns
502
+ * `undefined` for any other declaration.
503
+ */
504
+ function detectStateTrigger(
505
+ prop: ts.Symbol,
506
+ sourceFile: ts.SourceFile,
507
+ checker: ts.TypeChecker,
508
+ ): StateTriggerInfo | undefined {
509
+ const declaration = prop.getDeclarations()?.[0]
510
+ if (!declaration) return undefined
511
+
512
+ if (
513
+ !(ts.isPropertySignature(declaration) || ts.isPropertyDeclaration(declaration) || ts.isParameter(declaration)) ||
514
+ !declaration.type ||
515
+ !ts.isTypeReferenceNode(declaration.type) ||
516
+ !declaration.type.typeArguments ||
517
+ declaration.type.typeArguments.length !== 1
518
+ ) {
519
+ return undefined
520
+ }
521
+
522
+ const typeName = declaration.type.typeName
523
+ const identifier = ts.isIdentifier(typeName) ? typeName : typeName.right
524
+ if (identifier.text !== 'ElementState') return undefined
525
+ if (!isElementStateAliasFromUtils(identifier, checker)) return undefined
526
+
527
+ return { innerTypeName: declaration.type.typeArguments[0].getText(sourceFile) }
528
+ }
529
+
530
+ /**
531
+ * Confirms an identifier referencing `ElementState` resolves to the marker type
532
+ * alias exported by `@wix/react-component-utils`, rather than an unrelated local
533
+ * type that happens to be named `ElementState`.
534
+ */
535
+ function isElementStateAliasFromUtils(identifier: ts.Identifier, checker: ts.TypeChecker): boolean {
536
+ let symbol = checker.getSymbolAtLocation(identifier)
537
+ if (!symbol) return false
538
+ if ((symbol.getFlags() & ts.SymbolFlags.Alias) !== 0) {
539
+ symbol = checker.getAliasedSymbol(symbol)
540
+ }
541
+
542
+ const declaration = symbol.getDeclarations()?.[0]
543
+ if (!declaration || !ts.isTypeAliasDeclaration(declaration)) return false
544
+ if (declaration.name.text !== 'ElementState') return false
545
+
546
+ return declaration.getSourceFile().fileName.includes('react-component-utils')
547
+ }
548
+
480
549
  function getDeclaredTypeInfo(
481
550
  prop: ts.Symbol,
482
551
  sourceFile: ts.SourceFile,
@@ -51,6 +51,11 @@ export interface PropInfo {
51
51
  description?: string
52
52
  // Whether the prop is marked as deprecated via @deprecated JSDoc tag
53
53
  deprecated?: boolean
54
+ // True when the prop is wrapped in the `ElementState<>` marker type from
55
+ // @wix/react-component-utils, declaring it as the trigger for a custom design
56
+ // state. `type` and `resolvedType` reflect the unwrapped inner type, so the prop
57
+ // still behaves as a normal data prop everywhere else.
58
+ isStateTrigger?: boolean
54
59
  }
55
60
 
56
61
  export interface ComponentInfo {