react-props-parser 0.0.1

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,21 @@
1
+ import ts from 'typescript';
2
+ /**
3
+ * Returns a ts.Program that has `filePath` up to date and ready to read,
4
+ * reusing a shared per-project ts.LanguageService rather than building a
5
+ * new ts.Program from scratch on every call. See the module comment above
6
+ * for why that distinction matters.
7
+ *
8
+ * Uses the *consuming project's* real tsconfig.json when one can be found
9
+ * (walking up from the file's directory), rather than a fixed set of
10
+ * compiler options.
11
+ *
12
+ * This matters beyond just matching the project's strictness settings:
13
+ * without the real `baseUrl`/`paths`, any import that relies on a path
14
+ * alias — including a cross-file `extends` base interface reached that
15
+ * way — fails to resolve, silently degrading to whatever TS falls back
16
+ * to rather than the actual project structure. Falls back to a fixed
17
+ * set of reasonable defaults when no tsconfig.json is found (e.g. a
18
+ * standalone fixture file with no project root), so parsing a bare
19
+ * .tsx file in isolation still works.
20
+ */
21
+ export declare function createProgramForFile(filePath: string): ts.Program;
@@ -0,0 +1,172 @@
1
+ import ts from 'typescript';
2
+ import path from 'node:path';
3
+ const FALLBACK_OPTIONS = {
4
+ target: ts.ScriptTarget.ES2020,
5
+ module: ts.ModuleKind.ESNext,
6
+ jsx: ts.JsxEmit.ReactJSX,
7
+ esModuleInterop: true,
8
+ skipLibCheck: true,
9
+ strict: true,
10
+ };
11
+ // Every file parsed with the same effective compiler options (i.e. every
12
+ // file under the same resolved tsconfig, or every file that falls back to
13
+ // FALLBACK_OPTIONS) shares one long-lived ts.LanguageService instead of
14
+ // getting its own throwaway ts.Program. A LanguageService reparses only
15
+ // the files whose version actually changed since the last getProgram()
16
+ // call — it re-checks the rest (React's own types, lib.dom.d.ts, sibling
17
+ // component files already seen) from its cached ASTs. Building a fresh
18
+ // ts.Program per file, as this used to do, redid that shared work (often
19
+ // hundreds of files' worth of parsing/binding) on every single call —
20
+ // the dominant cost behind slow Storybook startup with many components.
21
+ //
22
+ // Keyed by resolved tsconfig path (or FALLBACK_KEY when none is found),
23
+ // since that's exactly the granularity at which compiler options — and
24
+ // therefore which files can safely share one Program — are the same.
25
+ const FALLBACK_KEY = '\0fallback';
26
+ const projectCache = new Map();
27
+ // Memoizes the two filesystem walks resolveCompilerOptions used to redo
28
+ // on every call: ts.findConfigFile (per directory) and reading/parsing
29
+ // the tsconfig it finds (per resolved config path). Many files share a
30
+ // directory or a tsconfig, so this turns O(files) I/O into O(directories)
31
+ // + O(tsconfigs).
32
+ const configPathByDir = new Map();
33
+ const optionsByConfigPath = new Map();
34
+ const registry = ts.createDocumentRegistry();
35
+ /**
36
+ * Returns a ts.Program that has `filePath` up to date and ready to read,
37
+ * reusing a shared per-project ts.LanguageService rather than building a
38
+ * new ts.Program from scratch on every call. See the module comment above
39
+ * for why that distinction matters.
40
+ *
41
+ * Uses the *consuming project's* real tsconfig.json when one can be found
42
+ * (walking up from the file's directory), rather than a fixed set of
43
+ * compiler options.
44
+ *
45
+ * This matters beyond just matching the project's strictness settings:
46
+ * without the real `baseUrl`/`paths`, any import that relies on a path
47
+ * alias — including a cross-file `extends` base interface reached that
48
+ * way — fails to resolve, silently degrading to whatever TS falls back
49
+ * to rather than the actual project structure. Falls back to a fixed
50
+ * set of reasonable defaults when no tsconfig.json is found (e.g. a
51
+ * standalone fixture file with no project root), so parsing a bare
52
+ * .tsx file in isolation still works.
53
+ */
54
+ export function createProgramForFile(filePath) {
55
+ const { key, options, projectDir } = resolveProjectForFile(filePath);
56
+ const entry = getOrCreateProject(key, options, projectDir);
57
+ entry.rootFiles.add(filePath);
58
+ // Always bump the requested file's own version so its content is
59
+ // re-read from disk on every call (needed for correctness across
60
+ // repeated parse() calls / watch-mode edits) — sibling files already
61
+ // in rootFiles keep their cached version, which is what lets the
62
+ // LanguageService skip re-parsing them.
63
+ entry.versions.set(filePath, (entry.versions.get(filePath) ?? 0) + 1);
64
+ const program = entry.service.getProgram();
65
+ if (!program) {
66
+ throw new Error(`Could not build a TypeScript program for: ${filePath}`);
67
+ }
68
+ return program;
69
+ }
70
+ function resolveProjectForFile(filePath) {
71
+ const configPath = resolveConfigPathForDir(path.dirname(filePath));
72
+ if (!configPath) {
73
+ return { key: FALLBACK_KEY, options: FALLBACK_OPTIONS, projectDir: process.cwd() };
74
+ }
75
+ let options = optionsByConfigPath.get(configPath);
76
+ if (!options) {
77
+ options = readCompilerOptions(configPath);
78
+ optionsByConfigPath.set(configPath, options);
79
+ }
80
+ return { key: configPath, options, projectDir: path.dirname(configPath) };
81
+ }
82
+ function resolveConfigPathForDir(dir) {
83
+ if (configPathByDir.has(dir))
84
+ return configPathByDir.get(dir) ?? null;
85
+ const foundPath = ts.findConfigFile(dir, ts.sys.fileExists);
86
+ const resolved = foundPath ? resolveSolutionStyleReferences(foundPath) : null;
87
+ configPathByDir.set(dir, resolved);
88
+ return resolved;
89
+ }
90
+ function readCompilerOptions(configPath) {
91
+ const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
92
+ if (configFile.error)
93
+ return FALLBACK_OPTIONS;
94
+ const parsed = ts.parseJsonConfigFileContent(configFile.config, ts.sys, path.dirname(configPath));
95
+ // A project's tsconfig may not set jsx at all if it doesn't compile
96
+ // .tsx directly (e.g. relies on a bundler for that) — without it, TS
97
+ // can't parse JSX syntax at all, so every component file would fail
98
+ // outright. Fill it in rather than let real project configs break.
99
+ return {
100
+ ...parsed.options,
101
+ jsx: parsed.options.jsx ?? ts.JsxEmit.ReactJSX,
102
+ };
103
+ }
104
+ function getOrCreateProject(key, options, projectDir) {
105
+ const cached = projectCache.get(key);
106
+ if (cached)
107
+ return cached;
108
+ const rootFiles = new Set();
109
+ const versions = new Map();
110
+ const host = {
111
+ getScriptFileNames: () => [...rootFiles],
112
+ getScriptVersion: (fileName) => String(versions.get(fileName) ?? 0),
113
+ getScriptSnapshot: (fileName) => {
114
+ if (!ts.sys.fileExists(fileName))
115
+ return undefined;
116
+ const text = ts.sys.readFile(fileName);
117
+ return text !== undefined ? ts.ScriptSnapshot.fromString(text) : undefined;
118
+ },
119
+ getCurrentDirectory: () => projectDir,
120
+ getCompilationSettings: () => options,
121
+ getDefaultLibFileName: (opts) => ts.getDefaultLibFilePath(opts),
122
+ fileExists: ts.sys.fileExists,
123
+ readFile: ts.sys.readFile,
124
+ readDirectory: ts.sys.readDirectory,
125
+ directoryExists: ts.sys.directoryExists,
126
+ getDirectories: ts.sys.getDirectories,
127
+ realpath: ts.sys.realpath,
128
+ };
129
+ const entry = {
130
+ service: ts.createLanguageService(host, registry),
131
+ rootFiles,
132
+ versions,
133
+ };
134
+ projectCache.set(key, entry);
135
+ return entry;
136
+ }
137
+ /**
138
+ * A "solution-style" root tsconfig.json — `{ "files": [], "references":
139
+ * [...] }`, common in real projects split into build/test/etc. sub-
140
+ * projects — declares no compilerOptions of its own; the actual settings
141
+ * (target, paths, jsx, ...) live in one of the referenced configs. Taking
142
+ * such a root at face value silently produces near-empty options: no
143
+ * `paths`, meaning any path-aliased import — including a cross-file
144
+ * `extends` base interface reached that way — fails to resolve.
145
+ *
146
+ * Follows the first `references` entry (recursively, depth-bounded)
147
+ * until landing on a config that either declares its own compilerOptions
148
+ * (which may itself `extends` a base config — parseJsonConfigFileContent
149
+ * follows that chain natively) or has no further references to follow.
150
+ */
151
+ function resolveSolutionStyleReferences(configPath, depth = 0) {
152
+ if (depth > 5)
153
+ return configPath;
154
+ const raw = ts.readConfigFile(configPath, ts.sys.readFile);
155
+ if (raw.error || !raw.config)
156
+ return configPath;
157
+ const hasOwnOptions = raw.config.compilerOptions && Object.keys(raw.config.compilerOptions).length > 0;
158
+ const references = raw.config.references;
159
+ if (hasOwnOptions || !Array.isArray(references) || references.length === 0) {
160
+ return configPath;
161
+ }
162
+ const firstRef = references[0];
163
+ if (!firstRef?.path)
164
+ return configPath;
165
+ const refTarget = path.resolve(path.dirname(configPath), firstRef.path);
166
+ const nextConfigPath = ts.sys.fileExists(refTarget)
167
+ ? refTarget
168
+ : path.join(refTarget, 'tsconfig.json');
169
+ if (!ts.sys.fileExists(nextConfigPath))
170
+ return configPath;
171
+ return resolveSolutionStyleReferences(nextConfigPath, depth + 1);
172
+ }
@@ -0,0 +1,52 @@
1
+ import ts from 'typescript';
2
+ import { type ResolvedParseOptions } from './options.js';
3
+ export interface ResolvedPropsType {
4
+ type: ts.Type;
5
+ /** Node to resolve individual prop symbols' types/locations against. */
6
+ contextNode: ts.Node;
7
+ displayName: string;
8
+ /**
9
+ * Symbol to read the top-level jsdoc description from — the *local*
10
+ * type reference's own symbol, not necessarily `type.symbol`/
11
+ * `type.aliasSymbol`. Those reflect whatever the resolved type's
12
+ * own definition is, which for something like `Partial<FullUserFields>`
13
+ * is TypeScript's *built-in* `Partial` alias — picking up its doc
14
+ * comment ("Make all properties in T optional") would be wrong. This
15
+ * is resolved from the actual identifier as written at the reference
16
+ * site (or the declaration site, for the bare-fallback case), which
17
+ * correctly follows to an imported type's own symbol without also
18
+ * picking up an unrelated wrapper utility type's docs.
19
+ */
20
+ docSymbol: ts.Symbol | undefined;
21
+ }
22
+ /**
23
+ * Finds the props type for a component in a source file, resolving it
24
+ * via the checker rather than by name-matching declarations in the same
25
+ * file. This matters because in a typical real codebase the props type
26
+ * is declared in a sibling `types.ts` and imported — using the checker
27
+ * to resolve whatever type node the component's signature actually
28
+ * points at follows that import for free, the same way it follows an
29
+ * `extends` base interface across files. Checked several ways, since
30
+ * "the props type" shows up in different places depending on how the
31
+ * component is written:
32
+ * - a parameter's own type annotation, `(props: XProps) => ...` or
33
+ * `({ a, b }: XProps) => ...` (destructured — the annotation is on
34
+ * the parameter, not affected by the binding pattern)
35
+ * - the variable's own type annotation, `const X: React.FC<XProps> = ({ a }) => ...`
36
+ * - any call expression with type arguments assigned to a variable —
37
+ * `forwardRef<Ref, XProps>((props, ref) => ...)`,
38
+ * `SomeWrapper<XProps>(Component, ...)`, `memo<XProps>(...)`, etc.
39
+ * Generalized rather than hardcoded to forwardRef/memo by name, since
40
+ * real component libraries wrap components in their own HOCs using
41
+ * the same shape (a type argument that *is* the props type).
42
+ * - a `memo(...)`/any other wrapper with no type arguments of its own —
43
+ * handled for free by walking the whole tree rather than only the
44
+ * top level of a function declaration or variable initializer, so
45
+ * the inner function/arrow expression's own parameter annotation is
46
+ * still found.
47
+ *
48
+ * Falls back to "first exported `Props`-or-`*Props`-named declaration
49
+ * in this file" when no component signature can be resolved at all
50
+ * (e.g. a bare type-only fixture with no component function).
51
+ */
52
+ export declare function resolvePropsType(sourceFile: ts.SourceFile, checker: ts.TypeChecker, options?: ResolvedParseOptions): ResolvedPropsType | undefined;
@@ -0,0 +1,206 @@
1
+ import ts from 'typescript';
2
+ import { DEFAULT_PARSE_OPTIONS } from './options.js';
3
+ import { truncateTypeName } from './utils/truncateTypeName.js';
4
+ /**
5
+ * Finds the props type for a component in a source file, resolving it
6
+ * via the checker rather than by name-matching declarations in the same
7
+ * file. This matters because in a typical real codebase the props type
8
+ * is declared in a sibling `types.ts` and imported — using the checker
9
+ * to resolve whatever type node the component's signature actually
10
+ * points at follows that import for free, the same way it follows an
11
+ * `extends` base interface across files. Checked several ways, since
12
+ * "the props type" shows up in different places depending on how the
13
+ * component is written:
14
+ * - a parameter's own type annotation, `(props: XProps) => ...` or
15
+ * `({ a, b }: XProps) => ...` (destructured — the annotation is on
16
+ * the parameter, not affected by the binding pattern)
17
+ * - the variable's own type annotation, `const X: React.FC<XProps> = ({ a }) => ...`
18
+ * - any call expression with type arguments assigned to a variable —
19
+ * `forwardRef<Ref, XProps>((props, ref) => ...)`,
20
+ * `SomeWrapper<XProps>(Component, ...)`, `memo<XProps>(...)`, etc.
21
+ * Generalized rather than hardcoded to forwardRef/memo by name, since
22
+ * real component libraries wrap components in their own HOCs using
23
+ * the same shape (a type argument that *is* the props type).
24
+ * - a `memo(...)`/any other wrapper with no type arguments of its own —
25
+ * handled for free by walking the whole tree rather than only the
26
+ * top level of a function declaration or variable initializer, so
27
+ * the inner function/arrow expression's own parameter annotation is
28
+ * still found.
29
+ *
30
+ * Falls back to "first exported `Props`-or-`*Props`-named declaration
31
+ * in this file" when no component signature can be resolved at all
32
+ * (e.g. a bare type-only fixture with no component function).
33
+ */
34
+ export function resolvePropsType(sourceFile, checker, options = DEFAULT_PARSE_OPTIONS) {
35
+ const fromSignature = findPropsTypeFromComponentSignature(sourceFile, checker, options);
36
+ if (fromSignature)
37
+ return fromSignature;
38
+ let fallback;
39
+ ts.forEachChild(sourceFile, (node) => {
40
+ if (fallback)
41
+ return;
42
+ if (ts.isInterfaceDeclaration(node) && isPropsName(node.name.text))
43
+ fallback = node;
44
+ if (ts.isTypeAliasDeclaration(node) && isPropsName(node.name.text))
45
+ fallback = node;
46
+ });
47
+ if (!fallback)
48
+ return undefined;
49
+ return {
50
+ type: checker.getTypeAtLocation(fallback),
51
+ contextNode: fallback,
52
+ displayName: fallback.name.text,
53
+ docSymbol: checker.getSymbolAtLocation(fallback.name),
54
+ };
55
+ }
56
+ function findPropsTypeFromComponentSignature(sourceFile, checker, options) {
57
+ let found;
58
+ const resolveFromTypeNode = (typeNode, contextNode) => {
59
+ if (found)
60
+ return;
61
+ const type = checker.getTypeFromTypeNode(typeNode);
62
+ // Not a plausible props type (e.g. a primitive parameter on some
63
+ // unrelated helper function encountered while walking the tree) —
64
+ // skip rather than confidently claim it.
65
+ if (!(type.getFlags() & ts.TypeFlags.Object) && !type.isUnion() && !type.isIntersection()) {
66
+ return;
67
+ }
68
+ // A props type is a plain data shape — it never has call/construct
69
+ // signatures. A parameter typed `React.ComponentType<XProps>` (or
70
+ // `FC<...>`, `ElementType<...>`, a class reference, etc.) is *itself*
71
+ // union-shaped and object-flagged, so it would otherwise pass the
72
+ // check above and get misidentified as the props type — this is
73
+ // exactly what a HOC factory's first parameter looks like
74
+ // (`withExtraProps(WrappedComponent: React.ComponentType<Props>)`),
75
+ // and matching it hands back React's own static properties
76
+ // (`contextTypes`, `defaultProps`, ...) instead of the real props.
77
+ const branches = type.isUnion() ? type.types : [type];
78
+ if (branches.every((t) => t.getCallSignatures().length > 0 || t.getConstructSignatures().length > 0)) {
79
+ return;
80
+ }
81
+ // Resolve documentation from the identifier actually written at
82
+ // this reference site (e.g. "XProps" in `props: XProps`), not from
83
+ // the resolved type's own symbol — see ResolvedPropsType.docSymbol.
84
+ const docSymbol = ts.isTypeReferenceNode(typeNode)
85
+ ? checker.getSymbolAtLocation(ts.isQualifiedName(typeNode.typeName) ? typeNode.typeName.right : typeNode.typeName)
86
+ : (type.aliasSymbol ?? type.symbol);
87
+ found = {
88
+ type,
89
+ contextNode,
90
+ displayName: typeDisplayName(typeNode, type, checker, options),
91
+ docSymbol,
92
+ };
93
+ };
94
+ const checkParams = (params) => {
95
+ if (found || params.length === 0)
96
+ return;
97
+ const paramType = params[0].type;
98
+ if (paramType)
99
+ resolveFromTypeNode(paramType, params[0]);
100
+ };
101
+ // `const X: React.FC<XProps> = (...) => ...` — the props type is a
102
+ // type argument on the *variable's* annotation, not the parameter.
103
+ //
104
+ // Requires the variable to be exported: without the old React.FC-
105
+ // name-only filter, matching *any* generic type reference here would
106
+ // false-positive on unrelated code (`const cache: Map<string, X> = ...`).
107
+ // An unexported local isn't a plausible component to document anyway.
108
+ const checkVariableTypeAnnotation = (decl) => {
109
+ if (found || !decl.type || !ts.isTypeReferenceNode(decl.type) || !isExported(decl))
110
+ return;
111
+ const propsArg = decl.type.typeArguments?.[0];
112
+ if (propsArg)
113
+ resolveFromTypeNode(propsArg, decl);
114
+ };
115
+ // Any `SomeWrapper<..., XProps, ...>(...)` call assigned to a
116
+ // variable — forwardRef, memo, or a project's own HOC. Not hardcoded
117
+ // to a specific wrapper name: tries each type argument in order and
118
+ // takes the first that resolves to a plausible object/union/
119
+ // intersection type, since which position holds the props type
120
+ // varies by wrapper (forwardRef's is typically the 2nd, a simple
121
+ // custom wrapper's is often the 1st). Same export requirement as
122
+ // above, for the same reason (avoid matching e.g. a local
123
+ // `useMemo<Y>(...)` call).
124
+ const checkWrapperCall = (decl) => {
125
+ if (found || !decl.initializer || !ts.isCallExpression(decl.initializer) || !isExported(decl)) {
126
+ return;
127
+ }
128
+ const typeArgs = decl.initializer.typeArguments;
129
+ if (!typeArgs || typeArgs.length === 0)
130
+ return;
131
+ // Prefer a type argument whose own name looks like a props type
132
+ // (the near-universal `*Props` convention) over just taking the
133
+ // first object-like one — forwardRef<HTMLButtonElement, XProps>
134
+ // would otherwise match the ref type first, since it's an object
135
+ // type too.
136
+ const byName = typeArgs.find((arg) => ts.isTypeReferenceNode(arg) && /Props$/.test(rightmostName(arg.typeName)));
137
+ if (byName) {
138
+ resolveFromTypeNode(byName, decl);
139
+ if (found)
140
+ return;
141
+ }
142
+ for (const typeArg of typeArgs) {
143
+ resolveFromTypeNode(typeArg, decl);
144
+ if (found)
145
+ return;
146
+ }
147
+ };
148
+ const visit = (node) => {
149
+ if (found)
150
+ return;
151
+ if (ts.isFunctionDeclaration(node)) {
152
+ checkParams(node.parameters);
153
+ }
154
+ // Checked regardless of where this function/arrow expression sits
155
+ // in the tree (top-level initializer, wrapped in memo(...), the
156
+ // second argument to forwardRef(...), etc.) — forEachChild below
157
+ // reaches it either way, so no wrapper needs special-casing here.
158
+ if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
159
+ checkParams(node.parameters);
160
+ }
161
+ if (ts.isVariableStatement(node)) {
162
+ for (const decl of node.declarationList.declarations) {
163
+ checkVariableTypeAnnotation(decl);
164
+ checkWrapperCall(decl);
165
+ }
166
+ }
167
+ ts.forEachChild(node, visit);
168
+ };
169
+ visit(sourceFile);
170
+ return found;
171
+ }
172
+ function typeDisplayName(typeNode, type, checker, options) {
173
+ if (ts.isTypeReferenceNode(typeNode)) {
174
+ return rightmostName(typeNode.typeName);
175
+ }
176
+ // A component's real parameter annotation is often an intersection
177
+ // like `XProps & InternalProps & WrapperProps` — internal/wrapper
178
+ // additions mixed in alongside the type a human actually thinks of
179
+ // as "the props type". Prefer the first `*Props`-named constituent
180
+ // over the full (long, implementation-detail-laden) intersection
181
+ // string as the display name; the full prop set from every
182
+ // constituent is still returned regardless, this only affects the
183
+ // one-line name shown for the component.
184
+ if (ts.isIntersectionTypeNode(typeNode)) {
185
+ const namedProps = typeNode.types.find((t) => ts.isTypeReferenceNode(t) && /Props$/.test(rightmostName(t.typeName)));
186
+ if (namedProps && ts.isTypeReferenceNode(namedProps)) {
187
+ return rightmostName(namedProps.typeName);
188
+ }
189
+ }
190
+ // Union/inline object literal/anything else — no single clean
191
+ // reference name on the node itself; fall back to whatever symbol
192
+ // the checker resolved, or the stringified type as a last resort.
193
+ return (type.symbol?.name ??
194
+ type.aliasSymbol?.name ??
195
+ truncateTypeName(checker.typeToString(type), options.maxTypeNameLength));
196
+ }
197
+ function isExported(decl) {
198
+ const statement = decl.parent.parent;
199
+ return !!(ts.getCombinedModifierFlags(statement) & ts.ModifierFlags.Export);
200
+ }
201
+ function rightmostName(name) {
202
+ return ts.isQualifiedName(name) ? name.right.text : name.text;
203
+ }
204
+ function isPropsName(name) {
205
+ return name === 'Props' || name.endsWith('Props');
206
+ }
@@ -0,0 +1,86 @@
1
+ export interface PropDescriptor {
2
+ name: string;
3
+ required: boolean;
4
+ type: {
5
+ /** Raw TS type text, e.g. "string", "'a' | 'b'" */
6
+ name: string;
7
+ /**
8
+ * Present only when this prop's type is a union of object shapes
9
+ * (e.g. `{type:'a',foo} | {type:'b',bar}`) — one entry per branch,
10
+ * each with its own resolved props. This is what lets a rich
11
+ * ArgsTable render per-variant fields instead of one opaque
12
+ * stringified union.
13
+ */
14
+ elements?: UnionBranch[];
15
+ /**
16
+ * Present when this prop's type resolves to a plain object shape
17
+ * (a named interface/type-alias reference, or an inline object
18
+ * type) — its own fields, one level deep, each with jsdoc intact.
19
+ * `name` stays the original reference (e.g. "AvatarUser") so the
20
+ * type identity is still visible; this is the expanded shape
21
+ * alongside it, not a replacement for it.
22
+ */
23
+ properties?: Record<string, PropDescriptor>;
24
+ /**
25
+ * Present when this prop's type is itself a function (a named
26
+ * function type alias, an `interface` call signature, or an inline
27
+ * `(...) => ...` type) — one entry per parameter of its call
28
+ * signature. Each parameter's own type is expanded the same way a
29
+ * regular object prop's would be (via `properties`), but only when
30
+ * it resolves to a type declared in the user's own project —
31
+ * expanding a built-in like `MouseEvent` would just surface
32
+ * TypeScript's own lib.dom.d.ts internals, which isn't what anyone
33
+ * wants to see in a props table.
34
+ */
35
+ parameters?: ParameterDescriptor[];
36
+ /**
37
+ * Present alongside `parameters` — the call signature's return type,
38
+ * expanded via `properties` under the same user-defined-only rule.
39
+ */
40
+ returnType?: FunctionTypePart;
41
+ };
42
+ description?: string;
43
+ /** From an `@default` jsdoc tag, e.g. `@default 3` -> "3" */
44
+ defaultValue?: {
45
+ value: string;
46
+ };
47
+ }
48
+ export interface ParameterDescriptor {
49
+ name: string;
50
+ required: boolean;
51
+ type: FunctionTypePart;
52
+ }
53
+ interface FunctionTypePart {
54
+ name: string;
55
+ /** Same rule as `PropDescriptor.type.properties` — only expanded for user-defined types, never for a built-in like `MouseEvent`. */
56
+ properties?: Record<string, PropDescriptor>;
57
+ }
58
+ export interface UnionBranch {
59
+ /** The literal discriminant prop shared across all branches, if one exists (e.g. `type: 'a'`). */
60
+ discriminant?: {
61
+ name: string;
62
+ value: string;
63
+ };
64
+ props: Record<string, PropDescriptor>;
65
+ }
66
+ export interface Documentation {
67
+ /** Name of the exported component/props type */
68
+ displayName: string;
69
+ description?: string;
70
+ /**
71
+ * Flattened prop set. When the top-level Props type is itself a
72
+ * union of object shapes, this is only the properties common to
73
+ * every branch (often empty) — variant-specific props, including
74
+ * their jsdoc, live in `elements` instead. This is the exact gap
75
+ * plain react-docgen has for a top-level union Props type: it
76
+ * reports the flattened set and drops per-variant jsdoc entirely.
77
+ */
78
+ props: Record<string, PropDescriptor>;
79
+ /**
80
+ * Present only when the top-level Props type is a union of object
81
+ * shapes (e.g. `type Props = AProps | BProps | ...`) — one entry
82
+ * per branch, each with its own resolved props and jsdoc intact.
83
+ */
84
+ elements?: UnionBranch[];
85
+ }
86
+ export {};
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ import ts from 'typescript';
2
+ export declare function getSymbolDescription(symbol: ts.Symbol, checker: ts.TypeChecker): string | undefined;
3
+ /** Reads an `@default <value>` jsdoc tag off a symbol, e.g. `@default 3` -> "3". */
4
+ export declare function getDefaultValueTag(symbol: ts.Symbol, checker: ts.TypeChecker): string | undefined;
@@ -0,0 +1,14 @@
1
+ import ts from 'typescript';
2
+ export function getSymbolDescription(symbol, checker) {
3
+ const parts = symbol.getDocumentationComment(checker);
4
+ const text = ts.displayPartsToString(parts).trim();
5
+ return text.length > 0 ? text : undefined;
6
+ }
7
+ /** Reads an `@default <value>` jsdoc tag off a symbol, e.g. `@default 3` -> "3". */
8
+ export function getDefaultValueTag(symbol, checker) {
9
+ const tag = symbol.getJsDocTags(checker).find((t) => t.name === 'default');
10
+ if (!tag)
11
+ return undefined;
12
+ const text = tag.text ? ts.displayPartsToString(tag.text).trim() : '';
13
+ return text.length > 0 ? text : undefined;
14
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Caps a rendered type-name string (`checker.typeToString(...)` output)
3
+ * at `maxLength` characters, replacing the overflow with a single `…`.
4
+ *
5
+ * `checker.typeToString` has no length limit of its own — a utility-type
6
+ * chain like `JssSupportedProperty<Pick<RheaTextProps, "content" |
7
+ * "ariaLabel" | "semanticTag" | ...>>` can run to hundreds of characters,
8
+ * which overflows fixed-width UI (e.g. Storybook's ArgsTable) instead of
9
+ * wrapping. Truncating here, once, at the source keeps every consumer of
10
+ * `Documentation` simple — no repeated CSS `text-overflow` workarounds
11
+ * downstream, and the same cap applies whether the value ends up in a
12
+ * table cell, a tooltip, or a JSON dump.
13
+ *
14
+ * `maxLength` of `Infinity` (or any falsy/`<= 0` value skipped by the
15
+ * caller) disables truncation entirely.
16
+ */
17
+ export declare function truncateTypeName(name: string, maxLength: number): string;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Caps a rendered type-name string (`checker.typeToString(...)` output)
3
+ * at `maxLength` characters, replacing the overflow with a single `…`.
4
+ *
5
+ * `checker.typeToString` has no length limit of its own — a utility-type
6
+ * chain like `JssSupportedProperty<Pick<RheaTextProps, "content" |
7
+ * "ariaLabel" | "semanticTag" | ...>>` can run to hundreds of characters,
8
+ * which overflows fixed-width UI (e.g. Storybook's ArgsTable) instead of
9
+ * wrapping. Truncating here, once, at the source keeps every consumer of
10
+ * `Documentation` simple — no repeated CSS `text-overflow` workarounds
11
+ * downstream, and the same cap applies whether the value ends up in a
12
+ * table cell, a tooltip, or a JSON dump.
13
+ *
14
+ * `maxLength` of `Infinity` (or any falsy/`<= 0` value skipped by the
15
+ * caller) disables truncation entirely.
16
+ */
17
+ export function truncateTypeName(name, maxLength) {
18
+ if (maxLength === Infinity || name.length <= maxLength)
19
+ return name;
20
+ if (maxLength <= 1)
21
+ return '…';
22
+ return `${name.slice(0, maxLength - 1)}…`;
23
+ }