@wix/zero-config-implementation 1.17.0 → 1.19.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,5 +1,9 @@
1
+ export declare function isGlobalSemanticClass(className: string): boolean;
1
2
  /**
2
- * Returns true if the class name looks like a global semantic class
3
- * i.e. not a CSS Module hash-suffixed class.
3
+ * Picks the best semantic class from a list of class names.
4
+ * Prefers a class without a BEM modifier (e.g. `card__header`) over one with
5
+ * a modifier (e.g. `card__header--active`), since the base class is more
6
+ * stable as a selector or element name. Falls back to the first semantic class
7
+ * found if all candidates have modifiers.
4
8
  */
5
- export declare function isGlobalSemanticClass(className: string): boolean;
9
+ export declare function findPreferredSemanticClass(classNames: string[]): string | undefined;
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "registry": "https://registry.npmjs.org/",
5
5
  "access": "public"
6
6
  },
7
- "version": "1.17.0",
7
+ "version": "1.19.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",
@@ -75,5 +75,5 @@
75
75
  ]
76
76
  }
77
77
  },
78
- "falconPackageHash": "453ef72fecd4eac2958f740eea8a7764343b089244f2a0afdd2b9d15"
78
+ "falconPackageHash": "c023fc2f7c36ad9d9d7f5d3e24b22d292e138350cb6a48ae6964e8a0"
79
79
  }
@@ -16,7 +16,7 @@ import type {
16
16
  ElementItem,
17
17
  } from '../schema'
18
18
  import { ELEMENTS } from '../schema'
19
- import { isGlobalSemanticClass } from '../utils/css-class'
19
+ import { findPreferredSemanticClass } from '../utils/css-class'
20
20
  import { buildDataItem } from './data-item-builder'
21
21
  import { formatDisplayName } from './utils'
22
22
 
