@wix/zero-config-implementation 1.30.0 → 1.32.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.
@@ -14,6 +14,7 @@ export interface ResolvedType {
14
14
  /**
15
15
  * Resolved default value with its type.
16
16
  * For literals (string, number, boolean, null), the value is the actual JavaScript value.
17
+ * For structured literals (object, array), the value is the parsed JavaScript value.
17
18
  * For unresolvable values (references, expressions), we store the raw source text.
18
19
  */
19
20
  export type DefaultValue = {
@@ -28,6 +29,12 @@ export type DefaultValue = {
28
29
  } | {
29
30
  kind: 'null';
30
31
  value: null;
32
+ } | {
33
+ kind: 'object';
34
+ value: Record<string, unknown>;
35
+ } | {
36
+ kind: 'array';
37
+ value: unknown[];
31
38
  } | {
32
39
  kind: 'unresolved';
33
40
  value: string;
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "registry": "https://registry.npmjs.org/",
5
5
  "access": "public"
6
6
  },
7
- "version": "1.30.0",
7
+ "version": "1.32.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",
@@ -83,5 +83,5 @@
83
83
  ]
84
84
  }
85
85
  },
86
- "falconPackageHash": "7c40209d1126d0b860d10bc39a13660c52b5a97620afd671085c8c41"
86
+ "falconPackageHash": "459bf8c730865e0703e2a8248d887164542b0044ede2b6bcdae37271"
87
87
  }
@@ -5,7 +5,7 @@
5
5
  * Each element gets a semantic name computed by concatenating ancestor names.
6
6
  */
7
7
 
8
- import { pascalCase } from 'case-anything'
8
+ import { camelCase, pascalCase } from 'case-anything'
9
9
  import { type DefaultTreeAdapterMap, parseFragment } from 'parse5'
10
10
  import { TRACE_ATTR } from '../../../../component-renderer'
11
11
  import { findPreferredSemanticClass } from '../../../../utils/css-class'
