@wix/zero-config-implementation 1.13.0 → 1.15.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.
@@ -5,5 +5,10 @@ export declare function extractAllComponentInfo(program: ts.Program, filePath: s
5
5
  * Extracts component info for only the default-exported component in a file.
6
6
  * Returns `undefined` if the file has no default export or if the default export
7
7
  * is not a recognized React component.
8
+ *
9
+ * Handles the re-export case where the entry file only re-exports the component
10
+ * from another file (e.g. `export { Foo as default } from './Foo'`). In that
11
+ * situation react-docgen-typescript finds no component in the entry file, so we
12
+ * follow the re-export to the defining file and extract from there instead.
8
13
  */
9
14
  export declare function extractDefaultComponentInfo(program: ts.Program, filePath: string): ComponentInfo | undefined;
@@ -1,3 +1,12 @@
1
1
  import { default as ts } from 'typescript';
2
2
  import { ResolvedType } from '../types';
3
- export declare function resolveType(type: ts.Type, checker: ts.TypeChecker, depth?: number, typeString?: string, declaredSymbol?: ts.Symbol): ResolvedType;
3
+ interface ResolveTypeOptions {
4
+ type: ts.Type;
5
+ checker: ts.TypeChecker;
6
+ depth?: number;
7
+ typeString?: string;
8
+ declaredSymbol?: ts.Symbol;
9
+ seenTypes?: Set<ts.Type>;
10
+ }
11
+ export declare function resolveType({ type, checker, depth, typeString, declaredSymbol, seenTypes, }: ResolveTypeOptions): ResolvedType;
12
+ export {};
@@ -7,6 +7,7 @@ export interface ExtractionWarning {
7
7
  componentName: string;
8
8
  phase: 'render' | 'coupling' | 'css' | 'loader' | 'conversion';
9
9
  error: string;
10
+ stack?: string;
10
11
  }
11
12
  export interface ExtractedCssInfo {
12
13
  filePath: 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.13.0",
7
+ "version": "1.15.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",
@@ -23,6 +23,7 @@
23
23
  },
24
24
  "scripts": {
25
25
  "build": "vite build",
26
+ "build:dev": "vite build --mode development",
26
27
  "lint": "biome check src/",
27
28
  "test": "vitest run --passWithNoTests",
28
29
  "test:watch": "vitest --passWithNoTests",
@@ -74,5 +75,5 @@
74
75
  ]
75
76
  }
76
77
  },
77
- "falconPackageHash": "415f1144395df7902b157f7d63d64cd53a60a5f69c460b61c217594b"
78
+ "falconPackageHash": "1d9fc602e0ba357d71e273c769064f5c8c38b293c6fbf5e964991825"
78
79
  }
package/src/index.ts CHANGED
@@ -43,6 +43,7 @@ export interface ExtractionError {
43
43
  componentName: string
44
44
  phase: 'render' | 'coupling' | 'css' | 'loader' | 'conversion'
45
45
  error: string
46
+ stack?: string
46
47
  }
47
48
 
48
49
  // ─────────────────────────────────────────────────────────────────────────────
