@krapwoo/ds-viewer 0.1.1 → 0.3.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.
Files changed (173) hide show
  1. package/README.md +182 -78
  2. package/dist/cli/agentsFile.js +31 -0
  3. package/dist/cli/config.js +26 -1
  4. package/dist/cli/dev.js +50 -5
  5. package/dist/cli/doctor.js +492 -0
  6. package/dist/cli/explain.js +151 -0
  7. package/dist/cli/githubAction.js +68 -0
  8. package/dist/cli/glob.js +8 -0
  9. package/dist/cli/init.js +152 -18
  10. package/dist/cli/main.js +115 -4
  11. package/dist/cli/pageIndex.js +27 -1
  12. package/dist/cli/props.js +170 -21
  13. package/dist/cli/staticPage.js +356 -0
  14. package/dist/cli/sync.js +28 -5
  15. package/dist/cli/workspace.js +49 -21
  16. package/dist/native/catalog/comparison.js +283 -0
  17. package/dist/native/catalog/tokens.js +119 -0
  18. package/dist/native/catalog/types.js +1 -0
  19. package/native/catalog/CatalogShell.tsx +6 -1
  20. package/native/catalog/CatalogSidebar.tsx +98 -5
  21. package/native/catalog/ComparisonGrid.tsx +9 -7
  22. package/native/catalog/SpacingScaleGallery.tsx +1 -1
  23. package/native/catalog/TokenRow.tsx +3 -2
  24. package/native/catalog/catalogNavigation.ts +1 -1
  25. package/native/catalog/comparison.ts +63 -5
  26. package/native/catalog/index.ts +8 -6
  27. package/native/catalog/logoLayout.ts +12 -0
  28. package/native/catalog/pageApi.ts +21 -3
  29. package/native/catalog/tokens.ts +5 -0
  30. package/native/catalog/types.ts +9 -6
  31. package/package.json +12 -6
  32. package/starter-kit/WHEN_TO_USE.md +124 -0
  33. package/starter-kit/components/AnimatedChevron/AnimatedChevron.catalog.tsx +23 -0
  34. package/starter-kit/components/AnimatedChevron/AnimatedChevron.tsx +46 -0
  35. package/starter-kit/components/AnimatedChevron/index.ts +2 -0
  36. package/starter-kit/components/Avatar/Avatar.catalog.tsx +54 -0
  37. package/starter-kit/components/Avatar/Avatar.tsx +74 -0
  38. package/starter-kit/components/Avatar/index.ts +2 -0
  39. package/starter-kit/components/Badge/Badge.catalog.tsx +88 -0
  40. package/starter-kit/components/Badge/Badge.tsx +56 -0
  41. package/starter-kit/components/Badge/Badge.types.ts +18 -0
  42. package/starter-kit/components/Badge/index.ts +2 -0
  43. package/starter-kit/components/Banner/Banner.catalog.tsx +87 -0
  44. package/starter-kit/components/Banner/Banner.tsx +257 -0
  45. package/starter-kit/components/Banner/Banner.types.ts +38 -0
  46. package/starter-kit/components/Banner/index.ts +2 -0
  47. package/starter-kit/components/BottomSheet/BottomSheet.catalog.tsx +77 -0
  48. package/starter-kit/components/BottomSheet/BottomSheet.tsx +151 -0
  49. package/starter-kit/components/BottomSheet/index.ts +2 -0
  50. package/starter-kit/components/Button/Button.catalog.tsx +99 -0
  51. package/starter-kit/components/Button/Button.tsx +173 -0
  52. package/starter-kit/components/Button/Button.types.ts +52 -0
  53. package/starter-kit/components/Button/index.ts +2 -0
  54. package/starter-kit/components/ButtonGroup/ButtonGroup.catalog.tsx +39 -0
  55. package/starter-kit/components/ButtonGroup/ButtonGroup.tsx +84 -0
  56. package/starter-kit/components/ButtonGroup/index.ts +2 -0
  57. package/starter-kit/components/Card/Card.catalog.tsx +68 -0
  58. package/starter-kit/components/Card/Card.tsx +133 -0
  59. package/starter-kit/components/Card/index.ts +2 -0
  60. package/starter-kit/components/Checkbox/Checkbox.catalog.tsx +28 -0
  61. package/starter-kit/components/Checkbox/Checkbox.tsx +104 -0
  62. package/starter-kit/components/Checkbox/index.ts +2 -0
  63. package/starter-kit/components/Dialog/Dialog.catalog.tsx +41 -0
  64. package/starter-kit/components/Dialog/Dialog.tsx +82 -0
  65. package/starter-kit/components/Dialog/index.ts +2 -0
  66. package/starter-kit/components/Divider/Divider.catalog.tsx +35 -0
  67. package/starter-kit/components/Divider/Divider.tsx +20 -0
  68. package/starter-kit/components/Divider/index.ts +2 -0
  69. package/starter-kit/components/Dock/Dock.catalog.tsx +71 -0
  70. package/starter-kit/components/Dock/Dock.tsx +58 -0
  71. package/starter-kit/components/Dock/index.ts +2 -0
  72. package/starter-kit/components/Dropdown/Dropdown.catalog.tsx +69 -0
  73. package/starter-kit/components/Dropdown/Dropdown.tsx +162 -0
  74. package/starter-kit/components/Dropdown/index.ts +2 -0
  75. package/starter-kit/components/EmptyState/EmptyState.catalog.tsx +67 -0
  76. package/starter-kit/components/EmptyState/EmptyState.tsx +88 -0
  77. package/starter-kit/components/EmptyState/index.ts +2 -0
  78. package/starter-kit/components/FieldContainer/FieldContainer.catalog.tsx +76 -0
  79. package/starter-kit/components/FieldContainer/FieldContainer.tsx +99 -0
  80. package/starter-kit/components/FieldContainer/FieldContainer.types.ts +27 -0
  81. package/starter-kit/components/FieldContainer/index.ts +2 -0
  82. package/starter-kit/components/InputClearButton/InputClearButton.catalog.tsx +18 -0
  83. package/starter-kit/components/InputClearButton/InputClearButton.tsx +63 -0
  84. package/starter-kit/components/InputClearButton/index.ts +2 -0
  85. package/starter-kit/components/InputField/InputField.catalog.tsx +76 -0
  86. package/starter-kit/components/InputField/InputField.tsx +418 -0
  87. package/starter-kit/components/InputField/index.ts +2 -0
  88. package/starter-kit/components/List/List.catalog.tsx +30 -0
  89. package/starter-kit/components/List/List.tsx +36 -0
  90. package/starter-kit/components/List/index.ts +2 -0
  91. package/starter-kit/components/ListItem/ListItem.catalog.tsx +89 -0
  92. package/starter-kit/components/ListItem/ListItem.tsx +165 -0
  93. package/starter-kit/components/ListItem/index.ts +2 -0
  94. package/starter-kit/components/Loading/Loading.catalog.tsx +34 -0
  95. package/starter-kit/components/Loading/Loading.tsx +121 -0
  96. package/starter-kit/components/Loading/index.ts +2 -0
  97. package/starter-kit/components/Pill/Pill.catalog.tsx +79 -0
  98. package/starter-kit/components/Pill/Pill.tsx +181 -0
  99. package/starter-kit/components/Pill/Pill.types.ts +30 -0
  100. package/starter-kit/components/Pill/index.ts +2 -0
  101. package/starter-kit/components/PillRow/PillRow.catalog.tsx +91 -0
  102. package/starter-kit/components/PillRow/PillRow.tsx +140 -0
  103. package/starter-kit/components/PillRow/PillRow.types.ts +24 -0
  104. package/starter-kit/components/PillRow/index.ts +2 -0
  105. package/starter-kit/components/ProgressDots/ProgressDots.catalog.tsx +22 -0
  106. package/starter-kit/components/ProgressDots/ProgressDots.tsx +42 -0
  107. package/starter-kit/components/ProgressDots/index.ts +2 -0
  108. package/starter-kit/components/Radio/Radio.catalog.tsx +38 -0
  109. package/starter-kit/components/Radio/Radio.tsx +114 -0
  110. package/starter-kit/components/Radio/index.ts +2 -0
  111. package/starter-kit/components/Radio/usePressScale.ts +33 -0
  112. package/starter-kit/components/SearchField/SearchField.catalog.tsx +28 -0
  113. package/starter-kit/components/SearchField/SearchField.tsx +98 -0
  114. package/starter-kit/components/SearchField/index.ts +2 -0
  115. package/starter-kit/components/SectionHeader/SectionHeader.catalog.tsx +72 -0
  116. package/starter-kit/components/SectionHeader/SectionHeader.tsx +139 -0
  117. package/starter-kit/components/SectionHeader/index.ts +2 -0
  118. package/starter-kit/components/SegmentedToggle/SegmentedToggle.catalog.tsx +31 -0
  119. package/starter-kit/components/SegmentedToggle/SegmentedToggle.tsx +163 -0
  120. package/starter-kit/components/SegmentedToggle/index.ts +2 -0
  121. package/starter-kit/components/SegmentedToggle/useSlideAnim.ts +29 -0
  122. package/starter-kit/components/Shimmer/Breathing.catalog.tsx +29 -0
  123. package/starter-kit/components/Shimmer/Shimmer.catalog.tsx +89 -0
  124. package/starter-kit/components/Shimmer/Shimmer.tsx +158 -0
  125. package/starter-kit/components/Shimmer/Shimmer.types.ts +27 -0
  126. package/starter-kit/components/Shimmer/SkeletonGroup.catalog.tsx +39 -0
  127. package/starter-kit/components/Shimmer/index.ts +2 -0
  128. package/starter-kit/components/Surface/Surface.catalog.tsx +44 -0
  129. package/starter-kit/components/Surface/Surface.tsx +45 -0
  130. package/starter-kit/components/Surface/index.ts +2 -0
  131. package/starter-kit/components/Switch/Switch.catalog.tsx +33 -0
  132. package/starter-kit/components/Switch/Switch.tsx +129 -0
  133. package/starter-kit/components/Switch/index.ts +2 -0
  134. package/starter-kit/components/TextArea/TextArea.catalog.tsx +29 -0
  135. package/starter-kit/components/TextArea/TextArea.tsx +99 -0
  136. package/starter-kit/components/TextArea/index.ts +2 -0
  137. package/starter-kit/components/Toast/Toast.catalog.tsx +61 -0
  138. package/starter-kit/components/Toast/Toast.tsx +144 -0
  139. package/starter-kit/components/Toast/index.ts +2 -0
  140. package/starter-kit/components/Tooltip/Tooltip.catalog.tsx +121 -0
  141. package/starter-kit/components/Tooltip/Tooltip.tsx +195 -0
  142. package/starter-kit/components/Tooltip/index.ts +2 -0
  143. package/starter-kit/components/TopNav/TopNav.catalog.tsx +71 -0
  144. package/starter-kit/components/TopNav/TopNav.tsx +77 -0
  145. package/starter-kit/components/TopNav/index.ts +2 -0
  146. package/starter-kit/components/UnderlineTabs/UnderlineTabs.catalog.tsx +30 -0
  147. package/starter-kit/components/UnderlineTabs/UnderlineTabs.tsx +156 -0
  148. package/starter-kit/components/UnderlineTabs/index.ts +2 -0
  149. package/starter-kit/components/index.ts +36 -0
  150. package/starter-kit/components/statusColors.ts +27 -0
  151. package/starter-kit/icons/Icon.native.tsx +90 -0
  152. package/starter-kit/icons/index.ts +10 -0
  153. package/starter-kit/icons/paths.ts +623 -0
  154. package/starter-kit/icons/types.ts +69 -0
  155. package/starter-kit/pages/Colors.catalog.tsx +55 -0
  156. package/starter-kit/pages/Font.catalog.tsx +45 -0
  157. package/starter-kit/pages/Icons.catalog.tsx +51 -0
  158. package/starter-kit/pages/Manifest.catalog.tsx +24 -0
  159. package/starter-kit/pages/Motion.catalog.tsx +101 -0
  160. package/starter-kit/pages/Radius.catalog.tsx +35 -0
  161. package/starter-kit/pages/ReportIssue.catalog.tsx +148 -0
  162. package/starter-kit/pages/SavedTrips.catalog.tsx +247 -0
  163. package/starter-kit/pages/Shadow.catalog.tsx +34 -0
  164. package/starter-kit/pages/Spacing.catalog.tsx +12 -0
  165. package/starter-kit/pages/TripPlannerForm.catalog.tsx +48 -0
  166. package/starter-kit/pages/Typography.catalog.tsx +17 -0
  167. package/starter-kit/tokens/index.ts +50 -0
  168. package/starter-kit/tokens/motion.ts +90 -0
  169. package/starter-kit/tokens/palette.ts +111 -0
  170. package/starter-kit/tokens/scales.ts +106 -0
  171. package/starter-kit/tokens/semantic.ts +98 -0
  172. package/starter-kit/tokens/shadow.ts +36 -0
  173. package/starter-kit/tokens/typography.ts +72 -0
