@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.
- package/README.md +182 -78
- package/dist/cli/agentsFile.js +31 -0
- package/dist/cli/config.js +26 -1
- package/dist/cli/dev.js +50 -5
- package/dist/cli/doctor.js +492 -0
- package/dist/cli/explain.js +151 -0
- package/dist/cli/githubAction.js +68 -0
- package/dist/cli/glob.js +8 -0
- package/dist/cli/init.js +152 -18
- package/dist/cli/main.js +115 -4
- package/dist/cli/pageIndex.js +27 -1
- package/dist/cli/props.js +170 -21
- package/dist/cli/staticPage.js +356 -0
- package/dist/cli/sync.js +28 -5
- package/dist/cli/workspace.js +49 -21
- package/dist/native/catalog/comparison.js +283 -0
- package/dist/native/catalog/tokens.js +119 -0
- package/dist/native/catalog/types.js +1 -0
- package/native/catalog/CatalogShell.tsx +6 -1
- package/native/catalog/CatalogSidebar.tsx +98 -5
- package/native/catalog/ComparisonGrid.tsx +9 -7
- package/native/catalog/SpacingScaleGallery.tsx +1 -1
- package/native/catalog/TokenRow.tsx +3 -2
- package/native/catalog/catalogNavigation.ts +1 -1
- package/native/catalog/comparison.ts +63 -5
- package/native/catalog/index.ts +8 -6
- package/native/catalog/logoLayout.ts +12 -0
- package/native/catalog/pageApi.ts +21 -3
- package/native/catalog/tokens.ts +5 -0
- package/native/catalog/types.ts +9 -6
- package/package.json +12 -6
- package/starter-kit/WHEN_TO_USE.md +124 -0
- package/starter-kit/components/AnimatedChevron/AnimatedChevron.catalog.tsx +23 -0
- package/starter-kit/components/AnimatedChevron/AnimatedChevron.tsx +46 -0
- package/starter-kit/components/AnimatedChevron/index.ts +2 -0
- package/starter-kit/components/Avatar/Avatar.catalog.tsx +54 -0
- package/starter-kit/components/Avatar/Avatar.tsx +74 -0
- package/starter-kit/components/Avatar/index.ts +2 -0
- package/starter-kit/components/Badge/Badge.catalog.tsx +88 -0
- package/starter-kit/components/Badge/Badge.tsx +56 -0
- package/starter-kit/components/Badge/Badge.types.ts +18 -0
- package/starter-kit/components/Badge/index.ts +2 -0
- package/starter-kit/components/Banner/Banner.catalog.tsx +87 -0
- package/starter-kit/components/Banner/Banner.tsx +257 -0
- package/starter-kit/components/Banner/Banner.types.ts +38 -0
- package/starter-kit/components/Banner/index.ts +2 -0
- package/starter-kit/components/BottomSheet/BottomSheet.catalog.tsx +77 -0
- package/starter-kit/components/BottomSheet/BottomSheet.tsx +151 -0
- package/starter-kit/components/BottomSheet/index.ts +2 -0
- package/starter-kit/components/Button/Button.catalog.tsx +99 -0
- package/starter-kit/components/Button/Button.tsx +173 -0
- package/starter-kit/components/Button/Button.types.ts +52 -0
- package/starter-kit/components/Button/index.ts +2 -0
- package/starter-kit/components/ButtonGroup/ButtonGroup.catalog.tsx +39 -0
- package/starter-kit/components/ButtonGroup/ButtonGroup.tsx +84 -0
- package/starter-kit/components/ButtonGroup/index.ts +2 -0
- package/starter-kit/components/Card/Card.catalog.tsx +68 -0
- package/starter-kit/components/Card/Card.tsx +133 -0
- package/starter-kit/components/Card/index.ts +2 -0
- package/starter-kit/components/Checkbox/Checkbox.catalog.tsx +28 -0
- package/starter-kit/components/Checkbox/Checkbox.tsx +104 -0
- package/starter-kit/components/Checkbox/index.ts +2 -0
- package/starter-kit/components/Dialog/Dialog.catalog.tsx +41 -0
- package/starter-kit/components/Dialog/Dialog.tsx +82 -0
- package/starter-kit/components/Dialog/index.ts +2 -0
- package/starter-kit/components/Divider/Divider.catalog.tsx +35 -0
- package/starter-kit/components/Divider/Divider.tsx +20 -0
- package/starter-kit/components/Divider/index.ts +2 -0
- package/starter-kit/components/Dock/Dock.catalog.tsx +71 -0
- package/starter-kit/components/Dock/Dock.tsx +58 -0
- package/starter-kit/components/Dock/index.ts +2 -0
- package/starter-kit/components/Dropdown/Dropdown.catalog.tsx +69 -0
- package/starter-kit/components/Dropdown/Dropdown.tsx +162 -0
- package/starter-kit/components/Dropdown/index.ts +2 -0
- package/starter-kit/components/EmptyState/EmptyState.catalog.tsx +67 -0
- package/starter-kit/components/EmptyState/EmptyState.tsx +88 -0
- package/starter-kit/components/EmptyState/index.ts +2 -0
- package/starter-kit/components/FieldContainer/FieldContainer.catalog.tsx +76 -0
- package/starter-kit/components/FieldContainer/FieldContainer.tsx +99 -0
- package/starter-kit/components/FieldContainer/FieldContainer.types.ts +27 -0
- package/starter-kit/components/FieldContainer/index.ts +2 -0
- package/starter-kit/components/InputClearButton/InputClearButton.catalog.tsx +18 -0
- package/starter-kit/components/InputClearButton/InputClearButton.tsx +63 -0
- package/starter-kit/components/InputClearButton/index.ts +2 -0
- package/starter-kit/components/InputField/InputField.catalog.tsx +76 -0
- package/starter-kit/components/InputField/InputField.tsx +418 -0
- package/starter-kit/components/InputField/index.ts +2 -0
- package/starter-kit/components/List/List.catalog.tsx +30 -0
- package/starter-kit/components/List/List.tsx +36 -0
- package/starter-kit/components/List/index.ts +2 -0
- package/starter-kit/components/ListItem/ListItem.catalog.tsx +89 -0
- package/starter-kit/components/ListItem/ListItem.tsx +165 -0
- package/starter-kit/components/ListItem/index.ts +2 -0
- package/starter-kit/components/Loading/Loading.catalog.tsx +34 -0
- package/starter-kit/components/Loading/Loading.tsx +121 -0
- package/starter-kit/components/Loading/index.ts +2 -0
- package/starter-kit/components/Pill/Pill.catalog.tsx +79 -0
- package/starter-kit/components/Pill/Pill.tsx +181 -0
- package/starter-kit/components/Pill/Pill.types.ts +30 -0
- package/starter-kit/components/Pill/index.ts +2 -0
- package/starter-kit/components/PillRow/PillRow.catalog.tsx +91 -0
- package/starter-kit/components/PillRow/PillRow.tsx +140 -0
- package/starter-kit/components/PillRow/PillRow.types.ts +24 -0
- package/starter-kit/components/PillRow/index.ts +2 -0
- package/starter-kit/components/ProgressDots/ProgressDots.catalog.tsx +22 -0
- package/starter-kit/components/ProgressDots/ProgressDots.tsx +42 -0
- package/starter-kit/components/ProgressDots/index.ts +2 -0
- package/starter-kit/components/Radio/Radio.catalog.tsx +38 -0
- package/starter-kit/components/Radio/Radio.tsx +114 -0
- package/starter-kit/components/Radio/index.ts +2 -0
- package/starter-kit/components/Radio/usePressScale.ts +33 -0
- package/starter-kit/components/SearchField/SearchField.catalog.tsx +28 -0
- package/starter-kit/components/SearchField/SearchField.tsx +98 -0
- package/starter-kit/components/SearchField/index.ts +2 -0
- package/starter-kit/components/SectionHeader/SectionHeader.catalog.tsx +72 -0
- package/starter-kit/components/SectionHeader/SectionHeader.tsx +139 -0
- package/starter-kit/components/SectionHeader/index.ts +2 -0
- package/starter-kit/components/SegmentedToggle/SegmentedToggle.catalog.tsx +31 -0
- package/starter-kit/components/SegmentedToggle/SegmentedToggle.tsx +163 -0
- package/starter-kit/components/SegmentedToggle/index.ts +2 -0
- package/starter-kit/components/SegmentedToggle/useSlideAnim.ts +29 -0
- package/starter-kit/components/Shimmer/Breathing.catalog.tsx +29 -0
- package/starter-kit/components/Shimmer/Shimmer.catalog.tsx +89 -0
- package/starter-kit/components/Shimmer/Shimmer.tsx +158 -0
- package/starter-kit/components/Shimmer/Shimmer.types.ts +27 -0
- package/starter-kit/components/Shimmer/SkeletonGroup.catalog.tsx +39 -0
- package/starter-kit/components/Shimmer/index.ts +2 -0
- package/starter-kit/components/Surface/Surface.catalog.tsx +44 -0
- package/starter-kit/components/Surface/Surface.tsx +45 -0
- package/starter-kit/components/Surface/index.ts +2 -0
- package/starter-kit/components/Switch/Switch.catalog.tsx +33 -0
- package/starter-kit/components/Switch/Switch.tsx +129 -0
- package/starter-kit/components/Switch/index.ts +2 -0
- package/starter-kit/components/TextArea/TextArea.catalog.tsx +29 -0
- package/starter-kit/components/TextArea/TextArea.tsx +99 -0
- package/starter-kit/components/TextArea/index.ts +2 -0
- package/starter-kit/components/Toast/Toast.catalog.tsx +61 -0
- package/starter-kit/components/Toast/Toast.tsx +144 -0
- package/starter-kit/components/Toast/index.ts +2 -0
- package/starter-kit/components/Tooltip/Tooltip.catalog.tsx +121 -0
- package/starter-kit/components/Tooltip/Tooltip.tsx +195 -0
- package/starter-kit/components/Tooltip/index.ts +2 -0
- package/starter-kit/components/TopNav/TopNav.catalog.tsx +71 -0
- package/starter-kit/components/TopNav/TopNav.tsx +77 -0
- package/starter-kit/components/TopNav/index.ts +2 -0
- package/starter-kit/components/UnderlineTabs/UnderlineTabs.catalog.tsx +30 -0
- package/starter-kit/components/UnderlineTabs/UnderlineTabs.tsx +156 -0
- package/starter-kit/components/UnderlineTabs/index.ts +2 -0
- package/starter-kit/components/index.ts +36 -0
- package/starter-kit/components/statusColors.ts +27 -0
- package/starter-kit/icons/Icon.native.tsx +90 -0
- package/starter-kit/icons/index.ts +10 -0
- package/starter-kit/icons/paths.ts +623 -0
- package/starter-kit/icons/types.ts +69 -0
- package/starter-kit/pages/Colors.catalog.tsx +55 -0
- package/starter-kit/pages/Font.catalog.tsx +45 -0
- package/starter-kit/pages/Icons.catalog.tsx +51 -0
- package/starter-kit/pages/Manifest.catalog.tsx +24 -0
- package/starter-kit/pages/Motion.catalog.tsx +101 -0
- package/starter-kit/pages/Radius.catalog.tsx +35 -0
- package/starter-kit/pages/ReportIssue.catalog.tsx +148 -0
- package/starter-kit/pages/SavedTrips.catalog.tsx +247 -0
- package/starter-kit/pages/Shadow.catalog.tsx +34 -0
- package/starter-kit/pages/Spacing.catalog.tsx +12 -0
- package/starter-kit/pages/TripPlannerForm.catalog.tsx +48 -0
- package/starter-kit/pages/Typography.catalog.tsx +17 -0
- package/starter-kit/tokens/index.ts +50 -0
- package/starter-kit/tokens/motion.ts +90 -0
- package/starter-kit/tokens/palette.ts +111 -0
- package/starter-kit/tokens/scales.ts +106 -0
- package/starter-kit/tokens/semantic.ts +98 -0
- package/starter-kit/tokens/shadow.ts +36 -0
- 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
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|