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.
- package/LICENSE +21 -0
- package/README.md +158 -0
- package/dist/extractProperties.d.ts +23 -0
- package/dist/extractProperties.js +213 -0
- package/dist/handlers/union.d.ts +15 -0
- package/dist/handlers/union.js +71 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +93 -0
- package/dist/options.d.ts +24 -0
- package/dist/options.js +10 -0
- package/dist/program.d.ts +21 -0
- package/dist/program.js +172 -0
- package/dist/resolvePropsType.d.ts +52 -0
- package/dist/resolvePropsType.js +206 -0
- package/dist/types.d.ts +86 -0
- package/dist/types.js +1 -0
- package/dist/utils/jsdoc.d.ts +4 -0
- package/dist/utils/jsdoc.js +14 -0
- package/dist/utils/truncateTypeName.d.ts +17 -0
- package/dist/utils/truncateTypeName.js +23 -0
- package/package.json +70 -0
- package/src/vite/argTypes.js +32 -0
- package/src/vite/argTypesEnhancer.js +172 -0
- package/src/vite/index.js +101 -0
- package/src/vite/preset.js +12 -0
- package/src/webpack/index.js +104 -0
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-props-parser",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "TypeScript-aware React props parser: expands discriminated unions, unions of objects, and nested object props into structured data (via the TypeScript compiler API) instead of stringifying types like react-docgen-typescript does. Ships with a Vite plugin and a webpack loader for wiring the parsed docgen info into Storybook.",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"registry": "https://registry.npmjs.org/",
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"react",
|
|
11
|
+
"docgen",
|
|
12
|
+
"react-docgen",
|
|
13
|
+
"typescript",
|
|
14
|
+
"storybook",
|
|
15
|
+
"props",
|
|
16
|
+
"documentation",
|
|
17
|
+
"vite-plugin",
|
|
18
|
+
"webpack-loader"
|
|
19
|
+
],
|
|
20
|
+
"type": "module",
|
|
21
|
+
"main": "dist/index.js",
|
|
22
|
+
"types": "dist/index.d.ts",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./vite": "./src/vite/index.js",
|
|
29
|
+
"./vite/preset": "./src/vite/preset.js",
|
|
30
|
+
"./webpack": "./src/webpack/index.js",
|
|
31
|
+
"./package.json": "./package.json"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"src/vite",
|
|
36
|
+
"src/webpack",
|
|
37
|
+
"LICENSE"
|
|
38
|
+
],
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"author": "Tural Hajiyev",
|
|
41
|
+
"homepage": "https://github.com/turalowski/react-props-parser#readme",
|
|
42
|
+
"bugs": "https://github.com/turalowski/react-props-parser/issues",
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "https://github.com/turalowski/react-props-parser.git"
|
|
46
|
+
},
|
|
47
|
+
"workspaces": [
|
|
48
|
+
"examples"
|
|
49
|
+
],
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsc -p tsconfig.json",
|
|
52
|
+
"prepublishOnly": "npm run build",
|
|
53
|
+
"test": "vitest run",
|
|
54
|
+
"test:watch": "vitest"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"typescript": "^5.6.0"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"vitest": "^2.1.0",
|
|
61
|
+
"@types/node": "^22.0.0",
|
|
62
|
+
"@types/react": "^18.3.0",
|
|
63
|
+
"react": "^18.3.0"
|
|
64
|
+
},
|
|
65
|
+
"allowScripts": {
|
|
66
|
+
"esbuild@0.21.5": true,
|
|
67
|
+
"fsevents@2.3.3": true,
|
|
68
|
+
"esbuild@0.28.2": true
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Given a component's __docgenInfo (as produced by react-props-parser + our
|
|
3
|
+
* vite plugin) and the discriminant value a particular story uses
|
|
4
|
+
* (e.g. 'text'), returns a Storybook `argTypes` override that hides
|
|
5
|
+
* every field belonging to *other* branches.
|
|
6
|
+
*
|
|
7
|
+
* Storybook's Controls panel merges `meta.argTypes` (built from the
|
|
8
|
+
* full, unioned `docgenInfo.props`) with each story's own `argTypes`.
|
|
9
|
+
* A field can be hidden per-story via `table: { disable: true }` —
|
|
10
|
+
* this computes exactly that set from `elements`, so a "Text" story
|
|
11
|
+
* only shows text-branch fields and a "CheckboxGroup" story only
|
|
12
|
+
* shows checkbox-group fields, without hand-listing them per story.
|
|
13
|
+
*/
|
|
14
|
+
export function argTypesForVariant(docgenInfo, discriminantValue) {
|
|
15
|
+
const elements = docgenInfo?.elements;
|
|
16
|
+
if (!elements) return {};
|
|
17
|
+
|
|
18
|
+
const wanted = JSON.stringify(discriminantValue);
|
|
19
|
+
const branch = elements.find((e) => e.discriminant?.value === wanted);
|
|
20
|
+
if (!branch) return {};
|
|
21
|
+
|
|
22
|
+
const allowedNames = new Set(Object.keys(branch.props));
|
|
23
|
+
const overrides = {};
|
|
24
|
+
|
|
25
|
+
for (const name of Object.keys(docgenInfo.props)) {
|
|
26
|
+
if (!allowedNames.has(name)) {
|
|
27
|
+
overrides[name] = { table: { disable: true } };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return overrides;
|
|
32
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storybook argTypesEnhancer + preview annotation in one file: this
|
|
3
|
+
* whole module gets bundled into the preview iframe (registered via
|
|
4
|
+
* previewAnnotations in preset.js), so `argTypesEnhancers` needs to be
|
|
5
|
+
* exported at the top level exactly like it would be from a project's
|
|
6
|
+
* own .storybook/preview.ts.
|
|
7
|
+
*
|
|
8
|
+
* Runs automatically for every story — no per-story wiring needed.
|
|
9
|
+
* Reads the rendering component's own __docgenInfo (attached by our
|
|
10
|
+
* vite plugin) and the story's args to figure out which union branch
|
|
11
|
+
* is active, then:
|
|
12
|
+
* - hides every field that belongs to a different branch
|
|
13
|
+
* - restores each visible field's branch-specific jsdoc description.
|
|
14
|
+
* core's merged top-level `props` deliberately drops a field's
|
|
15
|
+
* description when branches disagree on it (e.g. the discriminant
|
|
16
|
+
* field itself, like `type`, usually has a different doc comment
|
|
17
|
+
* per branch) rather than show the wrong text. Once we know which
|
|
18
|
+
* branch is actually active, there's no ambiguity left, so we can
|
|
19
|
+
* fill the correct one back in here.
|
|
20
|
+
*/
|
|
21
|
+
function rdrpArgTypesEnhancer(context) {
|
|
22
|
+
const docgenInfo = context.component?.__docgenInfo;
|
|
23
|
+
const elements = docgenInfo?.elements;
|
|
24
|
+
if (!elements || elements.length === 0) return context.argTypes;
|
|
25
|
+
|
|
26
|
+
const discriminantName = elements.find((e) => e.discriminant)?.discriminant?.name;
|
|
27
|
+
if (!discriminantName) return context.argTypes;
|
|
28
|
+
|
|
29
|
+
const currentValue = context.args?.[discriminantName] ?? context.initialArgs?.[discriminantName];
|
|
30
|
+
if (currentValue === undefined) return context.argTypes;
|
|
31
|
+
|
|
32
|
+
const branch = elements.find((e) => e.discriminant?.value === JSON.stringify(currentValue));
|
|
33
|
+
if (!branch) return context.argTypes;
|
|
34
|
+
|
|
35
|
+
const allowedNames = new Set(Object.keys(branch.props));
|
|
36
|
+
const argTypes = { ...context.argTypes };
|
|
37
|
+
|
|
38
|
+
for (const name of Object.keys(argTypes)) {
|
|
39
|
+
if (!allowedNames.has(name)) {
|
|
40
|
+
argTypes[name] = { ...argTypes[name], table: { ...argTypes[name]?.table, disable: true } };
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const branchDescription = branch.props[name]?.description;
|
|
45
|
+
if (branchDescription && !argTypes[name]?.description) {
|
|
46
|
+
argTypes[name] = { ...argTypes[name], description: branchDescription };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return argTypes;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* For a prop whose type resolved to a nested object shape (core's
|
|
55
|
+
* `type.properties`, e.g. `user: AvatarUser`, or `tags: Tag[]`) or a
|
|
56
|
+
* union of object shapes (`type.elements`, e.g. `action: LinkAction |
|
|
57
|
+
* ButtonAction`), Storybook's default Controls table would otherwise
|
|
58
|
+
* just show the bare reference name(s) as the Type column with no way
|
|
59
|
+
* to see the actual fields.
|
|
60
|
+
*
|
|
61
|
+
* Storybook's Type column supports a summary/detail pair —
|
|
62
|
+
* `table.type.summary` is the visible, clickable text; `table.type.
|
|
63
|
+
* detail` opens in a popover on click. This keeps summary as the
|
|
64
|
+
* original type name(s) (e.g. "AvatarUser", "Tag[]", or "LinkAction |
|
|
65
|
+
* ButtonAction") — clickable, recognizable — and puts the full
|
|
66
|
+
* expanded shape in detail, rendered recursively so an
|
|
67
|
+
* interface-inside-an-interface (core expands up to 2 levels) shows
|
|
68
|
+
* its own nested shape too, not just the outer one.
|
|
69
|
+
*
|
|
70
|
+
* Also disables Storybook's default interactive "object" control for
|
|
71
|
+
* every array-typed prop (`control: false`). react-docgen-typescript-
|
|
72
|
+
* shaped type names it can't otherwise classify (any named reference,
|
|
73
|
+
* including plain arrays like `Tag[]` or `string[]`) fall back to that
|
|
74
|
+
* generic control, which edits the *runtime* value via react-inspector
|
|
75
|
+
* — clicking its "show non-enumerable properties" toggle on an array
|
|
76
|
+
* value walks Array.prototype, surfacing push/pop/map/... as if they
|
|
77
|
+
* were editable fields. There's no safe/useful live-edit UI for an
|
|
78
|
+
* array here anyway, so this table.type.detail popover (read-only,
|
|
79
|
+
* static, no prototype involved) replaces it as the way to inspect the
|
|
80
|
+
* shape instead.
|
|
81
|
+
*/
|
|
82
|
+
function rdrpNestedShapeEnhancer(context) {
|
|
83
|
+
const props = context.component?.__docgenInfo?.props;
|
|
84
|
+
if (!props) return context.argTypes;
|
|
85
|
+
|
|
86
|
+
const argTypes = { ...context.argTypes };
|
|
87
|
+
|
|
88
|
+
for (const [name, prop] of Object.entries(props)) {
|
|
89
|
+
if (!argTypes[name]) continue;
|
|
90
|
+
|
|
91
|
+
const isArray = isArrayTypeName(prop.type?.name);
|
|
92
|
+
const detail = expandedShapeFor(prop);
|
|
93
|
+
|
|
94
|
+
argTypes[name] = {
|
|
95
|
+
...argTypes[name],
|
|
96
|
+
...(isArray ? { control: false } : {}),
|
|
97
|
+
...(detail
|
|
98
|
+
? { table: { ...argTypes[name].table, type: { summary: prop.type.name, detail } } }
|
|
99
|
+
: {}),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return argTypes;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** True for a TS-printed array type name — "Tag[]", "string[]", "readonly Tag[]" — covering every form checker.typeToString produces for an array type. */
|
|
107
|
+
function isArrayTypeName(name) {
|
|
108
|
+
return typeof name === 'string' && /\[\]$/.test(name);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Renders the expanded shape for one PropDescriptor — its `type.
|
|
113
|
+
* properties` (wrapped in `Array<...>` when the prop's own type name is
|
|
114
|
+
* an array, e.g. `tags: Tag[]` renders as `Array<{ ... }>`, not a bare
|
|
115
|
+
* `{ ... }` that reads as if `tags` held a single Tag) or its `type.
|
|
116
|
+
* elements` (each union branch's shape, joined with ` | `). Returns
|
|
117
|
+
* undefined when there's no structure to expand, e.g. a plain
|
|
118
|
+
* `string[]` — a regular array has nothing more useful to show than
|
|
119
|
+
* its type name, which is already the clickable-free (non-object-
|
|
120
|
+
* control) Type column text.
|
|
121
|
+
*/
|
|
122
|
+
function expandedShapeFor(prop, indent = 0) {
|
|
123
|
+
if (prop.type?.properties) {
|
|
124
|
+
const shape = renderShape(prop.type.properties, indent);
|
|
125
|
+
return isArrayTypeName(prop.type.name) ? `Array<${shape}>` : shape;
|
|
126
|
+
}
|
|
127
|
+
if (prop.type?.elements) {
|
|
128
|
+
return prop.type.elements.map((branch) => renderShape(branch.props, indent)).join(' | ');
|
|
129
|
+
}
|
|
130
|
+
if (prop.type?.parameters || prop.type?.returnType) {
|
|
131
|
+
return renderFunctionSignature(prop.type.parameters, prop.type.returnType, indent);
|
|
132
|
+
}
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Renders a function-shaped prop's (core's `type.parameters`/`type.
|
|
138
|
+
* returnType`) full call signature as `(param: Type, ...) => Return` —
|
|
139
|
+
* each part's own type is expanded inline via `renderShape` when core
|
|
140
|
+
* resolved it to a nested object shape (a user-defined interface, never
|
|
141
|
+
* a built-in like `MouseEvent`), the same way a plain object prop's
|
|
142
|
+
* nested fields are. A `void`/`undefined`/`any`/`unknown` return (core
|
|
143
|
+
* omits `returnType` for those) falls back to the generic `...`, same
|
|
144
|
+
* as before this had a real return type to show.
|
|
145
|
+
*/
|
|
146
|
+
function renderFunctionSignature(parameters, returnType, indent) {
|
|
147
|
+
const params = (parameters ?? [])
|
|
148
|
+
.map((p) => {
|
|
149
|
+
const valueType = p.type?.properties ? renderShape(p.type.properties, indent) : p.type?.name ?? 'unknown';
|
|
150
|
+
return `${p.name}${p.required ? '' : '?'}: ${valueType}`;
|
|
151
|
+
})
|
|
152
|
+
.join(', ');
|
|
153
|
+
|
|
154
|
+
const returns = returnType?.properties ? renderShape(returnType.properties, indent) : returnType?.name ?? '...';
|
|
155
|
+
|
|
156
|
+
return `(${params}) => ${returns}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Recursively renders a props map as a pretty-printed object shape, expanding nested properties/elements at any depth core resolved. */
|
|
160
|
+
function renderShape(properties, indent = 0) {
|
|
161
|
+
const pad = ' '.repeat(indent + 1);
|
|
162
|
+
const closePad = ' '.repeat(indent);
|
|
163
|
+
|
|
164
|
+
const fields = Object.values(properties).map((p) => {
|
|
165
|
+
const valueType = expandedShapeFor(p, indent + 1) ?? p.type?.name ?? 'unknown';
|
|
166
|
+
return `${pad}${p.name}${p.required ? '' : '?'}: ${valueType}`;
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
return `{\n${fields.join(';\n')};\n${closePad}}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export const argTypesEnhancers = [rdrpArgTypesEnhancer, rdrpNestedShapeEnhancer];
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { parse } from 'react-props-parser';
|
|
2
|
+
|
|
3
|
+
export { argTypesForVariant } from './argTypes.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Minimal Vite plugin: for each .tsx file, runs it through react-props-parser's
|
|
7
|
+
* parse() and appends `ComponentName.__docgenInfo = {...}` to the
|
|
8
|
+
* transformed module — the same static-property convention Storybook's
|
|
9
|
+
* addon-docs / ArgsTable already reads from
|
|
10
|
+
* @joshwooding/vite-plugin-react-docgen-typescript, so no Storybook-side
|
|
11
|
+
* changes are needed to see the result.
|
|
12
|
+
*
|
|
13
|
+
* Dummy-sandbox scope: finds component names via a simple regex
|
|
14
|
+
* (`export function X` / `export const X =` / `export { X as Y }`)
|
|
15
|
+
* rather than a real AST walk — good enough to prove the wiring end to
|
|
16
|
+
* end, but see findComponentNames' own comment for a real limitation
|
|
17
|
+
* this has on files with multiple differently-scoped exports.
|
|
18
|
+
*
|
|
19
|
+
* @param {import('react-props-parser').ParseOptions} [options] Forwarded
|
|
20
|
+
* as-is to every `parse()` call — e.g. `viteLoader({ maxTypeNameLength: 80 })`.
|
|
21
|
+
*/
|
|
22
|
+
export function viteLoader(options) {
|
|
23
|
+
return {
|
|
24
|
+
name: 'rdrp-docgen',
|
|
25
|
+
enforce: 'pre',
|
|
26
|
+
transform(code, id) {
|
|
27
|
+
if (!id.endsWith('.tsx') || id.includes('node_modules') || id.includes('.stories.')) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Only inject when we found real exported component identifiers
|
|
32
|
+
// in the source. Never fall back to documentation.displayName as
|
|
33
|
+
// an injection target — parse() will happily resolve a "props
|
|
34
|
+
// type" for a hook or utility function too (anything with an
|
|
35
|
+
// object-shaped parameter), and its displayName in that case can
|
|
36
|
+
// be an arbitrary type string like "Partial<ConfigProps> |
|
|
37
|
+
// undefined" — not a valid JS identifier. Splicing that into
|
|
38
|
+
// generated code produces a syntax error that breaks the whole
|
|
39
|
+
// build, not just a skipped file, so this check is load-bearing,
|
|
40
|
+
// not an optimization.
|
|
41
|
+
const componentNames = findComponentNames(code);
|
|
42
|
+
if (componentNames.length === 0) return null;
|
|
43
|
+
|
|
44
|
+
let documentation;
|
|
45
|
+
try {
|
|
46
|
+
documentation = parse(id, options);
|
|
47
|
+
} catch {
|
|
48
|
+
// No Props type in this file (e.g. not a component file) — skip silently.
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Attaches to *every* plausible exported binding, not just one —
|
|
53
|
+
// a file commonly exports several differently-scoped versions of
|
|
54
|
+
// "the" component (an internal base, a public wrapper, a
|
|
55
|
+
// Storybook-specific unwrapped alias via `export { X as Y }`),
|
|
56
|
+
// and this module has no reliable way to know which one a given
|
|
57
|
+
// consumer will actually import. Missing the right one means
|
|
58
|
+
// Storybook shows names/types with zero descriptions (the props
|
|
59
|
+
// come from wherever *is* documented, but the rendered component
|
|
60
|
+
// itself carries no __docgenInfo at all) — worse than attaching
|
|
61
|
+
// the same (single, best-guess) props to every candidate.
|
|
62
|
+
// Known imprecision: every export gets identical props even
|
|
63
|
+
// when their real prop types differ (e.g. an internal-only
|
|
64
|
+
// export vs. its public-facing wrapper) — a real per-export fix
|
|
65
|
+
// needs core to resolve props per named export, not just once
|
|
66
|
+
// per file.
|
|
67
|
+
const injections = componentNames
|
|
68
|
+
.map((name) => {
|
|
69
|
+
const docgenInfo = { ...documentation, displayName: name };
|
|
70
|
+
return `try { ${name}.__docgenInfo = ${JSON.stringify(docgenInfo)}; } catch (e) {}`;
|
|
71
|
+
})
|
|
72
|
+
.join('\n');
|
|
73
|
+
|
|
74
|
+
return { code: `${code}\n${injections}\n`, map: null };
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function findComponentNames(code) {
|
|
80
|
+
const names = new Set();
|
|
81
|
+
|
|
82
|
+
for (const m of code.matchAll(/export function ([A-Z][A-Za-z0-9]*)\s*\(/g)) {
|
|
83
|
+
names.add(m[1]);
|
|
84
|
+
}
|
|
85
|
+
for (const m of code.matchAll(/export const ([A-Z][A-Za-z0-9]*)\s*[:=]/g)) {
|
|
86
|
+
names.add(m[1]);
|
|
87
|
+
}
|
|
88
|
+
// `export { X, Z as W }` — a plain re-export list. The *local* name
|
|
89
|
+
// (before `as`, if present) is what needs __docgenInfo attached,
|
|
90
|
+
// since that's the actual declared binding in this module's scope;
|
|
91
|
+
// whatever alias another file imports it as doesn't change the
|
|
92
|
+
// object identity.
|
|
93
|
+
for (const m of code.matchAll(/export\s*\{([^}]+)\}/g)) {
|
|
94
|
+
for (const part of m[1].split(',')) {
|
|
95
|
+
const localName = part.trim().split(/\s+as\s+/)[0].trim();
|
|
96
|
+
if (/^[A-Z][A-Za-z0-9]*$/.test(localName)) names.add(localName);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return [...names];
|
|
101
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Storybook preset entry point, referenced from `addons` in
|
|
2
|
+
// .storybook/main.ts. Registers our argTypesEnhancer into
|
|
3
|
+
// `previewAnnotations` — the same mechanism a project's own
|
|
4
|
+
// .storybook/preview.ts uses — so it's actually bundled into the
|
|
5
|
+
// preview iframe and runs for every story automatically.
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const enhancerPath = fileURLToPath(new URL('./argTypesEnhancer.js', import.meta.url));
|
|
9
|
+
|
|
10
|
+
export function previewAnnotations(entry = []) {
|
|
11
|
+
return [...entry, enhancerPath];
|
|
12
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { parse } from 'react-props-parser';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webpack loader equivalent of the vite loader (src/vite/index.js): same
|
|
5
|
+
* job (parse the file via react-props-parser, inject __docgenInfo onto the
|
|
6
|
+
* component so Storybook's addon-docs/Controls can read it), but
|
|
7
|
+
* wired in via webpackFinal's module.rules instead of a Vite
|
|
8
|
+
* transform hook, since Storybook's webpack5 builder doesn't go
|
|
9
|
+
* through Vite at all.
|
|
10
|
+
*
|
|
11
|
+
* `enforce: 'pre'` (set by the consumer's webpackFinal, see README)
|
|
12
|
+
* runs this before the framework's own babel/ts transform, so it
|
|
13
|
+
* receives raw .tsx source and can safely append plain JS at the end
|
|
14
|
+
* — the appended assignment statement is valid syntax standing alone
|
|
15
|
+
* in a TS/TSX file, so the next loader in the chain parses the whole
|
|
16
|
+
* thing, injected line included, without needing to know about us.
|
|
17
|
+
*
|
|
18
|
+
* findComponentNames below is intentionally duplicated from
|
|
19
|
+
* src/vite/index.js rather than shared — same small regex-based
|
|
20
|
+
* heuristic, kept next to each builder integration so neither depends
|
|
21
|
+
* on the other's internals.
|
|
22
|
+
*
|
|
23
|
+
* Loader options (e.g. `{ maxTypeNameLength: 80 }`) are set on the
|
|
24
|
+
* rule itself — `use: [{ loader: 'react-props-parser/webpack', options: {...} }]`
|
|
25
|
+
* — and read here via the standard webpack 5 `this.getOptions()`, then
|
|
26
|
+
* forwarded as-is to `parse()`. No schema validation: this loader
|
|
27
|
+
* takes exactly react-props-parser's own `ParseOptions` shape, so a
|
|
28
|
+
* typo here just surfaces the same way a bad `parse()` call would.
|
|
29
|
+
*/
|
|
30
|
+
export default function webpackLoader(source) {
|
|
31
|
+
const filePath = this.resourcePath;
|
|
32
|
+
const options = this.getOptions?.();
|
|
33
|
+
|
|
34
|
+
if (!filePath.endsWith('.tsx') || filePath.includes('node_modules') || filePath.includes('.stories.')) {
|
|
35
|
+
return source;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Only inject when we found real exported component identifiers in
|
|
39
|
+
// the source. Never fall back to documentation.displayName as an
|
|
40
|
+
// injection target — parse() will happily resolve a "props type"
|
|
41
|
+
// for a hook or utility function too (anything with an object-
|
|
42
|
+
// shaped parameter), and its displayName in that case can be an
|
|
43
|
+
// arbitrary type string like "Partial<ConfigProps> | undefined" —
|
|
44
|
+
// not a valid JS identifier. Splicing that into generated code
|
|
45
|
+
// produces a syntax error that breaks the whole build, not just a
|
|
46
|
+
// skipped file, so this check is load-bearing, not an optimization.
|
|
47
|
+
const componentNames = findComponentNames(source);
|
|
48
|
+
if (componentNames.length === 0) return source;
|
|
49
|
+
|
|
50
|
+
let documentation;
|
|
51
|
+
try {
|
|
52
|
+
documentation = parse(filePath, options);
|
|
53
|
+
} catch {
|
|
54
|
+
// No Props type in this file (e.g. not a component file) — skip silently.
|
|
55
|
+
return source;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Attaches to *every* plausible exported binding, not just one — a
|
|
59
|
+
// file commonly exports several differently-scoped versions of "the"
|
|
60
|
+
// component (an internal base, a public wrapper, a Storybook-
|
|
61
|
+
// specific unwrapped alias via `export { X as Y }`), and this module
|
|
62
|
+
// has no reliable way to know which one a given consumer will
|
|
63
|
+
// actually import. Missing the right one means Storybook shows
|
|
64
|
+
// names/types with zero descriptions (the props come from wherever
|
|
65
|
+
// *is* documented, but the rendered component itself carries no
|
|
66
|
+
// __docgenInfo at all) — worse than attaching the same (single,
|
|
67
|
+
// best-guess) props to every candidate.
|
|
68
|
+
// Known imprecision: every export gets identical props even when
|
|
69
|
+
// their real prop types differ (e.g. an internal-only export vs. its
|
|
70
|
+
// public-facing wrapper) — a real per-export fix needs core to
|
|
71
|
+
// resolve props per named export, not just once per file.
|
|
72
|
+
const injections = componentNames
|
|
73
|
+
.map((name) => {
|
|
74
|
+
const docgenInfo = { ...documentation, displayName: name };
|
|
75
|
+
return `try { ${name}.__docgenInfo = ${JSON.stringify(docgenInfo)}; } catch (e) {}`;
|
|
76
|
+
})
|
|
77
|
+
.join('\n');
|
|
78
|
+
|
|
79
|
+
return `${source}\n${injections}\n`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function findComponentNames(code) {
|
|
83
|
+
const names = new Set();
|
|
84
|
+
|
|
85
|
+
for (const m of code.matchAll(/export function ([A-Z][A-Za-z0-9]*)\s*\(/g)) {
|
|
86
|
+
names.add(m[1]);
|
|
87
|
+
}
|
|
88
|
+
for (const m of code.matchAll(/export const ([A-Z][A-Za-z0-9]*)\s*[:=]/g)) {
|
|
89
|
+
names.add(m[1]);
|
|
90
|
+
}
|
|
91
|
+
// `export { X, Z as W }` — a plain re-export list. The *local* name
|
|
92
|
+
// (before `as`, if present) is what needs __docgenInfo attached,
|
|
93
|
+
// since that's the actual declared binding in this module's scope;
|
|
94
|
+
// whatever alias another file imports it as doesn't change the
|
|
95
|
+
// object identity.
|
|
96
|
+
for (const m of code.matchAll(/export\s*\{([^}]+)\}/g)) {
|
|
97
|
+
for (const part of m[1].split(',')) {
|
|
98
|
+
const localName = part.trim().split(/\s+as\s+/)[0].trim();
|
|
99
|
+
if (/^[A-Z][A-Za-z0-9]*$/.test(localName)) names.add(localName);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return [...names];
|
|
104
|
+
}
|