@@ -45,7 +45,7 @@ function buildEditorElement(component: ComponentInfoWithCss): EditorElement {
45
45
 
46
46
  function buildSelector(rootElement?: ExtractedElement): string {
47
47
  if (rootElement?.attributes.class) {
48
- const semanticClass = rootElement.attributes.class.split(' ').find(isGlobalSemanticClass)
48
+ const semanticClass = findPreferredSemanticClass(rootElement.attributes.class.split(' '))
49
49
  if (semanticClass) return `.${semanticClass}`
50
50
  }
51
51
  return rootElement?.tag ?? ''
@@ -8,7 +8,7 @@
8
8
  import { pascalCase } from 'case-anything'
9
9
  import { type DefaultTreeAdapterMap, parseFragment } from 'parse5'
10
10
  import { TRACE_ATTR } from '../../../../component-renderer'
11
- import { isGlobalSemanticClass } from '../../../../utils/css-class'
11
+ import { findPreferredSemanticClass } from '../../../../utils/css-class'
12
12
  import { PRESETS_WRAPPER_CLASS_NAME } from '../../utils/mock-generator'
13
13
  import type { CssPropertiesData } from '../css-properties'
14
14
  import { addTextProperties } from '../css-properties'
@@ -172,7 +172,7 @@ function getElementNamePart(element: Element, getElementById: (id: string) => El
172
172
 
173
173
  const classAttr = getAttribute(element, 'class')
174
174
  if (classAttr) {
175
- const semanticClass = classAttr.split(' ').find(isGlobalSemanticClass)
175
+ const semanticClass = findPreferredSemanticClass(classAttr.split(' '))
176
176
  if (semanticClass) return pascalCase(semanticClass)
177
177
  }
178
178
 
@@ -1,26 +1,44 @@
1
1
  import * as path from 'node:path'
2
2
  import ts, { type Program } from 'typescript'
3
3
 
4
+ const MAX_DEPTH = 100
5
+
4
6
  export function extractCssImports(program: Program): string[] {
5
7
  const cssFiles = new Set<string>()
8
+ const visited = new Set<string>()
6
9
 
7
- for (const sourceFile of program.getSourceFiles()) {
8
- // Skip external libraries
9
- if (sourceFile.isDeclarationFile || sourceFile.fileName.includes('node_modules')) continue
10
+ function walk(sourceFile: ts.SourceFile, depth: number): void {
11
+ if (depth > MAX_DEPTH) return
12
+ if (visited.has(sourceFile.fileName)) return
13
+ visited.add(sourceFile.fileName)
10
14
 
11
15
  ts.forEachChild(sourceFile, (node) => {
12
- // Look for: import './style.css' or import x from './style.css'
13
- if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
14
- const importPath = node.moduleSpecifier.text
15
-
16
- if (importPath.endsWith('.css')) {
17
- // Resolve relative path to an absolute disk path
18
- const fullPath = path.resolve(path.dirname(sourceFile.fileName), importPath)
19
- cssFiles.add(fullPath)
20
- }
16
+ if (!ts.isImportDeclaration(node) || !ts.isStringLiteral(node.moduleSpecifier)) return
17
+ const importPath = node.moduleSpecifier.text
18
+
19
+ if (importPath.endsWith('.css')) {
20
+ const fullPath = path.resolve(path.dirname(sourceFile.fileName), importPath)
21
+ cssFiles.add(fullPath)
22
+ return
21
23
  }
24
+
25
+ // Resolve the module to a source file in the program
26
+ const resolved = ts.resolveModuleName(importPath, sourceFile.fileName, program.getCompilerOptions(), ts.sys)
27
+ const resolvedFileName = resolved.resolvedModule?.resolvedFileName
28
+ if (!resolvedFileName) return
29
+
30
+ const resolvedSource = program.getSourceFile(resolvedFileName)
31
+ // Skip declaration files (.d.ts and @types packages)
32
+ if (!resolvedSource || resolvedSource.isDeclarationFile) return
33
+ walk(resolvedSource, depth + 1)
22
34
  })
23
35
  }
24
36
 
37
+ for (const sourceFile of program.getSourceFiles()) {
38
+ // Only start walks from project source files (skip .d.ts and node_modules)
39
+ if (sourceFile.isDeclarationFile || sourceFile.fileName.includes('node_modules')) continue
40
+ walk(sourceFile, 0)
41
+ }
42
+
25
43
  return Array.from(cssFiles)
26
44
  }
@@ -1,14 +1,48 @@
1
1
  /**
2
- * Matches CSS Module hash suffixes: one or more underscores followed by a
3
- * segment containing at least one digit (e.g. `_x7k9p2`, `__3xKqP`).
4
- * BEM modifiers like `--primary` or `--active` contain no digits and won't match.
2
+ * A lowercase kebab-case word: starts with a letter, followed by lowercase
3
+ * letters, digits, or hyphens. e.g. `menu`, `item-label`, `foo2`
5
4
  */
6
- const CSS_MODULE_HASH_PATTERN = /_+\w*\d\w*$/
5
+ const KEBAB_WORD = '[a-z][a-z0-9-]*'
7
6
 
8
7
  /**
9
- * Returns true if the class name looks like a global semantic class —
10
- * i.e. not a CSS Module hash-suffixed class.
8
+ * An optional BEM element suffix: `__` followed by a kebab word.
9
+ * e.g. `__item-label` in `menu__item-label`
11
10
  */
11
+ const OPTIONAL_BEM_ELEMENT = `(__${KEBAB_WORD})?`
12
+
13
+ /**
14
+ * A single BEM modifier suffix: `--` followed by a kebab word.
15
+ * e.g. `--primary` in `button--primary`
16
+ */
17
+ const BEM_MODIFIER = `--${KEBAB_WORD}`
18
+
19
+ /**
20
+ * A semantic CSS class name following BEM conventions: a kebab-case block,
21
+ * optionally followed by a `__element` and/or one or more `--modifier` parts.
22
+ * All parts must be lowercase — this rejects CSS Module hash suffixes which
23
+ * contain uppercase letters (e.g. `root__SvhJ-`) or leading underscores
24
+ * (e.g. `_root_x7k9p2`).
25
+ */
26
+ const SEMANTIC_CLASS_PATTERN = new RegExp(`^${KEBAB_WORD}${OPTIONAL_BEM_ELEMENT}(${BEM_MODIFIER})*$`)
27
+
28
+ /**
29
+ * Matches a class that has at least one BEM modifier (`--`).
30
+ * Used to deprioritize modifier classes in favour of the base class.
31
+ */
32
+ const HAS_BEM_MODIFIER = new RegExp(BEM_MODIFIER)
33
+
12
34
  export function isGlobalSemanticClass(className: string): boolean {
13
- return !CSS_MODULE_HASH_PATTERN.test(className)
35
+ return SEMANTIC_CLASS_PATTERN.test(className)
36
+ }
37
+
38
+ /**
39
+ * Picks the best semantic class from a list of class names.
40
+ * Prefers a class without a BEM modifier (e.g. `card__header`) over one with
41
+ * a modifier (e.g. `card__header--active`), since the base class is more
42
+ * stable as a selector or element name. Falls back to the first semantic class
43
+ * found if all candidates have modifiers.
44
+ */
45
+ export function findPreferredSemanticClass(classNames: string[]): string | undefined {
46
+ const semanticClasses = classNames.filter(isGlobalSemanticClass)
47
+ return semanticClasses.find((className) => !HAS_BEM_MODIFIER.test(className)) ?? semanticClasses[0]
14
48
  }