package/dist/cli/props.js CHANGED
@@ -11,15 +11,16 @@ const COMPONENT_TYPE_HINT = /MemoExoticComponent|ForwardRefExoticComponent|Named
11
11
  * fixtures have no tsconfig of their own, so lookup reaches this repository's root
12
12
  * `tsconfig.json` (Task 1); it declares no `paths`, `baseUrl`, or `jsx`, so `options.paths`
13
13
  * alone decides resolution in tests. */
14
- function readHostTsconfigOptions(entryFile) {
14
+ export function readHostTsconfigOptions(entryFile) {
15
15
  const tsconfigPath = ts.findConfigFile(path.dirname(entryFile), ts.sys.fileExists);
16
16
  if (!tsconfigPath)
17
- return {};
17
+ return { options: {}, declarationFiles: [] };
18
18
  const { config } = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
19
19
  const parsed = ts.parseJsonConfigFileContent(config, ts.sys, path.dirname(tsconfigPath));
20
20
  const { baseUrl, paths, jsx } = parsed.options;
21
21
  const pathsBasePath = parsed.options.pathsBasePath;
22
- return { baseUrl, paths, jsx, pathsBasePath };
22
+ const declarationFiles = parsed.fileNames.filter((file) => file.endsWith('.d.ts'));
23
+ return { baseUrl, paths, jsx, pathsBasePath, options: parsed.options, declarationFiles };
23
24
  }
24
25
  /** What is a component (design §3): an export whose type has a call signature returning
25
26
  * JSX/ReactNode, or is typed as a Memo/ForwardRef/NamedExotic/ComponentType wrapper.
@@ -52,7 +53,7 @@ export function createComponentReader(entryFiles, options = {}) {
52
53
  allowJs: false,
53
54
  noEmit: true,
54
55
  baseUrl: host.baseUrl,
55
- paths: { ...host.paths, ...options.paths },
56
+ paths: { ...options.fallbackPaths, ...host.paths, ...options.paths },
56
57
  // `pathsBasePath` is @internal: it tells the compiler which folder `paths` are relative to when
57
58
  // the host tsconfig has `paths` but no `baseUrl` (the TypeScript 6 idiom).
58
59
  ...(host.pathsBasePath ? { pathsBasePath: host.pathsBasePath } : {}),
@@ -156,33 +157,23 @@ function readComponentRecord(checker, name, type, declaration, optionRoots) {
156
157
  const propsType = signature && signature.parameters.length > 0 ? checker.getTypeOfSymbolAtLocation(signature.parameters[0], declaration) : undefined;
157
158
  const implementationDefaults = defaultsFromImplementation(declaration);
158
159
  const props = [];
159
- const inheritedFrom = new Set();
160
160
  if (propsType) {
161
161
  const branches = propsType.isUnion() ? propsType.types : [propsType];
162
162
  for (const merged of mergeUnionProps(checker, branches)) {
163
163
  const { symbol: prop, declaration: propDeclaration, branchCount } = merged;
164
164
  const propFile = propDeclaration?.getSourceFile().fileName ?? '';
165
- if (propFile.includes('node_modules')) {
166
- // `@types/react` itself (e.g. `ref` from `RefAttributes` on a `forwardRef` component) is
167
- // not a meaningful "inherited from" fact — every component already uses React — so it's
168
- // dropped rather than reported as "plus all react props". A host-type package's own props
169
- // (e.g. react-native's `TextInput`) are still reported as before.
170
- if (!/[/\\]@types[/\\]react[/\\]/.test(propFile) || /[/\\]@types[/\\]react-native[/\\]/.test(propFile)) {
171
- inheritedFrom.add(path.basename(path.dirname(propFile)));
172
- }
165
+ // A prop declared in `node_modules` (e.g. `value` from react-native's `TextInputProps`) is
166
+ // never listed individually — `inheritedFrom` below (design §3) summarises the whole
167
+ // extended type as one row instead.
168
+ if (propFile.includes('node_modules'))
173
169
  continue;
174
- }
175
170
  const propType = propDeclaration ? checker.getTypeOfSymbolAtLocation(prop, propDeclaration) : checker.getAnyType();
176
- // Under `strict`, an optional prop's type includes `| undefined` (that's how TypeScript
177
- // represents `label?: string` when read via a symbol) — print its non-nullable type instead,
178
- // keeping any alias name, so the Props table reads "string" rather than "string | undefined".
179
- const displayType = prop.flags & ts.SymbolFlags.Optional ? checker.getNonNullableType(propType) : propType;
180
171
  const jsDocDefault = prop.getJsDocTags().find((tag) => tag.name === 'default');
181
- const baseDesc = ts.displayPartsToString(prop.getDocumentationComment(checker));
172
+ const baseDesc = joinWrappedLines(ts.displayPartsToString(prop.getDocumentationComment(checker)));
182
173
  const isPartial = branchCount < branches.length;
183
174
  props.push({
184
175
  name: prop.getName(),
185
- type: checker.typeToString(displayType, undefined, ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope),
176
+ type: displayTypeString(checker, prop, propType),
186
177
  // A prop present in only some branches is never required overall — the caller can always
187
178
  // pick a branch that omits it — regardless of whether it's required within the branch(es)
188
179
  // it does appear in (that's `merged.requiredInEvery`, checked only when `!isPartial`).
@@ -193,7 +184,165 @@ function readComponentRecord(checker, name, type, declaration, optionRoots) {
193
184
  });
194
185
  }
195
186
  }
196
- return { name, file: path.relative(process.cwd(), sourceFile.fileName), props, inheritedFrom: [...inheritedFrom] };
187
+ const inheritedFrom = propsType ? nodeModulesHeritageNames(checker, propsType.isUnion() ? propsType.types : [propsType]) : [];
188
+ return { name, file: path.relative(process.cwd(), sourceFile.fileName), props, inheritedFrom };
189
+ }
190
+ /** Design §3 — "Type column: keeps alias names, not expanded unions." An optional prop's type
191
+ * (as read via its symbol) includes `undefined` — printed as `"T | undefined"` when `T` has no
192
+ * alias of its own (e.g. `label?: string`), but when `T` is itself an alias whose own definition
193
+ * already includes `undefined` (react-native's `StyleProp<T>`, React's `ReactNode`), the type as
194
+ * declared is simply `T` with no added union member, and `checker.typeToString` already prints
195
+ * just the alias name (e.g. `"StyleProp<ViewStyle>"`) — no `| undefined` to strip. Dropping a
196
+ * trailing/leading `undefined` segment as *text*, rather than calling `checker.getNonNullableType`
197
+ * on the type and re-printing that, is what keeps the alias in both cases: `getNonNullableType`
198
+ * rebuilds a fresh, unaliased union once it has to remove a member nested inside an alias's own
199
+ * definition (`StyleProp`/`ReactNode`), even though the printed string never needed that member
200
+ * removed in the first place. */
201
+ function displayTypeString(checker, prop, propType) {
202
+ const printed = checker.typeToString(propType, undefined, ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope);
203
+ if (!(prop.flags & ts.SymbolFlags.Optional))
204
+ return printed;
205
+ const kept = splitTopLevelUnion(printed).filter((part) => part !== 'undefined' && part !== 'null');
206
+ if (kept.length === 0)
207
+ return printed;
208
+ // A single remaining part that is itself wrapped in one outer pair of parentheses (e.g. the
209
+ // `(() => void)` TypeScript prints around an arrow type that was a union member) no longer needs
210
+ // those parentheses once it is the only part left — `(t: string) => void`, not `((t: string) =>
211
+ // void)`.
212
+ if (kept.length === 1)
213
+ return unwrapOuterParens(kept[0]);
214
+ return kept.join(' | ');
215
+ }
216
+ /** Splits a printed type's top-level ` | ` union members — skipping any `|` nested inside
217
+ * `<...>`, `(...)`, `{...}`, or `[...]` (e.g. the one inside `RecursiveArray<Falsy | ViewStyle>`)
218
+ * — so `displayTypeString` can drop a top-level `undefined`/`null` member without disturbing a
219
+ * union that appears only as part of a deeper type argument. An arrow type's `=>` prints a `>`
220
+ * that is not a closing bracket, so it must not decrement `depth`: skip it whenever the previous
221
+ * character was `=` (otherwise `(() => void) | undefined` goes to depth -1 at the arrow and the
222
+ * top-level ` | ` before `undefined` is read as nested, never splitting it off). */
223
+ function splitTopLevelUnion(typeString) {
224
+ const parts = [];
225
+ let depth = 0;
226
+ let start = 0;
227
+ for (let i = 0; i < typeString.length; i++) {
228
+ const ch = typeString[i];
229
+ if (ch === '<' || ch === '(' || ch === '{' || ch === '[')
230
+ depth += 1;
231
+ else if (ch === '>' || ch === ')' || ch === '}' || ch === ']') {
232
+ if (ch === '>' && typeString[i - 1] === '=')
233
+ continue;
234
+ depth -= 1;
235
+ }
236
+ else if (depth === 0 && typeString.startsWith(' | ', i)) {
237
+ parts.push(typeString.slice(start, i));
238
+ start = i + 3;
239
+ }
240
+ }
241
+ parts.push(typeString.slice(start));
242
+ return parts;
243
+ }
244
+ /** Strips one outer `(...)` pair from `part` when it wraps the whole string — not just any
245
+ * leading `(`, e.g. `(a) => (b)` has balanced outer parens only around `a`, not around the whole
246
+ * expression, so this walks depth to confirm the first `(` closes at the very last character. */
247
+ function unwrapOuterParens(part) {
248
+ if (part[0] !== '(' || part[part.length - 1] !== ')')
249
+ return part;
250
+ let depth = 0;
251
+ for (let i = 0; i < part.length; i++) {
252
+ const ch = part[i];
253
+ if (ch === '(')
254
+ depth += 1;
255
+ else if (ch === ')') {
256
+ depth -= 1;
257
+ if (depth === 0)
258
+ return i === part.length - 1 ? part.slice(1, -1) : part;
259
+ }
260
+ }
261
+ return part;
262
+ }
263
+ /** Design §3 — "Inherited props declared in `node_modules` ... are summarised as one row: 'plus
264
+ * all TextInput props'." Reads each branch's own `extends` heritage clause(s) — not the flattened
265
+ * property list `mergeUnionProps` builds for the table above — because a `node_modules` interface
266
+ * like react-native's `TextInputProps` itself extends several more `node_modules` interfaces
267
+ * (`ViewProps`, `TouchableWithoutFeedbackProps`, ...), each declared in its own file; grouping by
268
+ * every inherited *property*'s own declaration file (the previous approach) reported one noisy
269
+ * entry per ancestor file instead of the one name the component's own Props type actually names.
270
+ * A `forwardRef` component's props type is `OwnProps & RefAttributes<T>` (an intersection, not an
271
+ * interface) — `heritageInterfaceDeclarations` unwraps that to reach `OwnProps`'s own declaration. */
272
+ function nodeModulesHeritageNames(checker, branches) {
273
+ const names = new Set();
274
+ for (const branch of branches) {
275
+ for (const declaration of heritageInterfaceDeclarations(branch)) {
276
+ const declarationFile = declaration.getSourceFile().fileName;
277
+ if (declarationFile.includes('node_modules')) {
278
+ // `declaration` was pulled in directly by the branch itself (e.g. `type Props =
279
+ // TextInputProps & {...}`), not named in a *local* interface's own `extends` clause —
280
+ // report its own name. Walking its `heritageClauses` below (as the `extends
281
+ // TextInputProps` case does) would instead report TextInputProps's own ancestors, e.g.
282
+ // "View"/"TouchableWithoutFeedback".
283
+ if (!isReactOnlyFile(declarationFile))
284
+ names.add(declaration.name.getText().replace(/Props$/, ''));
285
+ continue;
286
+ }
287
+ for (const clause of declaration.heritageClauses ?? []) {
288
+ for (const typeNode of clause.types) {
289
+ // `getTypeAtLocation` on the whole `ExpressionWithTypeArguments` node (not just its
290
+ // `.expression`) is required for `resolveUtilityTypeSource` below to see `Omit`/`Pick`'s
291
+ // actual instantiated type arguments — on `.expression` alone they come back as the
292
+ // utility type's own unbound generic parameters ("T", "K"), not `TextInputProps`.
293
+ const heritageType = resolveUtilityTypeSource(checker.getTypeAtLocation(typeNode));
294
+ const heritageDeclaration = (heritageType.aliasSymbol ?? heritageType.symbol)?.declarations?.[0];
295
+ if (!heritageDeclaration)
296
+ continue;
297
+ const file = heritageDeclaration.getSourceFile().fileName;
298
+ if (!file.includes('node_modules'))
299
+ continue;
300
+ // `@types/react` itself (e.g. `Attributes`, reached through a `forwardRef` component's
301
+ // own `RefAttributes<T>`) is not a meaningful "inherited from" fact — every component
302
+ // already uses React. A host-type package's own heritage (e.g. react-native's `View`,
303
+ // reached through `TextInputProps`) is still reported as before.
304
+ if (isReactOnlyFile(file))
305
+ continue;
306
+ const rawName = (heritageType.aliasSymbol ?? heritageType.symbol)?.getName() ?? typeNode.expression.getText();
307
+ names.add(rawName.replace(/Props$/, ''));
308
+ }
309
+ }
310
+ }
311
+ }
312
+ return [...names];
313
+ }
314
+ /** True for a declaration file under `@types/react` that is not also under `@types/react-native`
315
+ * (react-native's own `@types` package path contains `react` as a substring). */
316
+ function isReactOnlyFile(file) {
317
+ return /[/\\]@types[/\\]react[/\\]/.test(file) && !/[/\\]@types[/\\]react-native[/\\]/.test(file);
318
+ }
319
+ /** `Omit<T, K>`/`Pick<T, K>` of a node_modules type prints its own alias symbol ("Omit"/"Pick")
320
+ * rather than `T`'s name — resolve to `T` (the type's first alias type argument) instead, so
321
+ * `extends Omit<TextInputProps, 'value'>` reports "TextInput", not "Omit" (Fable's review,
322
+ * cli/props.ts:270-301). */
323
+ function resolveUtilityTypeSource(type) {
324
+ const aliasName = type.aliasSymbol?.getName();
325
+ if ((aliasName === 'Omit' || aliasName === 'Pick') && type.aliasTypeArguments?.[0]) {
326
+ return type.aliasTypeArguments[0];
327
+ }
328
+ return type;
329
+ }
330
+ /** Every `InterfaceDeclaration` backing `type` — itself if `type` is one, or (recursing) each
331
+ * constituent of an intersection, so a `forwardRef` component's `OwnProps & RefAttributes<T>`
332
+ * still reaches `OwnProps`'s own heritage clause. */
333
+ function heritageInterfaceDeclarations(type) {
334
+ if (type.isIntersection())
335
+ return type.types.flatMap(heritageInterfaceDeclarations);
336
+ return (type.symbol?.declarations ?? []).filter(ts.isInterfaceDeclaration);
337
+ }
338
+ /** Design §3, finding 4: a JSDoc paragraph hard-wrapped in source (`getDocumentationComment`
339
+ * preserves each line break verbatim) must read as running text — join lines within a paragraph
340
+ * with a single space, but keep a blank-line paragraph break as its own line break. */
341
+ function joinWrappedLines(text) {
342
+ return text
343
+ .split(/\n{2,}/)
344
+ .map((paragraph) => paragraph.replace(/\s*\n\s*/g, ' ').trim())
345
+ .join('\n\n');
197
346
  }
198
347
  /**
199
348
  * Design §3 — "Unions of object types (e.g. `CardProps` as two intersections) are merged into one
@@ -0,0 +1,356 @@
1
+ import * as ts from 'typescript';
2
+ import { readFileSync } from 'node:fs';
3
+ const TRANSPILE_OPTIONS = {
4
+ compilerOptions: { jsx: ts.JsxEmit.ReactJSX, module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
5
+ reportDiagnostics: true,
6
+ };
7
+ /** Reads every page file's literal-checkable fields (design §4 "Static checking"). Never imports
8
+ * the file and never builds a `ts.Program` across files — no module resolution happens at all, so
9
+ * this cannot fail because of an unresolved import, a missing `node_modules`, or a stale type.
10
+ * One broken file never stops the rest (Global Constraints, Error-handling rule): each file is
11
+ * read independently. */
12
+ export function readStaticPages(pageFiles) {
13
+ // Error-handling rule (design, Global Constraints): one broken file never stops the rest of a
14
+ // read. `readStaticPage` already guards its own `readFileSync` call, but an unexpected throw
15
+ // from deeper in its AST walk (e.g. a pathological same-file `const` chain overflowing the call
16
+ // stack) must be caught per file too, not just per-read, or it aborts every other page in the
17
+ // same `doctor`/`explain` run.
18
+ return pageFiles.map((file) => {
19
+ try {
20
+ return readStaticPage(file);
21
+ }
22
+ catch (error) {
23
+ return { file, checkable: false, parseError: error.message };
24
+ }
25
+ });
26
+ }
27
+ function readStaticPage(file) {
28
+ let text;
29
+ try {
30
+ text = readFileSync(file, 'utf8');
31
+ }
32
+ catch (error) {
33
+ return { file, checkable: false, parseError: error.message };
34
+ }
35
+ // `transpileModule` parses a single file with no module resolution at all — its `diagnostics`
36
+ // are syntactic only (it never type-checks), which is exactly "fails to parse" (design §4's
37
+ // error rule) and nothing more; a semantic/type error in a page file is out of this reader's
38
+ // scope (it never had enough information — no resolved `node_modules` — to find one anyway).
39
+ const { diagnostics } = ts.transpileModule(text, TRANSPILE_OPTIONS);
40
+ if (diagnostics && diagnostics.length > 0) {
41
+ return { file, checkable: false, parseError: ts.flattenDiagnosticMessageText(diagnostics[0].messageText, '\n') };
42
+ }
43
+ const sourceFile = ts.createSourceFile(file, text, ts.ScriptTarget.ES2022, true, ts.ScriptKind.TSX);
44
+ const imports = importedNames(sourceFile, '@krapwoo/ds-viewer');
45
+ const defineCatalogPageNames = localNamesFor(imports, 'defineCatalogPage');
46
+ const gridNames = localNamesFor(imports, 'grid');
47
+ const call = defaultExportCall(sourceFile);
48
+ if (!call || !ts.isIdentifier(call.expression) || !defineCatalogPageNames.has(call.expression.text) || call.arguments.length !== 1) {
49
+ // Not a recognized `defineCatalogPage(...)` default export — e.g. a hand-written object, or a
50
+ // shadowed/renamed import this reader can't trace. Not a parse error: the file is valid
51
+ // TypeScript, just not literal-checkable.
52
+ return { file, checkable: false };
53
+ }
54
+ const consts = topLevelConsts(sourceFile);
55
+ const pageObject = resolve(call.arguments[0], consts, new Set());
56
+ if (!ts.isObjectLiteralExpression(pageObject))
57
+ return { file, checkable: false };
58
+ const page = { file, checkable: true };
59
+ const idProp = findProp(pageObject, 'id');
60
+ const componentProp = findProp(pageObject, 'component');
61
+ const groupProp = findProp(pageObject, 'group');
62
+ const specimenSizeProp = findProp(pageObject, 'specimenSize');
63
+ const tokenGalleryProp = findProp(pageObject, 'tokenGallery');
64
+ if (idProp)
65
+ page.id = stringLiteral(resolve(idProp.initializer, consts, new Set()));
66
+ if (componentProp)
67
+ page.component = stringLiteral(resolve(componentProp.initializer, consts, new Set()));
68
+ if (groupProp)
69
+ page.group = stringLiteral(resolve(groupProp.initializer, consts, new Set()));
70
+ if (specimenSizeProp)
71
+ page.specimenSize = stringLiteral(resolve(specimenSizeProp.initializer, consts, new Set()));
72
+ if (tokenGalleryProp)
73
+ page.tokenGallery = resolve(tokenGalleryProp.initializer, consts, new Set()).kind === ts.SyntaxKind.TrueKeyword;
74
+ page.hasWhenToUse = findProp(pageObject, 'whenToUse') !== undefined;
75
+ page.hasA11y = findProp(pageObject, 'a11y') !== undefined;
76
+ page.hasRender = findProp(pageObject, 'render') !== undefined;
77
+ const hideProp = findProp(pageObject, 'hide');
78
+ if (hideProp) {
79
+ const hideObject = resolve(hideProp.initializer, consts, new Set());
80
+ const literalBool = (name) => {
81
+ const prop = ts.isObjectLiteralExpression(hideObject) ? findProp(hideObject, name) : undefined;
82
+ return prop ? resolve(prop.initializer, consts, new Set()).kind === ts.SyntaxKind.TrueKeyword : false;
83
+ };
84
+ page.hidesAccessibility = literalBool('accessibility');
85
+ page.hideVariants = literalBool('variants');
86
+ page.hideStates = literalBool('states');
87
+ }
88
+ const previewWidthsProp = findProp(pageObject, 'previewWidths');
89
+ if (previewWidthsProp) {
90
+ const resolved = resolve(previewWidthsProp.initializer, consts, new Set());
91
+ if (ts.isStringLiteral(resolved) && resolved.text === 'full') {
92
+ page.previewWidths = 'full';
93
+ }
94
+ else if (ts.isArrayLiteralExpression(resolved) && resolved.elements.every(ts.isNumericLiteral)) {
95
+ page.previewWidths = resolved.elements.map((element) => Number(element.text));
96
+ }
97
+ // Anything else (a function call, a non-literal expression) is simply left unset — `previewWidths`
98
+ // isn't in design §4's literal-checkable list, so it's never a reason to flip `checkable`.
99
+ }
100
+ const fullWidthLabelProp = findProp(pageObject, 'fullWidthLabel');
101
+ if (fullWidthLabelProp)
102
+ page.fullWidthLabel = stringLiteral(resolve(fullWidthLabelProp.initializer, consts, new Set()));
103
+ const propNotesProp = findProp(pageObject, 'propNotes');
104
+ if (propNotesProp) {
105
+ const resolved = resolve(propNotesProp.initializer, consts, new Set());
106
+ if (ts.isObjectLiteralExpression(resolved)) {
107
+ page.propNoteKeys = resolved.properties.filter(ts.isPropertyAssignment).map((p) => propertyName(p.name));
108
+ }
109
+ else {
110
+ page.checkable = false;
111
+ }
112
+ }
113
+ const comparisonProp = findProp(pageObject, 'comparison');
114
+ if (comparisonProp) {
115
+ const comparison = readComparison(comparisonProp.initializer, consts, gridNames);
116
+ if (comparison)
117
+ page.comparison = comparison;
118
+ else
119
+ page.checkable = false;
120
+ }
121
+ const variants = readSlotItems(findProp(pageObject, 'variants'), consts);
122
+ if (!variants.checkable)
123
+ page.checkable = false;
124
+ else {
125
+ page.variantsItems = variants.items;
126
+ page.variantsItemsFill = variants.itemsFill;
127
+ }
128
+ const states = readSlotItems(findProp(pageObject, 'states'), consts);
129
+ if (!states.checkable)
130
+ page.checkable = false;
131
+ else {
132
+ page.statesItems = states.items;
133
+ page.statesItemsFill = states.itemsFill;
134
+ }
135
+ return page;
136
+ }
137
+ /** Local name -> imported name, for every named import from `moduleSpecifier`. */
138
+ function importedNames(sourceFile, moduleSpecifier) {
139
+ const names = new Map();
140
+ for (const statement of sourceFile.statements) {
141
+ if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== moduleSpecifier) {
142
+ continue;
143
+ }
144
+ const bindings = statement.importClause?.namedBindings;
145
+ if (!bindings || !ts.isNamedImports(bindings))
146
+ continue;
147
+ for (const element of bindings.elements) {
148
+ names.set(element.name.text, element.propertyName ? element.propertyName.text : element.name.text);
149
+ }
150
+ }
151
+ return names;
152
+ }
153
+ function localNamesFor(imports, importedName) {
154
+ const locals = new Set();
155
+ for (const [local, imported] of imports)
156
+ if (imported === importedName)
157
+ locals.add(local);
158
+ return locals;
159
+ }
160
+ function defaultExportCall(sourceFile) {
161
+ for (const statement of sourceFile.statements) {
162
+ if (ts.isExportAssignment(statement) && !statement.isExportEquals && ts.isCallExpression(statement.expression)) {
163
+ return statement.expression;
164
+ }
165
+ }
166
+ return undefined;
167
+ }
168
+ function topLevelConsts(sourceFile) {
169
+ const consts = new Map();
170
+ for (const statement of sourceFile.statements) {
171
+ if (!ts.isVariableStatement(statement))
172
+ continue;
173
+ for (const declaration of statement.declarationList.declarations) {
174
+ if (ts.isIdentifier(declaration.name) && declaration.initializer)
175
+ consts.set(declaration.name.text, declaration.initializer);
176
+ }
177
+ }
178
+ return consts;
179
+ }
180
+ /** Unwraps `as`/parenthesized/`satisfies` expressions and chases a same-file top-level `const`
181
+ * reference to its own initializer (same pattern as `cli/tokens.ts`'s `readExpression`) — returns
182
+ * the resolved expression node itself, not a value, so callers can pattern-match on its kind
183
+ * (array/object literal, call expression, …). `seen` guards a reference cycle. */
184
+ function resolve(node, consts, seen) {
185
+ if (ts.isAsExpression(node) || ts.isParenthesizedExpression(node) || ts.isSatisfiesExpression(node)) {
186
+ return resolve(node.expression, consts, seen);
187
+ }
188
+ if (ts.isIdentifier(node) && !seen.has(node.text)) {
189
+ const target = consts.get(node.text);
190
+ if (target)
191
+ return resolve(target, consts, new Set(seen).add(node.text));
192
+ }
193
+ return node;
194
+ }
195
+ function stringLiteral(node) {
196
+ return ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) ? node.text : undefined;
197
+ }
198
+ function propertyName(name) {
199
+ return ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : name.getText();
200
+ }
201
+ function findProp(obj, name) {
202
+ const prop = obj.properties.find((p) => ts.isPropertyAssignment(p) && propertyName(p.name) === name);
203
+ return prop;
204
+ }
205
+ function propertyStringLiteral(obj, name, consts) {
206
+ const prop = findProp(obj, name);
207
+ return prop ? stringLiteral(resolve(prop.initializer, consts, new Set())) : undefined;
208
+ }
209
+ /** A literal array of `{ key, label }` object literals — each element may itself be a same-file
210
+ * const reference. Undefined (not literal) for anything else: a `.map()`/other call, a spread, a
211
+ * cross-file import. */
212
+ function readAxisItemArray(node, consts) {
213
+ const resolved = resolve(node, consts, new Set());
214
+ if (!ts.isArrayLiteralExpression(resolved))
215
+ return undefined;
216
+ const items = [];
217
+ for (const element of resolved.elements) {
218
+ const resolvedElement = resolve(element, consts, new Set());
219
+ if (!ts.isObjectLiteralExpression(resolvedElement))
220
+ return undefined;
221
+ const key = propertyStringLiteral(resolvedElement, 'key', consts);
222
+ const label = propertyStringLiteral(resolvedElement, 'label', consts);
223
+ if (key === undefined || label === undefined)
224
+ return undefined;
225
+ items.push({ key, label });
226
+ }
227
+ return items;
228
+ }
229
+ /** A `GridAxis` (design §4): a plain literal array, or `{ prop, items }` (`items` itself read by
230
+ * `readAxisItemArray`). Undefined for anything else. */
231
+ function readAxis(node, consts) {
232
+ const resolved = resolve(node, consts, new Set());
233
+ if (ts.isArrayLiteralExpression(resolved)) {
234
+ const items = readAxisItemArray(resolved, consts);
235
+ return items ? { items } : undefined;
236
+ }
237
+ if (ts.isObjectLiteralExpression(resolved)) {
238
+ const propProp = findProp(resolved, 'prop');
239
+ const itemsProp = findProp(resolved, 'items');
240
+ if (!propProp || !itemsProp)
241
+ return undefined;
242
+ const prop = stringLiteral(resolve(propProp.initializer, consts, new Set()));
243
+ const items = readAxisItemArray(itemsProp.initializer, consts);
244
+ if (prop === undefined || !items)
245
+ return undefined;
246
+ return { prop, items };
247
+ }
248
+ return undefined;
249
+ }
250
+ /** A page's `comparison` field: either a direct `ComparisonDef`-shaped object literal, or (the
251
+ * starter kit's own convention since Task 1/2) a call to this package's own `grid()` helper — the
252
+ * one call shape this reader specially recognizes, reading its `rows`/`columns` arguments and
253
+ * skipping its `cell` argument entirely (design §4: "Specimen nodes and `cell(...)` functions are
254
+ * not evaluated"). `gridNames` is the set of local identifiers that import resolved to `grid` from
255
+ * `@krapwoo/ds-viewer` — a page's own unrelated local function named `grid` is never matched. */
256
+ function readComparison(node, consts, gridNames) {
257
+ const resolved = resolve(node, consts, new Set());
258
+ if (ts.isCallExpression(resolved) && ts.isIdentifier(resolved.expression) && gridNames.has(resolved.expression.text)) {
259
+ const [rowLabelArg, columnLabelArg, rowsArg, columnsArg, , sizeArg] = resolved.arguments;
260
+ const rows = rowsArg && readAxis(rowsArg, consts);
261
+ const columns = columnsArg && readAxis(columnsArg, consts);
262
+ if (!rows || !columns)
263
+ return undefined;
264
+ return {
265
+ rowLabel: rowLabelArg ? stringLiteral(resolve(rowLabelArg, consts, new Set())) : undefined,
266
+ columnLabel: columnLabelArg ? stringLiteral(resolve(columnLabelArg, consts, new Set())) : undefined,
267
+ rows,
268
+ columns,
269
+ size: sizeArg ? stringLiteral(resolve(sizeArg, consts, new Set())) : undefined,
270
+ };
271
+ }
272
+ if (ts.isObjectLiteralExpression(resolved)) {
273
+ const rowsProp = findProp(resolved, 'rows');
274
+ const columnsProp = findProp(resolved, 'columns');
275
+ if (!rowsProp || !columnsProp)
276
+ return undefined;
277
+ const rows = readAxis(rowsProp.initializer, consts);
278
+ const columns = readAxis(columnsProp.initializer, consts);
279
+ if (!rows || !columns)
280
+ return undefined;
281
+ const rowLabelProp = findProp(resolved, 'rowLabel');
282
+ const columnLabelProp = findProp(resolved, 'columnLabel');
283
+ const sizeProp = findProp(resolved, 'size');
284
+ return {
285
+ rowLabel: rowLabelProp ? stringLiteral(resolve(rowLabelProp.initializer, consts, new Set())) : undefined,
286
+ columnLabel: columnLabelProp ? stringLiteral(resolve(columnLabelProp.initializer, consts, new Set())) : undefined,
287
+ rows,
288
+ columns,
289
+ size: sizeProp ? stringLiteral(resolve(sizeProp.initializer, consts, new Set())) : undefined,
290
+ };
291
+ }
292
+ return undefined;
293
+ }
294
+ /** A literal array of list-item object literals — `key`/`name` required; `props` (read as a flat
295
+ * literal map, non-literal values simply omitted) and `group` optional. Each item's `node` field
296
+ * (always present, always JSX) is never inspected — reading the sibling fields never depends on it. */
297
+ function readListItems(node, consts) {
298
+ const resolved = resolve(node, consts, new Set());
299
+ if (!ts.isArrayLiteralExpression(resolved))
300
+ return undefined;
301
+ const items = [];
302
+ for (const element of resolved.elements) {
303
+ const resolvedElement = resolve(element, consts, new Set());
304
+ if (!ts.isObjectLiteralExpression(resolvedElement))
305
+ return undefined;
306
+ const key = propertyStringLiteral(resolvedElement, 'key', consts);
307
+ const name = propertyStringLiteral(resolvedElement, 'name', consts);
308
+ if (key === undefined || name === undefined)
309
+ return undefined;
310
+ const group = propertyStringLiteral(resolvedElement, 'group', consts);
311
+ const propsProp = findProp(resolvedElement, 'props');
312
+ items.push({ key, name, group, props: propsProp ? readLiteralProps(propsProp.initializer, consts) : undefined });
313
+ }
314
+ return items;
315
+ }
316
+ function readLiteralProps(node, consts) {
317
+ const resolved = resolve(node, consts, new Set());
318
+ if (!ts.isObjectLiteralExpression(resolved))
319
+ return undefined;
320
+ const result = {};
321
+ for (const property of resolved.properties) {
322
+ if (!ts.isPropertyAssignment(property))
323
+ continue;
324
+ const value = resolve(property.initializer, consts, new Set());
325
+ if (ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value))
326
+ result[propertyName(property.name)] = value.text;
327
+ else if (ts.isNumericLiteral(value))
328
+ result[propertyName(property.name)] = Number(value.text);
329
+ else if (value.kind === ts.SyntaxKind.TrueKeyword)
330
+ result[propertyName(property.name)] = true;
331
+ else if (value.kind === ts.SyntaxKind.FalseKeyword)
332
+ result[propertyName(property.name)] = false;
333
+ // Anything else (e.g. a token reference) is skipped, not a reason to reject the whole item —
334
+ // `props` tags are only ever compared against a prop's string options (design §4).
335
+ }
336
+ return result;
337
+ }
338
+ /** A `variants`/`states` field: `{ itemsFill?, items: [...] }`, `items` read by `readListItems`.
339
+ * Distinguishes "field absent" (always checkable, everything undefined) from "field present but
340
+ * not literal" (not checkable). `itemsFill` is read only if present and literal — like
341
+ * `previewWidths`/`fullWidthLabel`, it isn't in design §4's literal-checkable list, so a
342
+ * non-literal `itemsFill` is simply left unset, never a reason to reject the whole slot. */
343
+ function readSlotItems(prop, consts) {
344
+ if (!prop)
345
+ return { checkable: true };
346
+ const resolved = resolve(prop.initializer, consts, new Set());
347
+ const itemsProp = ts.isObjectLiteralExpression(resolved) ? findProp(resolved, 'items') : undefined;
348
+ if (!itemsProp)
349
+ return { checkable: false };
350
+ const items = readListItems(itemsProp.initializer, consts);
351
+ if (!items)
352
+ return { checkable: false };
353
+ const itemsFillProp = ts.isObjectLiteralExpression(resolved) ? findProp(resolved, 'itemsFill') : undefined;
354
+ const itemsFill = itemsFillProp ? resolve(itemsFillProp.initializer, consts, new Set()).kind === ts.SyntaxKind.TrueKeyword : undefined;
355
+ return { items, itemsFill, checkable: true };
356
+ }
package/dist/cli/sync.js CHANGED
@@ -33,12 +33,31 @@ export function sync(config, resolveOptions) {
33
33
  const components = [];
34
34
  const componentRefs = [];
35
35
  const seenNames = new Set();
36
- const readComponent = createComponentReader(componentEntryFiles, { ...resolveOptions, optionRoots });
36
+ // A component entry file can live outside `config.projectRoot` (e.g. kit-host's sibling
37
+ // `../starter-kit/`) — `createComponentReader`'s own host-tsconfig lookup walks up from the
38
+ // entry file, never reaching `config.projectRoot`'s own node_modules. This fallback (lowest
39
+ // priority — a real host `paths`, or `resolveOptions.paths`, still wins) is what lets such a
40
+ // file still resolve `react`/`react-native` against the project that actually declares them.
41
+ // `@types/*` is listed BEFORE the plain package deliberately (confirmed by spike,
42
+ // `spikes/README.md` "sync-fallback-paths"): once `paths` redirects a bare specifier to an
43
+ // on-disk folder, TypeScript resolves it there and never falls through to a second entry just
44
+ // because the first has no types — the reverse order left `react`/`react-native` both
45
+ // resolving to "implicitly any", because the plain package folder matched first. Only computed
46
+ // when at least one entry file actually lies outside `config.projectRoot` — normal projects
47
+ // (every entry file under projectRoot) keep standard TypeScript resolution untouched.
48
+ const hasEntryOutsideProjectRoot = componentEntryFiles.some((file) => !file.startsWith(`${config.projectRoot}${path.sep}`));
49
+ const fallbackPaths = hasEntryOutsideProjectRoot
50
+ ? { '*': [path.join(config.projectRoot, 'node_modules', '@types', '*'), path.join(config.projectRoot, 'node_modules', '*')] }
51
+ : undefined;
52
+ const readComponent = createComponentReader(componentEntryFiles, { ...resolveOptions, optionRoots, fallbackPaths });
53
+ const warnings = [];
37
54
  for (const entryFile of componentEntryFiles) {
38
55
  try {
39
56
  for (const record of readComponent(entryFile)) {
40
57
  if (seenNames.has(record.name)) {
41
- console.warn(`Duplicate component name "${record.name}" (${path.relative(config.projectRoot, entryFile)}); the catalog will show only one.`);
58
+ const message = `Duplicate component name "${record.name}" (${path.relative(config.projectRoot, entryFile)}); the catalog will show only one.`;
59
+ console.warn(message);
60
+ warnings.push(message);
42
61
  }
43
62
  seenNames.add(record.name);
44
63
  components.push(record);
@@ -46,7 +65,9 @@ export function sync(config, resolveOptions) {
46
65
  }
47
66
  }
48
67
  catch (error) {
49
- console.warn(`Could not read ${path.relative(config.projectRoot, entryFile)}: ${error.message}`);
68
+ const message = `Could not read ${path.relative(config.projectRoot, entryFile)}: ${error.message}`;
69
+ console.warn(message);
70
+ warnings.push(message);
50
71
  }
51
72
  }
52
73
  components.sort((a, b) => a.name.localeCompare(b.name));
@@ -57,12 +78,14 @@ export function sync(config, resolveOptions) {
57
78
  tokenRecords.push(...readTokenModules([tokenFile]));
58
79
  }
59
80
  catch (error) {
60
- console.warn(`Could not read ${path.relative(config.projectRoot, tokenFile)}: ${error.message}`);
81
+ const message = `Could not read ${path.relative(config.projectRoot, tokenFile)}: ${error.message}`;
82
+ console.warn(message);
83
+ warnings.push(message);
61
84
  }
62
85
  }
63
86
  writeFileSync(path.join(generatedDir, 'tokens.json'), `${JSON.stringify(tokenRecords, null, 2)}\n`);
64
87
  writeFileSync(path.join(generatedDir, 'index.ts'), GENERATED_INDEX);
65
88
  const pageFiles = discoverPages({ components: componentRefs, standalonePageGlobs: config.pages ?? [], projectRoot: config.projectRoot });
66
89
  writePageIndex(generatedDir, pageFiles);
67
- return { componentCount: components.length, pageCount: pageFiles.length, generatedDir };
90
+ return { componentCount: components.length, pageCount: pageFiles.length, generatedDir, warnings };
68
91
  }