@@ -66,13 +67,21 @@ export function extractComponentManifest(
66
67
  ): ResultAsync<ManifestResult, InstanceType<typeof NotFoundError> | InstanceType<typeof ParseError>> {
67
68
  // Step 1: Load the compiled package module (non-fatal)
68
69
  return loadModule(compiledEntryPath)
69
- .map((moduleExports) => ({ moduleExports, loaderError: null as string | null }))
70
+ .map((moduleExports) => ({
71
+ moduleExports,
72
+ loaderError: null as string | null,
73
+ loaderStack: undefined as string | undefined,
74
+ }))
70
75
  .orElse((err) =>
71
76
  ResultAsync.fromSafePromise(
72
- Promise.resolve({ moduleExports: null as Record<string, unknown> | null, loaderError: err.message }),
77
+ Promise.resolve({
78
+ moduleExports: null as Record<string, unknown> | null,
79
+ loaderError: err.message,
80
+ loaderStack: err.stack,
81
+ }),
73
82
  ),
74
83
  )
75
- .andThen(({ moduleExports, loaderError }) => {
84
+ .andThen(({ moduleExports, loaderError, loaderStack }) => {
76
85
  const loadComponent: (componentName: string) => ComponentType<unknown> | null = moduleExports
77
86
  ? (name) => findComponent(moduleExports, name)
78
87
  : () => null
@@ -111,6 +120,7 @@ export function extractComponentManifest(
111
120
  componentName: componentInfo.componentName,
112
121
  phase: 'loader',
113
122
  error: loaderError,
123
+ stack: loaderStack,
114
124
  })
115
125
  }
116
126
 
@@ -126,6 +136,7 @@ export function extractComponentManifest(
126
136
  componentName: componentPath,
127
137
  phase: 'css',
128
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,
129
140
  })
130
141
  }
131
142
 
@@ -236,14 +236,20 @@ function generateUnionValue(
236
236
  const types = resolvedType.types ?? []
237
237
 
238
238
  // Prefer string literals for textEnum-like unions
239
- const stringLiteral = types.find((t) => t.kind === 'literal' && typeof t.value === 'string')
239
+ const stringLiteral = types.find(
240
+ (candidateType) => candidateType.kind === 'literal' && typeof candidateType.value === 'string',
241
+ )
240
242
  if (stringLiteral) {
241
243
  return stringLiteral.value
242
244
  }
243
245
 
244
- // Otherwise, use the first type
245
- if (types.length > 0) {
246
- return generateValueFromResolvedType(types[0], propName, path, registrar)
246
+ // Skip undefined/null when picking a representative type — Partial<T> produces T | undefined
247
+ // unions where TypeScript may place undefined first, which would generate useless mock values
248
+ const isNullish = (candidateType: ResolvedType) =>
249
+ candidateType.kind === 'primitive' && (candidateType.value === 'undefined' || candidateType.value === 'null')
250
+ const representative = types.find((candidateType) => !isNullish(candidateType)) ?? types[0]
251
+ if (representative) {
252
+ return generateValueFromResolvedType(representative, propName, path, registrar)
247
253
  }
248
254
 
249
255
  return `mock_${propName}_${faker.string.alphanumeric(6)}`
@@ -317,7 +323,7 @@ function generateObjectValue(
317
323
  */
318
324
  function generateEnumValue(resolvedType: ResolvedType): unknown {
319
325
  const types = resolvedType.types ?? []
320
- const firstLiteral = types.find((t) => t.kind === 'literal')
326
+ const firstLiteral = types.find((candidateType) => candidateType.kind === 'literal')
321
327
 
322
328
  if (firstLiteral) {
323
329
  return firstLiteral.value
@@ -334,12 +340,12 @@ function generateMockFunction(
334
340
  path: string,
335
341
  registrar?: PropSpyRegistrar,
336
342
  ): (...args: unknown[]) => void {
337
- const fn = function mockFn(): void {
343
+ const mockCallback = function mockFn(): void {
338
344
  // No-op mock function
339
345
  }
340
- Object.defineProperty(fn, 'name', { value: `mock_${propName}` })
341
- if (registrar) registrar.registerFunction(path, propName, fn)
342
- return fn
346
+ Object.defineProperty(mockCallback, 'name', { value: `mock_${propName}` })
347
+ if (registrar) registrar.registerFunction(path, propName, mockCallback)
348
+ return mockCallback
343
349
  }
344
350
 
345
351
  // ─────────────────────────────────────────────────────────────────────────────
@@ -358,12 +364,21 @@ function generateMockImage(): Record<string, unknown> {
358
364
  }
359
365
 
360
366
  function generateMockVideo(): Record<string, unknown> {
367
+ const videoUrl = `${faker.internet.url()}/video.mp4`
361
368
  return {
362
- url: `${faker.internet.url()}/video.mp4`,
363
- width: faker.number.int({ min: 640, max: 1920 }),
364
- height: faker.number.int({ min: 480, max: 1080 }),
365
- duration: faker.number.int({ min: 10, max: 300 }),
366
- name: faker.lorem.words(2),
369
+ uri: videoUrl,
370
+ sources: [
371
+ {
372
+ quality: '720p',
373
+ width: 1280,
374
+ height: 720,
375
+ types: [{ format: 'mp4', uri: videoUrl, url: videoUrl }],
376
+ },
377
+ ],
378
+ adaptiveSources: [],
379
+ hasAudio: true,
380
+ fps: 30,
381
+ poster: generateMockImage(),
367
382
  }
368
383
  }
369
384
 
@@ -387,6 +402,7 @@ function generateMockVectorArt(): Record<string, unknown> {
387
402
  return {
388
403
  svgId: faker.string.uuid(),
389
404
  url: `${faker.internet.url()}/icon.svg`,
405
+ svgContent: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/></svg>',
390
406
  }
391
407
  }
392
408
 
@@ -8,9 +8,14 @@ import { resolveType } from './utils/semantic-type-resolver'
8
8
  // ─────────────────────────────────────────────────────────────────────────────
9
9
 
10
10
  const sharedPropFilter = (prop: PropItem) => {
11
- // Exclude props inherited from node_modules (e.g. HTMLAttributes)
11
+ // Exclude props whose every declaration comes from a @types/* package (e.g. HTMLAttributes,
12
+ // React.HTMLProps). Props from non-@types node_modules (e.g. Wix component libraries) are
13
+ // intentionally kept so that props like `label` from StylableButtonPlatformProps are visible.
12
14
  if (prop.declarations && prop.declarations.length > 0) {
13
- return !prop.declarations.every((declaration) => declaration.fileName.includes('node_modules'))
15
+ return !prop.declarations.every((declaration) => {
16
+ const fileName = declaration.fileName
17
+ return fileName.includes('node_modules/@types/') || fileName.includes('node_modules/typescript/')
18
+ })
14
19
  }
15
20
  return true
16
21
  }
@@ -42,7 +47,9 @@ export function extractAllComponentInfo(program: ts.Program, filePath: string):
42
47
  const componentNameResolver = (_exp: ts.Symbol, source: ts.SourceFile): string | undefined => {
43
48
  const moduleSymbol = checker.getSymbolAtLocation(source)
44
49
  if (!moduleSymbol) return undefined
45
- const defaultSym = checker.getExportsOfModule(moduleSymbol).find((s) => s.getName() === 'default')
50
+ const defaultSym = checker
51
+ .getExportsOfModule(moduleSymbol)
52
+ .find((exportSymbol) => exportSymbol.getName() === 'default')
46
53
  if (!defaultSym || !(defaultSym.getFlags() & ts.SymbolFlags.Alias)) return undefined
47
54
  const aliased = checker.getAliasedSymbol(defaultSym)
48
55
  const name = aliased.getName()
@@ -60,13 +67,25 @@ export function extractAllComponentInfo(program: ts.Program, filePath: string):
60
67
  * Extracts component info for only the default-exported component in a file.
61
68
  * Returns `undefined` if the file has no default export or if the default export
62
69
  * is not a recognized React component.
70
+ *
71
+ * Handles the re-export case where the entry file only re-exports the component
72
+ * from another file (e.g. `export { Foo as default } from './Foo'`). In that
73
+ * situation react-docgen-typescript finds no component in the entry file, so we
74
+ * follow the re-export to the defining file and extract from there instead.
63
75
  */
64
76
  export function extractDefaultComponentInfo(program: ts.Program, filePath: string): ComponentInfo | undefined {
65
77
  const defaultName = findDefaultExportName(program, filePath)
66
78
  if (!defaultName) return undefined
67
79
 
68
- const all = extractAllComponentInfo(program, filePath)
69
- return all.find((c) => c.componentName === defaultName)
80
+ const found = extractAllComponentInfo(program, filePath).find((c) => c.componentName === defaultName)
81
+ if (found) return found
82
+
83
+ const definingFilePath = resolveDefaultReExportFilePath(program, filePath)
84
+ if (definingFilePath) {
85
+ return extractAllComponentInfo(program, definingFilePath).find((c) => c.componentName === defaultName)
86
+ }
87
+
88
+ return undefined
70
89
  }
71
90
 
72
91
  // ─────────────────────────────────────────────────────────────────────────────
@@ -75,7 +94,7 @@ export function extractDefaultComponentInfo(program: ts.Program, filePath: strin
75
94
 
76
95
  /**
77
96
  * Resolves the name of the default-exported symbol in a file using the
78
- * TypeScript type checker.
97
+ * TypeScript type checker, with an AST fallback for cross-module re-exports.
79
98
  *
80
99
  * Returns `undefined` for anonymous default exports (e.g. `export default () => ...`)
81
100
  * where no stable name can be determined.
@@ -91,17 +110,83 @@ function findDefaultExportName(program: ts.Program, filePath: string): string |
91
110
  const defaultSymbol = checker.getExportsOfModule(moduleSymbol).find((symbol) => symbol.getName() === 'default')
92
111
  if (!defaultSymbol) return undefined
93
112
 
94
- // For `export default Foo` the symbol is an alias — resolve it to get 'Foo'
95
113
  if (defaultSymbol.getFlags() & ts.SymbolFlags.Alias) {
96
- const aliased = checker.getAliasedSymbol(defaultSymbol)
97
- const name = aliased.getName()
98
- // 'default' means the alias resolved back to an anonymous default export
99
- return name !== 'default' ? name : undefined
114
+ const aliasedSymbol = checker.getAliasedSymbol(defaultSymbol)
115
+ const name = aliasedSymbol.getName()
116
+ // Cross-module re-exports (e.g. `export { Foo as default } from './Foo'`) produce a
117
+ // transient alias with no declarations. Fall back to reading the name from the AST.
118
+ const hasDeclarations = (aliasedSymbol.getDeclarations()?.length ?? 0) > 0
119
+ if (name !== 'default' && hasDeclarations) return name
120
+ }
121
+
122
+ return findDefaultReExportNameFromAST(sourceFile)
123
+ }
124
+
125
+ /**
126
+ * Scans the AST for a named re-export that aliases a symbol to `default`
127
+ * (i.e. `export { Foo as default } from './other'`) and returns the original
128
+ * symbol name (`Foo`).
129
+ */
130
+ function findDefaultReExportNameFromAST(sourceFile: ts.SourceFile): string | undefined {
131
+ const specifier = findDefaultReExportSpecifier(sourceFile)
132
+ // propertyName is the original name before aliasing; when absent, the specifier
133
+ // name is both the local and exported name (which here would be 'default').
134
+ return specifier?.propertyName?.text
135
+ }
136
+
137
+ /**
138
+ * Resolves the absolute file path of the module targeted by a default re-export.
139
+ *
140
+ * For `export { Foo as default } from './other'`, returns the resolved path of
141
+ * `'./other'`. Uses `ts.resolveModuleName` because the type checker's alias chain
142
+ * ends at a transient symbol with no declarations for cross-module re-exports.
143
+ *
144
+ * Returns `undefined` when the file has no default re-export or the module
145
+ * specifier cannot be resolved.
146
+ */
147
+ function resolveDefaultReExportFilePath(program: ts.Program, filePath: string): string | undefined {
148
+ const sourceFile = program.getSourceFile(filePath)
149
+ if (!sourceFile) return undefined
150
+
151
+ const exportDeclaration = findDefaultReExportSpecifier(sourceFile)?.parent?.parent
152
+ if (!exportDeclaration || !ts.isExportDeclaration(exportDeclaration)) return undefined
153
+ if (!exportDeclaration.moduleSpecifier || !ts.isStringLiteral(exportDeclaration.moduleSpecifier)) return undefined
154
+
155
+ const resolveResult = ts.resolveModuleName(
156
+ exportDeclaration.moduleSpecifier.text,
157
+ filePath,
158
+ program.getCompilerOptions(),
159
+ ts.sys,
160
+ )
161
+
162
+ const resolvedPath = resolveResult.resolvedModule?.resolvedFileName
163
+ if (resolvedPath && resolvedPath !== filePath) {
164
+ return resolvedPath
100
165
  }
101
166
 
102
167
  return undefined
103
168
  }
104
169
 
170
+ /**
171
+ * Finds the `ExportSpecifier` node for the specifier that aliases something
172
+ * to `default` in a re-export declaration (e.g. the `Foo as default` part of
173
+ * `export { Foo as default } from './other'`).
174
+ */
175
+ function findDefaultReExportSpecifier(sourceFile: ts.SourceFile): ts.ExportSpecifier | undefined {
176
+ for (const statement of sourceFile.statements) {
177
+ if (!ts.isExportDeclaration(statement)) continue
178
+ if (!statement.moduleSpecifier) continue
179
+ if (!statement.exportClause || !ts.isNamedExports(statement.exportClause)) continue
180
+
181
+ for (const specifier of statement.exportClause.elements) {
182
+ if (specifier.name.text === 'default') {
183
+ return specifier
184
+ }
185
+ }
186
+ }
187
+ return undefined
188
+ }
189
+
105
190
  // ─────────────────────────────────────────────────────────────────────────────
106
191
  // ComponentDoc → ComponentInfo conversion
107
192
  // ─────────────────────────────────────────────────────────────────────────────
@@ -132,7 +217,7 @@ function convertComponentDoc(doc: ComponentDoc, program: ts.Program, checker: ts
132
217
 
133
218
  const typeString = declaredTypeInfo?.name ?? propItem.type.name
134
219
  const resolvedType = propType
135
- ? resolveType(propType, checker, 0, typeString, declaredTypeInfo?.symbol)
220
+ ? resolveType({ type: propType, checker, typeString, declaredSymbol: declaredTypeInfo?.symbol })
136
221
  : { kind: 'primitive' as const, value: propItem.type.name }
137
222
 
138
223
  // Convert default value
@@ -119,18 +119,49 @@ function checkForReactNode(type: ts.Type, checker: ts.TypeChecker): boolean {
119
119
  return false
120
120
  }
121
121
 
122
- export function resolveType(
123
- type: ts.Type,
124
- checker: ts.TypeChecker,
122
+ interface ResolveTypeOptions {
123
+ type: ts.Type
124
+ checker: ts.TypeChecker
125
+ depth?: number
126
+ typeString?: string
127
+ declaredSymbol?: ts.Symbol
128
+ seenTypes?: Set<ts.Type>
129
+ }
130
+
131
+ export function resolveType({
132
+ type,
133
+ checker,
125
134
  depth = 0,
126
- typeString?: string,
127
- declaredSymbol?: ts.Symbol,
128
- ): ResolvedType {
129
- // Prevent infinite recursion
135
+ typeString,
136
+ declaredSymbol,
137
+ seenTypes = new Set(),
138
+ }: ResolveTypeOptions): ResolvedType {
139
+ // Prevent infinite recursion on self-referential types
140
+ if (seenTypes.has(type)) {
141
+ return { kind: 'primitive', value: 'any' }
142
+ }
130
143
  if (depth > MAX_RESOLVE_DEPTH) {
131
144
  throw new Error(`Max resolve depth reached for type: ${checker.typeToString(type)}`)
132
145
  }
146
+ // Track the type while we recurse into it. We delete it after resolution so
147
+ // that sibling properties sharing the same ts.Type (e.g. two `string` props)
148
+ // are not misidentified as cycles. Only types currently on the call stack
149
+ // trigger the cycle guard above.
150
+ seenTypes.add(type)
151
+ const result = resolveTypeBody({ type, checker, depth, typeString, declaredSymbol, seenTypes })
152
+ seenTypes.delete(type)
153
+ return result
154
+ }
133
155
 
156
+ function resolveTypeBody({
157
+ type,
158
+ checker,
159
+ depth,
160
+ typeString,
161
+ declaredSymbol,
162
+ seenTypes,
163
+ }: Required<Pick<ResolveTypeOptions, 'type' | 'checker' | 'seenTypes'>> &
164
+ Pick<ResolveTypeOptions, 'typeString' | 'declaredSymbol'> & { depth: number }): ResolvedType {
134
165
  // Check for semantic types from React or Wix packages
135
166
  const semanticType = checkForSemanticType(type, checker)
136
167
  if (semanticType) {
@@ -182,17 +213,17 @@ export function resolveType(
182
213
  const resolvers = [
183
214
  () => resolvePrimitiveType(type),
184
215
  () => resolveLiteralType(type, checker),
185
- () => resolveUnionType(type, checker, depth),
186
- () => resolveIntersectionType(type, checker, depth),
187
- () => resolveArrayType(type, checker, depth),
216
+ () => resolveUnionType(type, checker, depth, seenTypes),
217
+ () => resolveIntersectionType(type, checker, depth, seenTypes),
218
+ () => resolveArrayType(type, checker, depth, seenTypes),
188
219
  () => resolveFunctionType(type, checker),
189
- () => resolveObjectType(type, checker, depth),
220
+ () => resolveObjectType(type, checker, depth, seenTypes),
190
221
  ]
191
222
 
192
223
  for (const resolver of resolvers) {
193
- const result = resolver()
194
- if (result) {
195
- return result
224
+ const resolved = resolver()
225
+ if (resolved) {
226
+ return resolved
196
227
  }
197
228
  }
198
229
 
@@ -253,36 +284,55 @@ function resolveLiteralType(type: ts.Type, checker: ts.TypeChecker): ResolvedTyp
253
284
  return undefined
254
285
  }
255
286
 
256
- function resolveUnionType(type: ts.Type, checker: ts.TypeChecker, depth: number): ResolvedType | undefined {
287
+ function resolveUnionType(
288
+ type: ts.Type,
289
+ checker: ts.TypeChecker,
290
+ depth: number,
291
+ seenTypes: Set<ts.Type>,
292
+ ): ResolvedType | undefined {
257
293
  if (type.flags & ts.TypeFlags.Union) {
258
294
  const unionType = type as ts.UnionType
259
295
  return {
260
296
  kind: 'union',
261
- types: unionType.types.map((t) => resolveType(t, checker, depth + 1)),
297
+ types: unionType.types.map((memberType) =>
298
+ resolveType({ type: memberType, checker, depth: depth + 1, seenTypes }),
299
+ ),
262
300
  }
263
301
  }
264
302
  return undefined
265
303
  }
266
304
 
267
- function resolveIntersectionType(type: ts.Type, checker: ts.TypeChecker, depth: number): ResolvedType | undefined {
305
+ function resolveIntersectionType(
306
+ type: ts.Type,
307
+ checker: ts.TypeChecker,
308
+ depth: number,
309
+ seenTypes: Set<ts.Type>,
310
+ ): ResolvedType | undefined {
268
311
  if (type.flags & ts.TypeFlags.Intersection) {
269
312
  const intersectionType = type as ts.IntersectionType
270
313
  return {
271
314
  kind: 'intersection',
272
- types: intersectionType.types.map((t) => resolveType(t, checker, depth + 1)),
315
+ types: intersectionType.types.map((memberType) =>
316
+ resolveType({ type: memberType, checker, depth: depth + 1, seenTypes }),
317
+ ),
273
318
  }
274
319
  }
275
320
  return undefined
276
321
  }
277
322
 
278
- function resolveArrayType(type: ts.Type, checker: ts.TypeChecker, depth: number): ResolvedType | undefined {
323
+ function resolveArrayType(
324
+ type: ts.Type,
325
+ checker: ts.TypeChecker,
326
+ depth: number,
327
+ seenTypes: Set<ts.Type>,
328
+ ): ResolvedType | undefined {
279
329
  if (checker.isArrayType(type)) {
280
330
  const arrayType = type as ts.TypeReference
281
331
  const typeArgs = checker.getTypeArguments(arrayType)
282
332
  if (typeArgs.length > 0) {
283
333
  return {
284
334
  kind: 'array',
285
- elementType: resolveType(typeArgs[0], checker, depth + 1),
335
+ elementType: resolveType({ type: typeArgs[0], checker, depth: depth + 1, seenTypes }),
286
336
  }
287
337
  }
288
338
  return { kind: 'array', elementType: { kind: 'primitive', value: 'any' } }
@@ -314,7 +364,12 @@ function resolveFunctionType(type: ts.Type, checker: ts.TypeChecker): ResolvedTy
314
364
  return undefined
315
365
  }
316
366
 
317
- function resolveObjectType(type: ts.Type, checker: ts.TypeChecker, depth: number): ResolvedType | undefined {
367
+ function resolveObjectType(
368
+ type: ts.Type,
369
+ checker: ts.TypeChecker,
370
+ depth: number,
371
+ seenTypes: Set<ts.Type>,
372
+ ): ResolvedType | undefined {
318
373
  const properties = type.getProperties()
319
374
  if (properties.length === 0) {
320
375
  return undefined
@@ -337,7 +392,7 @@ function resolveObjectType(type: ts.Type, checker: ts.TypeChecker, depth: number
337
392
  name: propName,
338
393
  type: checker.typeToString(propType),
339
394
  required: !isOptional,
340
- resolvedType: resolveType(propType, checker, depth + 1),
395
+ resolvedType: resolveType({ type: propType, checker, depth: depth + 1, seenTypes }),
341
396
  }
342
397
  }
343
398
 
@@ -27,6 +27,7 @@ export interface ExtractionWarning {
27
27
  componentName: string
28
28
  phase: 'render' | 'coupling' | 'css' | 'loader' | 'conversion'
29
29
  error: string
30
+ stack?: string
30
31
  }
31
32
 
32
33
  export interface ExtractedCssInfo {
@@ -84,6 +85,7 @@ export function processComponent(
84
85
  componentName: componentInfo.componentName,
85
86
  phase: 'loader',
86
87
  error: `Failed to load "${componentInfo.componentName}": ${error instanceof Error ? error.message : String(error)}`,
88
+ stack: error instanceof Error ? error.stack : undefined,
87
89
  })
88
90
  }
89
91
  }
@@ -116,6 +118,7 @@ export function processComponent(
116
118
  componentName: componentInfo.componentName,
117
119
  phase: 'render',
118
120
  error: error instanceof Error ? error.message : String(error),
121
+ stack: error instanceof Error ? error.stack : undefined,
119
122
  })
120
123
  }
121
124
  }
@@ -156,6 +159,7 @@ export function processComponent(
156
159
  componentName: componentInfo.componentName,
157
160
  phase: 'css',
158
161
  error: `CSS selector matching failed: ${error instanceof Error ? error.message : String(error)}`,
162
+ stack: error instanceof Error ? error.stack : undefined,
159
163
  })
160
164
  }
161
165
  }
@@ -360,6 +364,7 @@ function extractCssInfo(
360
364
  componentName,
361
365
  phase: 'css',
362
366
  error: `Failed to parse ${cssPath}: ${error instanceof Error ? error.message : String(error)}`,
367
+ stack: error instanceof Error ? error.stack : undefined,
363
368
  })
364
369
  }
365
370
  }
@@ -30,12 +30,5 @@ export function compileTsFile(
30
30
  cause: error as Error,
31
31
  props: { phase: 'compile' },
32
32
  }),
33
- ).map(({ tsconfig, tsconfigFile }) => {
34
- // Parse the JSON config through TypeScript's API to handle inheritance,
35
- // convert string values (like "ES2020") to enum values, and process extends
36
- const configDir = path.dirname(tsconfigFile)
37
- const parsedConfig = ts.parseJsonConfigFileContent(tsconfig, ts.sys, configDir)
38
-
39
- return ts.createProgram([filePath], parsedConfig.options)
40
- })
33
+ ).map(({ result }) => ts.createProgram([filePath], result.options))
41
34
  }
package/vite.config.ts CHANGED
@@ -3,7 +3,7 @@ import { resolve } from 'node:path'
3
3
  import { defineConfig } from 'vite'
4
4
  import dts from 'vite-plugin-dts'
5
5
 
6
- export default defineConfig({
6
+ export default defineConfig(({ mode }) => ({
7
7
  plugins: [
8
8
  dts({
9
9
  include: ['src/**/*.ts'],
@@ -11,6 +11,7 @@ export default defineConfig({
11
11
  }),
12
12
  ],
13
13
  build: {
14
+ minify: mode !== 'development',
14
15
  lib: {
15
16
  entry: {
16
17
  index: resolve(__dirname, 'src/index.ts'),
@@ -48,4 +49,4 @@ export default defineConfig({
48
49
  test: {
49
50
  globals: true,
50
51
  },
51
- })
52
+ }))