@wix/zero-config-implementation 1.25.0 → 1.27.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,6 +1,8 @@
1
1
  export interface CSSProperty {
2
2
  name: string;
3
3
  value: string;
4
+ /** CSS variable names referenced via var() in this property's value */
5
+ varRefs?: string[];
4
6
  }
5
7
  export interface CssSelectorMatch {
6
8
  selector: string;
@@ -2,18 +2,13 @@ import { ComponentInfo } from './information-extractors/ts';
2
2
  import { RunExtractorsOptions, CoupledComponentInfo } from './information-extractors/react';
3
3
  import { CSSParserAPI } from './information-extractors/css';
4
4
  import { ComponentType } from 'react';
5
- /** A non-fatal issue encountered during component processing. */
6
- export interface ExtractionWarning {
7
- componentName: string;
8
- phase: 'render' | 'coupling' | 'css' | 'loader' | 'conversion';
9
- error: string;
10
- stack?: string;
11
- }
5
+ import { ExtractionError } from './extraction-types';
12
6
  export interface ExtractedCssInfo {
13
7
  filePath: string;
14
8
  api: CSSParserAPI;
15
9
  properties: Map<string, string>;
16
10
  customProperties: Map<string, string>;
11
+ isCssModule: boolean;
17
12
  }
18
13
  export interface ComponentInfoWithCss extends CoupledComponentInfo {
19
14
  css: ExtractedCssInfo[];
@@ -22,7 +17,6 @@ export interface ComponentInfoWithCss extends CoupledComponentInfo {
22
17
  /** The result of processing a single component through the manifest pipeline. */
23
18
  export interface ProcessComponentResult {
24
19
  component: ComponentInfoWithCss;
25
- warnings: ExtractionWarning[];
26
20
  }
27
21
  /**
28
22
  * Processes a single component through the full manifest pipeline:
@@ -32,4 +26,4 @@ export interface ProcessComponentResult {
32
26
  * encountered during processing. Always returns a component, falling back
33
27
  * to minimal info (without DOM coupling) when rendering fails.
34
28
  */
35
- export declare function processComponent(componentInfo: ComponentInfo, loadComponent: (componentName: string) => ComponentType<unknown> | null, cssImportPaths: string[], loaderHasError?: boolean, options?: RunExtractorsOptions): ProcessComponentResult;
29
+ export declare function processComponent(componentInfo: ComponentInfo, loadComponent: (componentName: string) => ComponentType<unknown> | null, cssImportPaths: string[], loaderHasError: boolean, report: (error: ExtractionError) => void, options?: RunExtractorsOptions): ProcessComponentResult;
@@ -1,7 +1,15 @@
1
1
  import { ResultAsync } from 'neverthrow';
2
2
  import { ComponentType } from 'react';
3
- import { IoError } from './errors';
4
- type IoErrorInstance = InstanceType<typeof IoError>;
3
+ /**
4
+ * Structured failure from `loadModule` when both ESM import and CJS require fail.
5
+ * Carries each error separately so callers can report them independently.
6
+ */
7
+ export interface LoadModuleFailure {
8
+ /** The ESM import error, or null when no ESM was attempted (e.g. empty path). */
9
+ esmError: Error | null;
10
+ /** The CJS require error, or a generic error for the empty-path case. */
11
+ cjsError: Error;
12
+ }
5
13
  /**
6
14
  * Attempts to load a module, first via ESM `import()`, then via CJS `require`.
7
15
  *
@@ -13,8 +21,7 @@ type IoErrorInstance = InstanceType<typeof IoError>;
13
21
  *
14
22
  * @param entryPath - Absolute path to the module entry point.
15
23
  * @returns A `ResultAsync` containing the module exports on success.
16
- * @errors {IoError} When both ESM import and CJS require fail.
24
+ * @errors {LoadModuleFailure} When both ESM import and CJS require fail.
17
25
  */
18
- export declare function loadModule(entryPath: string): ResultAsync<Record<string, unknown>, IoErrorInstance>;
26
+ export declare function loadModule(entryPath: string): ResultAsync<Record<string, unknown>, LoadModuleFailure>;
19
27
  export declare function findComponent(moduleExports: Record<string, unknown>, name: string): ComponentType<unknown> | null;
20
- export {};
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "registry": "https://registry.npmjs.org/",
5
5
  "access": "public"
6
6
  },
7
- "version": "1.25.0",
7
+ "version": "1.27.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": "e76e4e02feadede0d8c9d011d32118218b349b5ab948a12605907e98"
86
+ "falconPackageHash": "2e095856ec4492a70d52bef24a3f7ac22639454039660a7869423748"
87
87
  }
@@ -0,0 +1,8 @@
1
+ import type { BaseError } from './errors'
2
+
3
+ /** A non-fatal issue encountered during component extraction. */
4
+ export interface ExtractionError {
5
+ componentName: string
6
+ phase: 'render' | 'coupling' | 'css' | 'loader' | 'conversion'
7
+ error: InstanceType<typeof BaseError>
8
+ }
package/src/index.ts CHANGED
@@ -1,32 +1,16 @@
1
1
  import type { EditorReactComponent } from '@wix/zero-config-schema'
2
- import { Result, ResultAsync } from 'neverthrow'
2
+ import { Result, type ResultAsync, okAsync } from 'neverthrow'
3
3
  import type { ComponentType } from 'react'
4
4
 
5
- import { extractAllComponentInfo, extractCssImports, extractDefaultComponentInfo } from './information-extractors/ts'
6
- import type { ComponentInfo } from './information-extractors/ts'
7
- // TypeScript extraction
8
- import { compileTsFile } from './ts-compiler'
9
-
10
- // Error classes
11
- import { type NotFoundError, ParseError } from './errors'
12
-
13
- // Component loader helpers
14
- import { findComponent, loadModule } from './module-loader'
15
-
5
+ import { toEditorReactComponent } from './converters'
6
+ import { IoError, type NotFoundError, ParseError } from './errors'
7
+ import type { ExtractionError } from './extraction-types'
16
8
  import type { RunExtractorsOptions } from './information-extractors/react'
17
- // Pipeline orchestration
9
+ import { extractCssImports, extractDefaultComponentInfo } from './information-extractors/ts'
18
10
  import { processComponent } from './manifest-pipeline'
19
- export type {
20
- ComponentInfoWithCss,
21
- ExtractedCssInfo,
22
- ExtractionWarning,
23
- ProcessComponentResult,
24
- } from './manifest-pipeline'
25
-
26
- export type { EditorReactComponent } from '@wix/zero-config-schema'
27
-
28
- // Converter
29
- import { toEditorReactComponent } from './converters'
11
+ import { findComponent, loadModule } from './module-loader'
12
+ import type { LoadModuleFailure } from './module-loader'
13
+ import { compileTsFile } from './ts-compiler'
30
14
 
31
15
  // ─────────────────────────────────────────────────────────────────────────────
32
16
  // Types
@@ -37,15 +21,6 @@ export interface ManifestResult {
37
21
  errors: ExtractionError[]
38
22
  }
39
23
 
40
- export type { RunExtractorsOptions } from './information-extractors/react'
41
-
42
- export interface ExtractionError {
43
- componentName: string
44
- phase: 'render' | 'coupling' | 'css' | 'loader' | 'conversion'
45
- error: string
46
- stack?: string
47
- }
48
-
49
24
  // ─────────────────────────────────────────────────────────────────────────────
50
25
  // Main API
51
26
  // ─────────────────────────────────────────────────────────────────────────────
@@ -55,96 +30,100 @@ export interface ExtractionError {
55
30
  *
56
31
  * @param componentPath - Path to the TypeScript source file
57
32
  * @param compiledEntryPath - Path to the built JS entry file of the component's package (e.g. dist/index.js)
58
- * @returns The manifest result containing components and non-fatal warnings
33
+ * @param options.onError - Called for each non-fatal extraction error as it occurs
59
34
  * @errors
60
35
  * - {@link NotFoundError} — Source file does not exist (phase: `compile`)
61
36
  * - {@link ParseError} — TypeScript config or component types could not be parsed (phase: `compile` | `extract`)
62
37
  */
38
+ export interface ExtractComponentManifestOptions extends RunExtractorsOptions {
39
+ onError?: (error: ExtractionError) => void
40
+ }
41
+
63
42
  export function extractComponentManifest(
64
43
  componentPath: string,
65
44
  compiledEntryPath: string,
66
- options?: RunExtractorsOptions,
45
+ options?: ExtractComponentManifestOptions,
67
46
  ): ResultAsync<ManifestResult, InstanceType<typeof NotFoundError> | InstanceType<typeof ParseError>> {
68
- // Step 1: Load the compiled package module (non-fatal)
69
- return loadModule(compiledEntryPath)
70
- .map((moduleExports) => ({
71
- moduleExports,
72
- loaderError: null as string | null,
73
- loaderStack: undefined as string | undefined,
74
- }))
75
- .orElse((err) =>
76
- ResultAsync.fromSafePromise(
77
- Promise.resolve({
78
- moduleExports: null as Record<string, unknown> | null,
79
- loaderError: err.message,
80
- loaderStack: err.stack,
81
- }),
82
- ),
83
- )
84
- .andThen(({ moduleExports, loaderError, loaderStack }) => {
85
- const loadComponent: (componentName: string) => ComponentType<unknown> | null = moduleExports
86
- ? (name) => findComponent(moduleExports, name)
87
- : () => null
88
-
89
- // Step 2: Compile TypeScript (fatal)
90
- return compileTsFile(componentPath)
91
- .andThen((program) => {
92
- // Step 3: Extract default-exported component types (fatal)
93
- const safeExtract = Result.fromThrowable(
94
- (prog: typeof program) => {
95
- const componentInfo = extractDefaultComponentInfo(prog, componentPath)
96
- if (!componentInfo) {
97
- throw new Error(`No default export found in "${componentPath}"`)
98
- }
99
- return componentInfo
100
- },
101
- (thrown) =>
102
- new ParseError(
103
- `Failed to extract component types from "${componentPath}": ${thrown instanceof Error ? thrown.message : String(thrown)}`,
104
- {
105
- cause: thrown as Error,
106
- props: { phase: 'extract' },
107
- },
108
- ),
109
- )
110
- const componentInfoResult = safeExtract(program)
111
-
112
- return componentInfoResult.map((componentInfo) => ({ program, componentInfo }))
113
- })
114
- .map(({ program, componentInfo }) => {
115
- const errors: ExtractionError[] = []
116
-
117
- // Surface loader error as a non-fatal error
118
- if (loaderError) {
119
- errors.push({
120
- componentName: componentInfo.componentName,
47
+ const errors: ExtractionError[] = []
48
+ const report = (error: ExtractionError): void => {
49
+ errors.push(error)
50
+ options?.onError?.(error)
51
+ }
52
+
53
+ // Step 1: Compile TypeScript (fatal)
54
+ return compileTsFile(componentPath)
55
+ .andThen((program) => {
56
+ // Step 2: Extract default-exported component types (fatal)
57
+ const safeExtract = Result.fromThrowable(
58
+ (prog: typeof program) => {
59
+ const componentInfo = extractDefaultComponentInfo(prog, componentPath)
60
+ if (!componentInfo) {
61
+ throw new Error(`No default export found in "${componentPath}"`)
62
+ }
63
+ return componentInfo
64
+ },
65
+ (thrown) =>
66
+ new ParseError(
67
+ `Failed to extract component types from "${componentPath}": ${thrown instanceof Error ? thrown.message : String(thrown)}`,
68
+ { cause: thrown as Error, props: { phase: 'extract' } },
69
+ ),
70
+ )
71
+ return safeExtract(program).map((componentInfo) => ({ program, componentInfo }))
72
+ })
73
+ .andThen(({ program, componentInfo }) => {
74
+ const { componentName } = componentInfo
75
+
76
+ // Step 3: Load the compiled package module (non-fatal) done after TS extraction
77
+ // so componentName is known when reporting failures
78
+ return loadModule(compiledEntryPath)
79
+ .map((moduleExports) => ({
80
+ loadComponent: (name: string) => findComponent(moduleExports, name),
81
+ failure: null as LoadModuleFailure | null,
82
+ }))
83
+ .orElse((failure) => okAsync({ loadComponent: () => null as ComponentType<unknown> | null, failure }))
84
+ .map(({ loadComponent, failure }) => {
85
+ if (failure) {
86
+ if (failure.esmError) {
87
+ report({
88
+ componentName,
89
+ phase: 'loader',
90
+ error: new IoError('ESM import failed', { cause: failure.esmError, props: { phase: 'loader' } }),
91
+ })
92
+ }
93
+ report({
94
+ componentName,
121
95
  phase: 'loader',
122
- error: loaderError,
123
- stack: loaderStack,
96
+ error: new IoError('CJS require failed', { cause: failure.cjsError, props: { phase: 'loader' } }),
124
97
  })
125
98
  }
126
99
 
127
100
  // Step 4: Extract CSS imports (non-fatal)
128
101
  let cssImportPaths: string[] = []
129
- const safeCssImports = Result.fromThrowable(extractCssImports, (thrown) => thrown)
130
- const cssResult = safeCssImports(program)
102
+ const cssResult = Result.fromThrowable(extractCssImports, (thrown) => thrown)(program)
131
103
  if (cssResult.isOk()) {
132
104
  cssImportPaths = cssResult.value
133
105
  } else {
134
106
  const thrown = cssResult.error
135
- errors.push({
136
- componentName: componentPath,
107
+ report({
108
+ componentName,
137
109
  phase: 'css',
138
- error: `Failed to extract CSS imports: ${thrown instanceof Error ? (thrown as Error).message : String(thrown)}`,
139
- stack: thrown instanceof Error ? (thrown as Error).stack : undefined,
110
+ error: new ParseError(
111
+ `Failed to extract CSS imports: ${thrown instanceof Error ? thrown.message : String(thrown)}`,
112
+ { cause: thrown instanceof Error ? thrown : undefined, props: { phase: 'css' } },
113
+ ),
140
114
  })
141
115
  }
142
116
 
143
117
  // Step 5: Process the default-exported component (non-fatal)
144
- const processResult = processComponent(componentInfo, loadComponent, cssImportPaths, !!loaderError, options)
145
- errors.push(...processResult.warnings)
118
+ const processResult = processComponent(
119
+ componentInfo,
120
+ loadComponent,
121
+ cssImportPaths,
122
+ !!failure,
123
+ report,
124
+ options,
125
+ )
146
126
  const component = toEditorReactComponent(processResult.component)
147
-
148
127
  return { component, errors }
149
128
  })
150
129
  })
@@ -171,6 +150,9 @@ export function extractComponentManifest(
171
150
 
172
151
  // ── Tier 1: High-Level API ──────────────────────────────────────────────────
173
152
  // extractComponentManifest() is exported above as a named function declaration.
153
+ export type { ExtractionError } from './extraction-types'
154
+ export type { EditorReactComponent } from '@wix/zero-config-schema'
155
+ export type { ComponentInfoWithCss, ExtractedCssInfo, ProcessComponentResult } from './manifest-pipeline'
174
156
 
175
157
  // ── Tier 2: Pipeline Building Blocks ────────────────────────────────────────
176
158
 
@@ -187,6 +169,7 @@ export type {
187
169
  } from './information-extractors/ts/types'
188
170
 
189
171
  /** React render-time extraction */
172
+ export type { RunExtractorsOptions } from './information-extractors/react'
190
173
  export {
191
174
  runExtractors,
192
175
  ExtractorStore,
@@ -204,8 +187,6 @@ export type {
204
187
  PropTrackerData,
205
188
  PropTrackerExtractorState,
206
189
  CssPropertiesData,
207
- } from './information-extractors/react'
208
- export type {
209
190
  CoupledComponentInfo,
210
191
  CoupledProp,
211
192
  DOMBinding,
@@ -231,6 +212,7 @@ export {
231
212
 
232
213
  /** Module loader primitives */
233
214
  export { loadModule, findComponent } from './module-loader'
215
+ export type { LoadModuleFailure } from './module-loader'
234
216
 
235
217
  // ── Tier 3: Low-Level Renderer ──────────────────────────────────────────────
236
218
 
@@ -345,7 +345,8 @@ function extractPropertyNameAndValue(decl: LightningDecl): CSSProperty | null {
345
345
  if (name) {
346
346
  const value = propertyValueToString(decl)
347
347
  if (value) {
348
- return { name, value }
348
+ const varRefs = extractVarNamesFromTokens(unparsedValue.value ?? [])
349
+ return { name, value, ...(varRefs.length > 0 && { varRefs }) }
349
350
  }
350
351
  }
351
352
  return null
@@ -412,7 +413,7 @@ function serializeCustomPropertyValue(valueArray: unknown[]): string {
412
413
  } else if (tokenValue.type === 'dimension') {
413
414
  parts.push(`${tokenValue.value}${tokenValue.unit}`)
414
415
  } else if (tokenValue.type === 'white-space') {
415
- parts.push(tokenValue.value as string)
416
+ // skip — parts.join(' ') already handles separation
416
417
  }
417
418
  }
418
419
  }
@@ -4,6 +4,45 @@ import type { ExtractedCssInfo } from '../../index'
4
4
  import type { ExtractedElement } from '../react/extractors/core/tree-builder'
5
5
  import type { CSSProperty, CssSelectorMatch, MatchedCssData } from './types'
6
6
 
7
+ /**
8
+ * Matches selectors composed entirely of one or more class selectors with no combinators,
9
+ * pseudo-classes, tag names, or IDs — e.g. `.wrapper`, `.wrapper.active`.
10
+ * Used to identify selectors eligible for CSS module prefix matching.
11
+ */
12
+ const SIMPLE_CLASS_SELECTOR_PATTERN = /^(\.[a-zA-Z][\w-]*)+$/
13
+
14
+ /**
15
+ * Finds DOM elements matching a CSS selector, with special handling for CSS module files.
16
+ *
17
+ * For CSS module files, class names in the source CSS (e.g. `.wrapper`) are hashed at build
18
+ * time into names like `wrapper_AbCdE`. When the selector is a simple class selector
19
+ * (e.g. `.wrapper` or `.wrapper.active`), this function matches elements whose class list
20
+ * contains a class that exactly equals the local name OR starts with `localName_` /
21
+ * `_localName_` (the two most common Vite CSS module hash patterns).
22
+ *
23
+ * For non-module files, or for selectors that contain combinators or non-class parts
24
+ * (e.g. `div .wrapper`, `#id`), falls back to a direct Cheerio query.
25
+ */
26
+ function findMatchingElements($: cheerio.CheerioAPI, domSelector: string, isCssModule: boolean) {
27
+ if (isCssModule && SIMPLE_CLASS_SELECTOR_PATTERN.test(domSelector)) {
28
+ const classNames = domSelector.split('.').filter(Boolean)
29
+ return $('[class]').filter((_index, element) => {
30
+ const classAttr = $(element).attr('class')
31
+ if (!classAttr) return false
32
+ const elementClasses = classAttr.trim().split(/\s+/)
33
+ return classNames.every((localName) =>
34
+ elementClasses.some(
35
+ (elementClass) =>
36
+ elementClass === localName ||
37
+ elementClass.startsWith(`${localName}_`) ||
38
+ elementClass.startsWith(`_${localName}_`),
39
+ ),
40
+ )
41
+ })
42
+ }
43
+ return $(domSelector)
44
+ }
45
+
7
46
  export function matchCssSelectors(
8
47
  html: string,
9
48
  elements: ExtractedElement[],
@@ -28,8 +67,8 @@ export function matchCssSelectors(
28
67
  if (!domSelector) continue
29
68
 
30
69
  try {
31
- $(domSelector).each((_, elem) => {
32
- const traceId = $(elem).attr(TRACE_ATTR)
70
+ findMatchingElements($, domSelector, cssInfo.isCssModule).each((_index, element) => {
71
+ const traceId = $(element).attr(TRACE_ATTR)
33
72
  if (!traceId) return
34
73
 
35
74
  // Use reduce to separate regular and custom properties in one pass
@@ -37,13 +76,13 @@ export function matchCssSelectors(
37
76
  regular: CSSProperty[]
38
77
  custom: Record<string, string>
39
78
  }>(
40
- (acc, prop) => {
79
+ (accumulator, prop) => {
41
80
  if (prop.name.startsWith('--')) {
42
- acc.custom[prop.name] = prop.value
81
+ accumulator.custom[prop.name] = prop.value
43
82
  } else {
44
- acc.regular.push(prop)
83
+ accumulator.regular.push(prop)
45
84
  }
46
- return acc
85
+ return accumulator
47
86
  },
48
87
  { regular: [], custom: {} },
49
88
  )
@@ -55,7 +94,7 @@ export function matchCssSelectors(
55
94
 
56
95
  // Track which CSS custom properties are used (via var()) by this element
57
96
  for (const regularProp of regular) {
58
- for (const varName of extractVarRefs(regularProp.value)) {
97
+ for (const varName of regularProp.varRefs ?? []) {
59
98
  const traceIdSet = varUsedByTraceId.get(varName) ?? new Set()
60
99
  traceIdSet.add(traceId)
61
100
  varUsedByTraceId.set(varName, traceIdSet)
@@ -81,20 +120,6 @@ export function matchCssSelectors(
81
120
  }
82
121
  }
83
122
 
84
- /**
85
- * Extracts all CSS custom property names referenced via var() in a property value string.
86
- */
87
- function extractVarRefs(value: string): string[] {
88
- const varNames: string[] = []
89
- const varPattern = /var\(\s*(--[\w-]+)/g
90
- let varMatch = varPattern.exec(value)
91
- while (varMatch !== null) {
92
- varNames.push(varMatch[1])
93
- varMatch = varPattern.exec(value)
94
- }
95
- return varNames
96
- }
97
-
98
123
  function enrichElements(
99
124
  elements: ExtractedElement[],
100
125
  matchesByTraceId: Map<string, CssSelectorMatch[]>,
@@ -1,6 +1,8 @@
1
1
  export interface CSSProperty {
2
2
  name: string
3
3
  value: string
4
+ /** CSS variable names referenced via var() in this property's value */
5
+ varRefs?: string[]
4
6
  }
5
7
 
6
8
  export interface CssSelectorMatch {