@@ -34,59 +34,59 @@ export interface ExtractedElement {
34
34
  // ─────────────────────────────────────────────────────────────────────────────
35
35
 
36
36
  const TAG_NAMES: Record<string, string> = {
37
- a: 'Anchor',
38
- abbr: 'Abbreviation',
39
- article: 'Article',
40
- aside: 'Aside',
41
- button: 'Button',
42
- caption: 'Caption',
43
- col: 'TableColumn',
44
- colgroup: 'TableColumnGroup',
45
- dd: 'DescriptionDetails',
46
- details: 'Details',
47
- div: 'Div',
48
- dl: 'DescriptionList',
49
- dt: 'DescriptionTerm',
50
- fieldset: 'Fieldset',
51
- figcaption: 'FigureCaption',
52
- figure: 'Figure',
53
- footer: 'Footer',
54
- form: 'Form',
55
- h1: 'Heading1',
56
- h2: 'Heading2',
57
- h3: 'Heading3',
58
- h4: 'Heading4',
59
- h5: 'Heading5',
60
- h6: 'Heading6',
61
- header: 'Header',
62
- hr: 'HorizontalRule',
63
- img: 'Image',
64
- input: 'Input',
65
- label: 'Label',
66
- legend: 'Legend',
67
- li: 'ListItem',
68
- main: 'Main',
69
- nav: 'Navigation',
70
- ol: 'OrderedList',
71
- optgroup: 'OptionGroup',
72
- option: 'Option',
73
- p: 'Paragraph',
74
- pre: 'Preformatted',
75
- progress: 'Progress',
76
- section: 'Section',
77
- select: 'Select',
78
- span: 'Span',
79
- strong: 'Strong',
80
- summary: 'Summary',
81
- table: 'Table',
82
- tbody: 'TableBody',
83
- td: 'TableCell',
84
- textarea: 'TextArea',
85
- tfoot: 'TableFooter',
86
- th: 'TableHeader',
87
- thead: 'TableHead',
88
- tr: 'TableRow',
89
- ul: 'UnorderedList',
37
+ a: 'anchor',
38
+ abbr: 'abbreviation',
39
+ article: 'article',
40
+ aside: 'aside',
41
+ button: 'button',
42
+ caption: 'caption',
43
+ col: 'tableColumn',
44
+ colgroup: 'tableColumnGroup',
45
+ dd: 'descriptionDetails',
46
+ details: 'details',
47
+ div: 'div',
48
+ dl: 'descriptionList',
49
+ dt: 'descriptionTerm',
50
+ fieldset: 'fieldset',
51
+ figcaption: 'figureCaption',
52
+ figure: 'figure',
53
+ footer: 'footer',
54
+ form: 'form',
55
+ h1: 'heading1',
56
+ h2: 'heading2',
57
+ h3: 'heading3',
58
+ h4: 'heading4',
59
+ h5: 'heading5',
60
+ h6: 'heading6',
61
+ header: 'header',
62
+ hr: 'horizontalRule',
63
+ img: 'image',
64
+ input: 'input',
65
+ label: 'label',
66
+ legend: 'legend',
67
+ li: 'listItem',
68
+ main: 'main',
69
+ nav: 'navigation',
70
+ ol: 'orderedList',
71
+ optgroup: 'optionGroup',
72
+ option: 'option',
73
+ p: 'paragraph',
74
+ pre: 'preformatted',
75
+ progress: 'progress',
76
+ section: 'section',
77
+ select: 'select',
78
+ span: 'span',
79
+ strong: 'strong',
80
+ summary: 'summary',
81
+ table: 'table',
82
+ tbody: 'tableBody',
83
+ td: 'tableCell',
84
+ textarea: 'textArea',
85
+ tfoot: 'tableFooter',
86
+ th: 'tableHeader',
87
+ thead: 'tableHead',
88
+ tr: 'tableRow',
89
+ ul: 'unorderedList',
90
90
  }
91
91
 
92
92
  // ─────────────────────────────────────────────────────────────────────────────
@@ -118,10 +118,10 @@ const hasDirectTextContent = (element: Element): boolean =>
118
118
  element.childNodes.some((child) => isTextNode(child) && child.value.trim().length > 0)
119
119
 
120
120
  /**
121
- * Normalizes a tag name to its semantic PascalCase form
121
+ * Normalizes a tag name to its semantic camelCase form
122
122
  */
123
123
  function normalizeTagName(tag: string): string {
124
- return TAG_NAMES[tag.toLowerCase()] ?? pascalCase(tag)
124
+ return TAG_NAMES[tag.toLowerCase()] ?? camelCase(tag)
125
125
  }
126
126
 
127
127
  /**
@@ -148,16 +148,17 @@ function getTextContent(element: Element): string {
148
148
  * Priority: id > aria-label > aria-labelledby > normalized tag name
149
149
  */
150
150
  function getElementNamePart(element: Element, getElementById: (id: string) => Element | undefined): string {
151
+ let elementName: string | undefined
151
152
  const id = getAttribute(element, 'id')
152
153
  // Skip spy-instrumented ids (mock_propName_XXXXXX) — they are runtime mock values,
153
154
  // not semantic identifiers, and would produce garbage element names.
154
155
  if (id && !id.includes('mock_')) {
155
- return pascalCase(id)
156
+ elementName = id
156
157
  }
157
158
 
158
159
  const ariaLabel = getAttribute(element, 'aria-label')
159
160
  if (ariaLabel && !ariaLabel.startsWith('mock_')) {
160
- return pascalCase(ariaLabel)
161
+ elementName = ariaLabel
161
162
  }
162
163
 
163
164
  const ariaLabelledBy = getAttribute(element, 'aria-labelledby')
@@ -166,7 +167,7 @@ function getElementNamePart(element: Element, getElementById: (id: string) => El
166
167
  if (labelElement) {
167
168
  const labelText = getTextContent(labelElement)
168
169
  if (labelText) {
169
- return pascalCase(labelText)
170
+ elementName = labelText
170
171
  }
171
172
  }
172
173
  }
@@ -174,7 +175,11 @@ function getElementNamePart(element: Element, getElementById: (id: string) => El
174
175
  const classAttr = getAttribute(element, 'class')
175
176
  if (classAttr) {
176
177
  const semanticClass = findPreferredSemanticClass(classAttr.split(' '))
177
- if (semanticClass) return pascalCase(semanticClass)
178
+ if (semanticClass) elementName = semanticClass
179
+ }
180
+
181
+ if (elementName) {
182
+ return camelCase(elementName)
178
183
  }
179
184
 
180
185
  return normalizeTagName(element.tagName)
@@ -268,8 +273,10 @@ export function buildElementTree(html: string, store: ExtractorStore): Extracted
268
273
  // Compute this element's name part
269
274
  const namePart = isRoot ? 'root' : getElementNamePart(node, getElementById)
270
275
 
271
- // Full name is ancestor path + this element's name (no separator)
272
- const name = isRoot ? 'root' : ancestorPath + namePart
276
+ // Full name is ancestor path + this element's name (no separator).
277
+ // Use pascalCase on namePart when appending so that
278
+ // e.g. "mediaSection" + "vectorArt" → "mediaSectionVectorArt".
279
+ const name = isRoot ? 'root' : ancestorPath.length > 0 ? ancestorPath + pascalCase(namePart) : namePart
273
280
 
274
281
  // Check for text content
275
282
  const hasText = hasDirectTextContent(node)
@@ -74,20 +74,8 @@ function generateMockValue(propInfo: PropInfo, propName: string, path: string, r
74
74
  return generateValueFromResolvedType(propInfo.resolvedType, propName, path, registrar)
75
75
  }
76
76
 
77
- /**
78
- * Extract the actual value from a DefaultValue object
79
- */
80
77
  function extractDefaultValueValue(defaultValue: DefaultValue): unknown {
81
- switch (defaultValue.kind) {
82
- case 'string':
83
- case 'number':
84
- case 'boolean':
85
- case 'null':
86
- return defaultValue.value
87
- case 'unresolved':
88
- // For unresolved values, return the raw string (e.g., MY_CONST)
89
- return defaultValue.value
90
- }
78
+ return defaultValue.value
91
79
  }
92
80
 
93
81
  /**
@@ -372,6 +372,17 @@ function parseDefaultValueFromString(text: string, fallbackToString = false): De
372
372
  return { kind: 'string', value: trimmed.slice(1, -1) }
373
373
  }
374
374
 
375
+ // Object / array literals: parse via the TS compiler API so single-quoted
376
+ // strings and unquoted keys are handled correctly.
377
+ if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
378
+ const parsed = tryParseStructuredLiteral(trimmed)
379
+ if (parsed !== undefined) {
380
+ return Array.isArray(parsed)
381
+ ? { kind: 'array', value: parsed }
382
+ : { kind: 'object', value: parsed as Record<string, unknown> }
383
+ }
384
+ }
385
+
375
386
  // react-docgen-typescript strips quotes from string defaults, so treat as string
376
387
  if (fallbackToString) {
377
388
  return { kind: 'string', value: trimmed }
@@ -381,6 +392,82 @@ function parseDefaultValueFromString(text: string, fallbackToString = false): De
381
392
  return { kind: 'unresolved', value: trimmed }
382
393
  }
383
394
 
395
+ /**
396
+ * Attempts to parse a string that looks like a TS object or array literal into
397
+ * an actual JavaScript value. Uses `ts.createSourceFile` so that single-quoted
398
+ * strings, unquoted keys, and nested structures are all handled natively.
399
+ *
400
+ * Returns `undefined` when any part of the expression cannot be statically
401
+ * evaluated (references, function calls, binary expressions, etc.).
402
+ */
403
+ function tryParseStructuredLiteral(text: string): Record<string, unknown> | unknown[] | undefined {
404
+ const sourceFile = ts.createSourceFile('__default__.ts', `const __v__ = ${text}`, ts.ScriptTarget.Latest, true)
405
+
406
+ const statement = sourceFile.statements[0]
407
+ if (!statement || !ts.isVariableStatement(statement)) return undefined
408
+
409
+ const initializer = statement.declarationList.declarations[0]?.initializer
410
+ if (!initializer) return undefined
411
+
412
+ return evaluateLiteralExpression(initializer) as Record<string, unknown> | unknown[] | undefined
413
+ }
414
+
415
+ /**
416
+ * Recursively evaluates a TS AST node into a plain JS value.
417
+ * Only pure literals are supported; returns `undefined` for anything dynamic.
418
+ *
419
+ * The sentinel `undefined` means "could not evaluate" -- this is unambiguous
420
+ * because `null` literals evaluate to `null`, and there is no `undefined`
421
+ * keyword in the TS AST (it is a global identifier, not a literal).
422
+ */
423
+ function evaluateLiteralExpression(node: ts.Expression): unknown {
424
+ if (ts.isObjectLiteralExpression(node)) {
425
+ const result: Record<string, unknown> = {}
426
+ for (const property of node.properties) {
427
+ if (!ts.isPropertyAssignment(property)) return undefined
428
+ const key = getStaticPropertyName(property.name)
429
+ if (key === undefined) return undefined
430
+
431
+ const value = evaluateLiteralExpression(property.initializer)
432
+ if (value === undefined) return undefined
433
+ result[key] = value
434
+ }
435
+ return result
436
+ }
437
+
438
+ if (ts.isArrayLiteralExpression(node)) {
439
+ const elements: unknown[] = []
440
+ for (const element of node.elements) {
441
+ const value = evaluateLiteralExpression(element)
442
+ if (value === undefined) return undefined
443
+ elements.push(value)
444
+ }
445
+ return elements
446
+ }
447
+
448
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text
449
+ if (ts.isNumericLiteral(node)) return Number(node.text)
450
+ if (
451
+ ts.isPrefixUnaryExpression(node) &&
452
+ node.operator === ts.SyntaxKind.MinusToken &&
453
+ ts.isNumericLiteral(node.operand)
454
+ ) {
455
+ return -Number(node.operand.text)
456
+ }
457
+ if (node.kind === ts.SyntaxKind.TrueKeyword) return true
458
+ if (node.kind === ts.SyntaxKind.FalseKeyword) return false
459
+ if (node.kind === ts.SyntaxKind.NullKeyword) return null
460
+
461
+ return undefined
462
+ }
463
+
464
+ function getStaticPropertyName(name: ts.PropertyName): string | undefined {
465
+ if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
466
+ return name.text
467
+ }
468
+ return undefined
469
+ }
470
+
384
471
  // ─────────────────────────────────────────────────────────────────────────────
385
472
  // Helpers
386
473
  // ─────────────────────────────────────────────────────────────────────────────
@@ -26,6 +26,7 @@ export interface ResolvedType {
26
26
  /**
27
27
  * Resolved default value with its type.
28
28
  * For literals (string, number, boolean, null), the value is the actual JavaScript value.
29
+ * For structured literals (object, array), the value is the parsed JavaScript value.
29
30
  * For unresolvable values (references, expressions), we store the raw source text.
30
31
  */
31
32
  export type DefaultValue =
@@ -33,7 +34,9 @@ export type DefaultValue =
33
34
  | { kind: 'number'; value: number }
34
35
  | { kind: 'boolean'; value: boolean }
35
36
  | { kind: 'null'; value: null }
36
- | { kind: 'unresolved'; value: string } // For references like MY_CONST or complex expressions
37
+ | { kind: 'object'; value: Record<string, unknown> }
38
+ | { kind: 'array'; value: unknown[] }
39
+ | { kind: 'unresolved'; value: string }
37
40
 
38
41
  export interface PropInfo {
39
42
  name: string