@krapwoo/ds-viewer 0.2.0 → 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.
@@ -0,0 +1,151 @@
1
+ import path from 'node:path';
2
+ import { readStaticPages } from "./staticPage.js";
3
+ import { discoverAllPageFiles, resolvePageId } from "./pageIndex.js";
4
+ import { resolveGlob } from "./glob.js";
5
+ import { axisItems, choosePlacement, gridColumnLimit, listGeometry, presentationBlocks, MATRIX_LAYOUT, } from "../native/catalog/comparison.js";
6
+ import { CATALOG_LAYOUT } from "../native/catalog/tokens.js";
7
+ function placeholderItem(item) {
8
+ return { key: item.key, name: item.name, node: null, group: item.group };
9
+ }
10
+ /** A throwaway `SectionDef` built from a `StaticPage` (Task 3) — every specimen's `node` is `null`
11
+ * (design §4: specimen nodes are never evaluated), since none of the layout functions below ever
12
+ * look at it; only the counts/keys/sizes they do look at are carried over. Every field Task 3
13
+ * reads is carried through here, including `previewWidths`/`fullWidthLabel`/`hide`/`itemsFill` —
14
+ * an earlier draft dropped these four, so `explain` reported different geometry than the real
15
+ * viewer for any page that set one of them (25 kit pages set `itemsFill`; every `kit-host`
16
+ * Viewer page sets `hide`). */
17
+ function toSectionDef(page) {
18
+ const comparison = page.comparison && {
19
+ rowLabel: page.comparison.rowLabel ?? '',
20
+ columnLabel: page.comparison.columnLabel ?? '',
21
+ rows: page.comparison.rows.prop !== undefined ? { prop: page.comparison.rows.prop, items: page.comparison.rows.items } : page.comparison.rows.items,
22
+ columns: page.comparison.columns.prop !== undefined ? { prop: page.comparison.columns.prop, items: page.comparison.columns.items } : page.comparison.columns.items,
23
+ cells: [],
24
+ size: page.comparison.size,
25
+ };
26
+ return {
27
+ id: resolvePageId(page, page.file),
28
+ description: '',
29
+ path: page.file,
30
+ tokenGallery: page.tokenGallery,
31
+ fullWidthLabel: page.fullWidthLabel,
32
+ specimenSize: page.specimenSize,
33
+ previewWidths: page.previewWidths,
34
+ variants: page.variantsItems ? { itemsFill: page.variantsItemsFill, items: page.variantsItems.map(placeholderItem) } : undefined,
35
+ states: page.statesItems ? { itemsFill: page.statesItemsFill, items: page.statesItems.map(placeholderItem) } : undefined,
36
+ comparison,
37
+ render: page.hasRender ? () => null : undefined,
38
+ hide: (page.hideVariants || page.hideStates) ? { variants: page.hideVariants, states: page.hideStates } : undefined,
39
+ };
40
+ }
41
+ function explainBlock(block) {
42
+ if (block.kind === 'grid') {
43
+ const rows = axisItems(block.comparison.rows).length;
44
+ const columns = axisItems(block.comparison.columns).length;
45
+ const limit = gridColumnLimit(block.size);
46
+ return {
47
+ kind: 'grid', title: block.title,
48
+ reason: `${rows} rows × ${columns} columns (${block.size}); ${columns} of ${limit} max ${block.size} columns used — ` +
49
+ `${columns <= limit ? 'fits' : 'does NOT fit'} a 1280px laptop.`,
50
+ };
51
+ }
52
+ if (block.kind === 'list') {
53
+ const geometry = listGeometry(block.items.length, MATRIX_LAYOUT.laptopContentWidth, block.size);
54
+ return {
55
+ kind: 'list', title: block.title,
56
+ reason: `${block.items.length} items (${block.size}) → ${geometry.columns} columns × ${geometry.rows} row${geometry.rows === 1 ? '' : 's'}` +
57
+ (geometry.fillers > 0 ? ` (${geometry.fillers} filler${geometry.fillers === 1 ? '' : 's'})` : '') + '.',
58
+ };
59
+ }
60
+ if (block.kind === 'grouped') {
61
+ return { kind: 'grouped', title: block.title, reason: `${block.groups.length} variant row(s), each with its own configurations.` };
62
+ }
63
+ if (block.kind === 'preview') {
64
+ const widths = block.widths === 'full' ? 'full width' : `${block.widths.join('/')}px`;
65
+ return { kind: 'preview', title: block.title, reason: `render() content at ${widths}.` };
66
+ }
67
+ return { kind: 'empty', title: block.title, reason: block.message };
68
+ }
69
+ /** Mirrors `native/catalog/SectionBlock.tsx`'s own `blockWidth` helper: a list block's width is its
70
+ * own natural, content-hugging width (`listGeometry`'s `containerWidth`) — the real viewer only
71
+ * ever moves a *second* block beside a *first* one when that first block doesn't already fill the
72
+ * whole row, so `choosePlacement` only reads `first.width` (never `second.width`) — but it's
73
+ * computed uniformly here for either role, since setting it on `second` is simply unused, not wrong. */
74
+ function toPlacementBlock(block, height, available) {
75
+ if (block.kind === 'list') {
76
+ return { kind: 'list', height, itemCount: block.items.length, size: block.size, width: listGeometry(block.items.length, available, block.size).containerWidth };
77
+ }
78
+ return { kind: block.kind, height };
79
+ }
80
+ /** Finds `pageId` among every page `cli/doctor.ts`'s own `discoverAllPageFiles` would find, builds
81
+ * its throwaway `SectionDef`, and asks the viewer's own pure layout functions for each block's
82
+ * decision — undefined when no page resolves to that id. */
83
+ export function explainPage(config, pageId, options = {}) {
84
+ // Same exclusion `cli/doctor.ts`'s `runDoctor` applies (mirroring `sync`) — an excluded folder's
85
+ // page should never resolve here either.
86
+ const excluded = new Set((config.exclude ?? []).flatMap((pattern) => resolveGlob(config.projectRoot, pattern)));
87
+ const componentFolders = [
88
+ ...new Set(config.components
89
+ .flatMap((pattern) => resolveGlob(config.projectRoot, pattern))
90
+ .filter((file) => !excluded.has(file))
91
+ .map((file) => path.dirname(file))),
92
+ ];
93
+ const pageFiles = discoverAllPageFiles({ componentFolders, standalonePageGlobs: config.pages ?? [], projectRoot: config.projectRoot });
94
+ const page = readStaticPages(pageFiles).find((p) => resolvePageId(p, p.file) === pageId);
95
+ if (!page)
96
+ return undefined;
97
+ // Important finding 4 (Fable's review): a page whose layout data isn't a literal (or that has a
98
+ // syntax error) must never be explained from partial/empty data — design §4 requires `explain`
99
+ // to never drift from what the viewer actually renders.
100
+ if (!page.checkable)
101
+ return { pageId, notCheckable: true, blocks: [] };
102
+ const def = toSectionDef(page);
103
+ const blocks = presentationBlocks(def, { defaultPreviewWidths: [402] });
104
+ const explained = blocks.map(explainBlock);
105
+ let placement;
106
+ if (blocks.length === 2) {
107
+ if (options.heights) {
108
+ const [firstHeight, secondHeight] = options.heights;
109
+ const decision = choosePlacement({
110
+ available: MATRIX_LAYOUT.laptopContentWidth,
111
+ gap: CATALOG_LAYOUT.blockGap,
112
+ first: toPlacementBlock(blocks[0], firstHeight, MATRIX_LAYOUT.laptopContentWidth),
113
+ second: toPlacementBlock(blocks[1], secondHeight, MATRIX_LAYOUT.laptopContentWidth),
114
+ });
115
+ placement = {
116
+ decision,
117
+ reason: `With heights ${firstHeight}px/${secondHeight}px on a 1280px laptop (${MATRIX_LAYOUT.laptopContentWidth}px content width): ${decision}.`,
118
+ };
119
+ }
120
+ else {
121
+ placement = {
122
+ decision: 'decided-in-viewer',
123
+ reason: "Side-by-side placement depends on each block's measured height — decided in the viewer at runtime. Pass --heights <first>,<second> to check it here.",
124
+ };
125
+ }
126
+ }
127
+ return { pageId, blocks: explained, placement };
128
+ }
129
+ export function formatExplain(result) {
130
+ if (result.notCheckable) {
131
+ return `${result.pageId}\n\nPage not statically checkable — some of its layout data is not a literal doctor can read.`;
132
+ }
133
+ const lines = [result.pageId, ''];
134
+ for (const block of result.blocks) {
135
+ lines.push(block.title);
136
+ lines.push(` ${block.reason}`);
137
+ lines.push('');
138
+ }
139
+ if (result.placement) {
140
+ lines.push('Placement');
141
+ lines.push(` ${result.placement.reason}`);
142
+ }
143
+ return lines.join('\n').replace(/\n+$/, '');
144
+ }
145
+ /** `"80,300"` → `[80, 300]`; undefined for anything else (not exactly two positive numbers). */
146
+ export function parseHeights(raw) {
147
+ const parts = raw.split(',').map((part) => Number(part.trim()));
148
+ if (parts.length !== 2 || parts.some((n) => !Number.isFinite(n) || n <= 0))
149
+ return undefined;
150
+ return [parts[0], parts[1]];
151
+ }
@@ -0,0 +1,68 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ // Design §4 "GitHub Action" + §5's lockfile-detection assumption. `checkout`/`setup-node` pins
4
+ // reused verbatim from this repository's own `.github/workflows/ci.yml` (Global Constraints);
5
+ // `pnpm/action-setup` is the one new SHA this plan resolves (`gh api
6
+ // repos/pnpm/action-setup/git/refs/tags/v6.1.0`, dereferenced to its commit). `node-version: '20'`
7
+ // matches the package's own published `engines` floor (`>=20.19.0`): a user's project only ever
8
+ // runs the compiled `dist/cli/main.js` via `npx ds-viewer`, which needs no type-stripping (unlike
9
+ // this repo's own CI, which type-strips its *source* and therefore needs Node 22).
10
+ // Errata #5: `pnpm/action-setup` needs an explicit pnpm version — it has no lockfile to infer one
11
+ // from. Read the project's own `packageManager` field (`"pnpm@9.1.0"`) when present; otherwise
12
+ // default to 10, matching the pin's own major version line.
13
+ function pnpmVersion(projectRoot) {
14
+ const packageJsonPath = path.join(projectRoot, 'package.json');
15
+ if (!existsSync(packageJsonPath))
16
+ return '10';
17
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
18
+ const match = packageJson.packageManager?.match(/^pnpm@(\d+)/);
19
+ return match ? match[1] : '10';
20
+ }
21
+ // Important finding 6 (Fable's review): Yarn 1 (still the common Expo lockfile) has no
22
+ // `--immutable` flag — its equivalent is `--frozen-lockfile`. Yarn Berry needs `corepack enable`
23
+ // first, since `actions/setup-node` ships Yarn 1 on `ubuntu-latest`. A v1 lockfile's own first line
24
+ // is always exactly `# yarn lockfile v1`; anything else (Berry's own header/`__metadata`) is Berry.
25
+ function yarnInstallStep(projectRoot) {
26
+ const firstLine = readFileSync(path.join(projectRoot, 'yarn.lock'), 'utf8').split('\n', 1)[0];
27
+ if (firstLine === '# yarn lockfile v1')
28
+ return ' - run: yarn install --frozen-lockfile\n';
29
+ return ' - run: corepack enable\n - run: yarn install --immutable\n';
30
+ }
31
+ function installStep(projectRoot) {
32
+ if (existsSync(path.join(projectRoot, 'pnpm-lock.yaml'))) {
33
+ return ` - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0\n with:\n version: ${pnpmVersion(projectRoot)}\n - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0\n with:\n node-version: '20'\n - run: pnpm install --frozen-lockfile\n`;
34
+ }
35
+ const setupNode = ` - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0\n with:\n node-version: '20'\n`;
36
+ if (existsSync(path.join(projectRoot, 'yarn.lock')))
37
+ return `${setupNode}${yarnInstallStep(projectRoot)}`;
38
+ // `package-lock.json`, or no lockfile at all (a fresh project before its first commit) — `npm
39
+ // ci` is npm's own documented behavior for the latter: it installs from `package.json` and
40
+ // writes a fresh lockfile, exactly like a plain `npm install` would.
41
+ return `${setupNode} - run: npm ci\n`;
42
+ }
43
+ function workflowContent(projectRoot) {
44
+ return `name: DS Viewer
45
+
46
+ on:
47
+ pull_request:
48
+
49
+ jobs:
50
+ doctor:
51
+ runs-on: ubuntu-latest
52
+ steps:
53
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
54
+ ${installStep(projectRoot)} - run: npx ds-viewer doctor --ci
55
+ `;
56
+ }
57
+ /** Design §4 "GitHub Action", §1 Ownership: written once, never overwritten — a user's own edits
58
+ * to this file (or a differently-named workflow they wrote instead) are never touched again. The
59
+ * install step is picked from whichever lockfile is present *right now*, at `init` time — never
60
+ * re-picked later, since this function never runs again once the file exists. */
61
+ export function ensureGithubAction(projectRoot) {
62
+ const workflowPath = path.join(projectRoot, '.github', 'workflows', 'ds-viewer.yml');
63
+ if (existsSync(workflowPath))
64
+ return [];
65
+ mkdirSync(path.dirname(workflowPath), { recursive: true });
66
+ writeFileSync(workflowPath, workflowContent(projectRoot));
67
+ return [workflowPath];
68
+ }
package/dist/cli/init.js CHANGED
@@ -4,6 +4,8 @@ import { createInterface } from 'node:readline/promises';
4
4
  import { execFileSync } from 'node:child_process';
5
5
  import { preflight } from "./preflight.js";
6
6
  import { detectComponentFolders, detectTokenFiles } from "./detect.js";
7
+ import { ensureAgentsFileSection } from "./agentsFile.js";
8
+ import { ensureGithubAction } from "./githubAction.js";
7
9
  async function promptConfirm(message) {
8
10
  // A closed, non-interactive stdin (CI, piped npx) would reject `question` with an AbortError;
9
11
  // say how to proceed instead.
@@ -104,8 +106,8 @@ export async function initExistingProject(projectRoot, options = {}) {
104
106
  return { messages, written };
105
107
  }
106
108
  /** Design §2, "Both paths then write what is missing": `ds-viewer.config.ts` is written by each
107
- * path separately (its contents differ), but the package script, the devDependency entry, and the
108
- * `.gitignore` entry are identical either way. */
109
+ * path separately (its contents differ), but the package script, the devDependency entry, the
110
+ * `.gitignore` entry, and (from 0.3) the `AGENTS.md` section are identical either way. */
109
111
  function ensureSharedProjectFiles(projectRoot) {
110
112
  const written = [];
111
113
  const packageJsonPath = path.join(projectRoot, 'package.json');
@@ -134,6 +136,8 @@ function ensureSharedProjectFiles(projectRoot) {
134
136
  writeFileSync(gitignorePath, `${existingGitignore}${separator}${gitignoreEntry}\n`);
135
137
  written.push(gitignorePath);
136
138
  }
139
+ written.push(...ensureAgentsFileSection(projectRoot));
140
+ written.push(...ensureGithubAction(projectRoot));
137
141
  return written;
138
142
  }
139
143
  const KIT_REQUIRED_PACKAGES = ['react-native-svg', 'react-native-safe-area-context'];
package/dist/cli/main.js CHANGED
@@ -6,6 +6,8 @@ import { resolveConfig } from "./config.js";
6
6
  import { initExistingProject, initNewProject, promptInitMode } from "./init.js";
7
7
  import { dev } from "./dev.js";
8
8
  import { sync } from "./sync.js";
9
+ import { formatHuman, runDoctor, toDoctorJson } from "./doctor.js";
10
+ import { explainPage, formatExplain, parseHeights } from "./explain.js";
9
11
  const USAGE = `Usage: ds-viewer <command>
10
12
 
11
13
  Commands:
@@ -16,6 +18,13 @@ Commands:
16
18
  --yes accept the detected components and tokens without asking (--existing only)
17
19
  sync Regenerate the component, props, token, and page data from source
18
20
  dev Sync, then serve the catalog on a local web address and keep it current as files change
21
+ doctor Check every page for drift and coverage gaps
22
+ --json print the machine-readable report instead
23
+ --ci exit 1 when any error was found (never on a warning alone)
24
+ explain <Page>
25
+ Print why a page's specimens are laid out the way they are
26
+ --heights <first>,<second> check side-by-side placement with these measured heights
27
+ --json print the machine-readable report instead
19
28
 
20
29
  Options:
21
30
  -h, --help Show this help
@@ -95,6 +104,31 @@ export async function runInit(projectRoot, rest, options = {}) {
95
104
  ? initNewProject(projectRoot, { kitRoot, installer: options.installer })
96
105
  : initExistingProject(projectRoot, { yes: rest.includes('--yes'), confirm: options.confirm });
97
106
  }
107
+ /** Pulled out of `main()` for the same reason `runInit` was in 0.1/0.2 (Fable's review): testable
108
+ * without spawning a process. `--ci`'s exit code is read from `runDoctor`'s own `exitCode` — a
109
+ * plain `doctor` (no `--ci`) always returns 0 here regardless of errors found. */
110
+ export function runDoctorCommand(projectRoot, rest) {
111
+ const config = resolveConfig(projectRoot);
112
+ const result = runDoctor(config);
113
+ const output = rest.includes('--json') ? JSON.stringify(toDoctorJson(result), null, 2) : formatHuman(result);
114
+ return { output, exitCode: rest.includes('--ci') ? result.exitCode : 0 };
115
+ }
116
+ /** Same "testable without spawning" shape as `runDoctorCommand` above. */
117
+ export function runExplainCommand(projectRoot, pageId, rest) {
118
+ if (!pageId)
119
+ return { output: 'Usage: ds-viewer explain <Page> [--heights <first>,<second>] [--json]', exitCode: 1 };
120
+ const heightsIndex = rest.indexOf('--heights');
121
+ const heightsRaw = heightsIndex !== -1 ? rest[heightsIndex + 1] : undefined;
122
+ const heights = heightsRaw ? parseHeights(heightsRaw) : undefined;
123
+ if (heightsRaw && !heights) {
124
+ return { output: `--heights must be two comma-separated numbers, e.g. --heights 420,610 (got "${heightsRaw}")`, exitCode: 1 };
125
+ }
126
+ const config = resolveConfig(projectRoot);
127
+ const result = explainPage(config, pageId, { heights });
128
+ if (!result)
129
+ return { output: `No page named "${pageId}" was found.`, exitCode: 1 };
130
+ return { output: rest.includes('--json') ? JSON.stringify(result, null, 2) : formatExplain(result), exitCode: 0 };
131
+ }
98
132
  async function main() {
99
133
  const [command, ...rest] = process.argv.slice(2);
100
134
  const projectRoot = process.cwd();
@@ -122,6 +156,21 @@ async function main() {
122
156
  console.log(`Synced ${result.componentCount} components, ${result.pageCount} pages.`);
123
157
  return;
124
158
  }
159
+ if (command === 'doctor') {
160
+ const { output, exitCode } = runDoctorCommand(projectRoot, rest);
161
+ console.log(output);
162
+ if (exitCode)
163
+ process.exitCode = exitCode;
164
+ return;
165
+ }
166
+ if (command === 'explain') {
167
+ const [pageId, ...explainRest] = rest;
168
+ const { output, exitCode } = runExplainCommand(projectRoot, pageId, explainRest);
169
+ console.log(output);
170
+ if (exitCode)
171
+ process.exitCode = exitCode;
172
+ return;
173
+ }
125
174
  if (command === 'dev') {
126
175
  const config = resolveConfig(projectRoot);
127
176
  await dev(config);
@@ -1,4 +1,4 @@
1
- import { existsSync, writeFileSync } from 'node:fs';
1
+ import { existsSync, readdirSync, writeFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { resolveGlob } from "./glob.js";
4
4
  /** Component pages: `<Export>.catalog.tsx` beside the component's entry file (design §2). A
@@ -10,6 +10,25 @@ export function discoverPages(input) {
10
10
  const standalonePages = input.standalonePageGlobs.flatMap((pattern) => resolveGlob(input.projectRoot, pattern));
11
11
  return [...componentPages, ...standalonePages].sort();
12
12
  }
13
+ /** Every `*.catalog.tsx`/`.catalog.ts` file `doctor` should read — unlike `discoverPages`, this
14
+ * also finds a page beside a component whose folder still exists but whose export was renamed or
15
+ * removed (that page's file never changed, so it's still sitting right there). `componentFolders`
16
+ * is every distinct folder containing a currently-resolved component entry file — `cli/doctor.ts`
17
+ * derives it the same way `cli/sync.ts` does, by resolving `config.components` and taking each
18
+ * match's `path.dirname`. A folder that was deleted entirely (not just renamed within) has nothing
19
+ * left to find here, which is correct: there's no stale page file left on disk to warn about. */
20
+ export function discoverAllPageFiles(input) {
21
+ const componentPages = input.componentFolders.flatMap((folder) => {
22
+ try {
23
+ return readdirSync(folder).filter((name) => /\.catalog\.tsx?$/.test(name)).map((name) => path.join(folder, name));
24
+ }
25
+ catch {
26
+ return [];
27
+ }
28
+ });
29
+ const standalonePages = input.standalonePageGlobs.flatMap((pattern) => resolveGlob(input.projectRoot, pattern));
30
+ return [...new Set([...componentPages, ...standalonePages])].sort();
31
+ }
13
32
  /** Writes `.ds-viewer/generated/pages.ts`: imports every discovered page's default export,
14
33
  * resolves each one's id (design §2 "Page id": `id ?? component ?? <file stem>` — a component
15
34
  * page defaults to its export name, a standalone page to its own file's stem) and `file` (the
@@ -25,6 +44,13 @@ export function writePageIndex(generatedDir, pageFiles) {
25
44
  writeFileSync(outFile, `${importLines.join('\n')}\n\n${resolveLines.join('\n')}\n\n${exportLine}\n`);
26
45
  return outFile;
27
46
  }
47
+ /** `page.id ?? page.component ?? <file's own stem>` (design §2 "Page id") — factored out of
48
+ * `writePageIndex`'s own inline logic above so `cli/doctor.ts` resolves a page's id exactly the
49
+ * same way, never a second, drifting copy of this rule. Takes the same two fields `StaticPage`
50
+ * (Task 3) and the generated page index both already carry. */
51
+ export function resolvePageId(page, file) {
52
+ return page.id ?? page.component ?? fileStem(file);
53
+ }
28
54
  function toImportPath(fromDir, toFile) {
29
55
  const withoutExt = toFile.replace(/\.tsx?$/, '');
30
56
  const relativePath = path.relative(fromDir, withoutExt).split(path.sep).join('/');
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.