Skip to content

Commit

Permalink
Create / use react-docgen vite plugin to show docsPage info (#299)
Browse files Browse the repository at this point in the history
Fixes #103
Closes #190
Fixes #2

This takes a slightly different approach from #190.  Instead of using a babel transform, this creates a vite plugin, similar to the vue-docgen plugin, but based on the babel-plugin-react-docgen.  This gives us the chance to rely directly on react-docgen, without needing another package in between, and it avoids being accidentally overwritten if users replace our @vitejs/plugin-react with their own config.

This new vite plugin is not configurable like the babel plugin is, but I'm not certain those configurations are necessary in the first place.  If we hear from users that it's something they need, we can find a way to make them customizable, but I prefer to avoid extra options if possible.

This version does not create a global list of all the docs, like the babel plugin does (`DOC_GEN_COLLECTION_NAME`).  I looked through the storybook source, and it doesn't seem like anything still relies on it, so I think it's just a legacy detail for now that we can probably avoid supporting.

I've updated the react example to demonstrate the use of the plugin, so feel free to check this out there.
  • Loading branch information
IanVS authored Apr 3, 2022
1 parent aabbba3 commit 1e9cbc5
Show file tree
Hide file tree
Showing 8 changed files with 166 additions and 2 deletions.
5 changes: 5 additions & 0 deletions examples/react/.storybook/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,9 @@ module.exports = {
// customize the Vite config here
return config;
},
// Normally this wouldn't be necessary, but `react-docgen-typescript` is the default,
// and since typescript is installed in the monorepo, we can't rely on that check.
typescript: {
reactDocgen: 'react-docgen',
},
};
9 changes: 9 additions & 0 deletions examples/react/.storybook/preview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
};
2 changes: 2 additions & 0 deletions packages/builder-vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@
"@storybook/csf-tools": "^6.3.3",
"@storybook/source-loader": "^6.3.12",
"@vitejs/plugin-react": "^1.0.8",
"ast-types": "^0.14.2",
"es-module-lexer": "^0.9.3",
"glob": "^7.2.0",
"glob-promise": "^4.2.0",
"magic-string": "^0.26.1",
"react-docgen": "^6.0.0-alpha.0",
"slash": "^3.0.0",
"vite-plugin-mdx": "^3.5.6"
},
Expand Down
46 changes: 46 additions & 0 deletions packages/builder-vite/plugins/docgen-handlers/actualNameHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* This is heavily based on the react-docgen `displayNameHandler`
* (https://github.com/reactjs/react-docgen/blob/26c90c0dd105bf83499a83826f2a6ff7a724620d/src/handlers/displayNameHandler.ts)
* but instead defines an `actualName` property on the generated docs that is taken first from the component's actual name.
* This addresses an issue where the name that the generated docs are stored under is incorrectly named with the `displayName`
* and not the component's actual name.
*
* This is inspired by `actualNameHandler` from https://github.com/storybookjs/babel-plugin-react-docgen, but is modified
* directly from displayNameHandler, using the same approach as babel-plugin-react-docgen.
*/

import { namedTypes as t } from 'ast-types';
import type { NodePath } from 'ast-types/lib/node-path';
import { getNameOrValue, isReactForwardRefCall } from 'react-docgen/dist/utils';
// import { getNameOrValue, isReactForwardRefCall } from 'react-docgen/lib/utils';
import type { Importer } from 'react-docgen/dist/parse';
// import type { Importer } from 'react-docgen/lib/parse';
import type Documentation from 'react-docgen/lib/Documentation';

export default function actualNameHandler(documentation: Documentation, path: NodePath, importer: Importer): void {
if (t.ClassDeclaration.check(path.node) || t.FunctionDeclaration.check(path.node)) {
documentation.set('actualName', getNameOrValue(path.get('id')));
} else if (
t.ArrowFunctionExpression.check(path.node) ||
t.FunctionExpression.check(path.node) ||
isReactForwardRefCall(path, importer)
) {
let currentPath = path;
while (currentPath.parent) {
if (t.VariableDeclarator.check(currentPath.parent.node)) {
documentation.set('actualName', getNameOrValue(currentPath.parent.get('id')));
return;
} else if (t.AssignmentExpression.check(currentPath.parent.node)) {
const leftPath = currentPath.parent.get('left');
if (t.Identifier.check(leftPath.node) || t.Literal.check(leftPath.node)) {
documentation.set('actualName', getNameOrValue(leftPath));
return;
}
}
currentPath = currentPath.parent;
}
// Could not find an actual name
documentation.set('actualName', '');
}
return;
}
53 changes: 53 additions & 0 deletions packages/builder-vite/plugins/react-docgen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {
parse,
handlers as docgenHandlers,
resolver as docgenResolver,
importers as docgenImporters,
} from 'react-docgen';
import type { DocumentationObject } from 'react-docgen/lib/Documentation';
import MagicString from 'magic-string';
import type { Plugin } from 'vite';
import actualNameHandler from './docgen-handlers/actualNameHandler';

type DocObj = DocumentationObject & { actualName: string };

// TODO: None of these are able to be overridden, so `default` is aspirational here.
const defaultHandlers = Object.values(docgenHandlers).map((handler) => handler);
const defaultResolver = docgenResolver.findAllExportedComponentDefinitions;
const defaultImporter = docgenImporters.makeFsImporter();
const handlers = [...defaultHandlers, actualNameHandler];

export function reactDocgen(): Plugin {
return {
name: 'react-docgen',
enforce: 'pre',
async transform(src: string, id: string) {
// JSX syntax is only allowed in .tsx and .jsx, but components can technically be created without JSX
if (/\.(mjs|tsx?|jsx?)$/.test(id)) {
try {
// Since we're using `findAllExportedComponentDefinitions`, this will always be an array.
const docgenResults = parse(src, defaultResolver, handlers, { importer: defaultImporter, filename: id }) as
| DocObj[];
const s = new MagicString(src);

docgenResults.forEach((info) => {
const { actualName, ...docgenInfo } = info;
if (actualName) {
const docNode = JSON.stringify(docgenInfo);
s.append(`;${actualName}.__docgenInfo=${docNode}`);
}
});

return {
code: s.toString(),
map: s.generateMap(),
};
} catch (e) {
// Usually this is just an error from react-docgen that it couldn't find a component
// Only uncomment for troubleshooting
// console.error(e);
}
}
},
};
}
22 changes: 22 additions & 0 deletions packages/builder-vite/types/react-docgen.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// TODO: delete this stub file once a new alpha of react-docgen is released (will include ts types).

declare module 'react-docgen' {
declare const parse;
declare const handlers;
declare const resolver;
declare const importers;
}

declare module 'react-docgen/lib/Documentation' {
export type DocumentationObject = Record<string, any>;
export default Documentation;
}

declare module 'react-docgen/dist/utils' {
declare const getNameOrValue;
declare const isReactForwardRefCall;
}

declare module 'react-docgen/dist/parse' {
declare type Importer = any;
}
4 changes: 4 additions & 0 deletions packages/builder-vite/vite-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ export async function pluginConfig(options: ExtendedOptions, _type: PluginConfig

if (reactDocgen === 'react-docgen-typescript' && typescriptPresent) {
plugins.push(require('@joshwooding/vite-plugin-react-docgen-typescript').default(reactDocgenTypescriptOptions));
} else if (reactDocgen) {
const { reactDocgen } = await import('./plugins/react-docgen');
// Needs to run before the react plugin, so add to the front
plugins.unshift(reactDocgen());
}
}

Expand Down
27 changes: 25 additions & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3505,10 +3505,12 @@ __metadata:
"@storybook/source-loader": ^6.3.12
"@types/express": ^4.17.13
"@vitejs/plugin-react": ^1.0.8
ast-types: ^0.14.2
es-module-lexer: ^0.9.3
glob: ^7.2.0
glob-promise: ^4.2.0
magic-string: ^0.26.1
react-docgen: ^6.0.0-alpha.0
slash: ^3.0.0
vite-plugin-mdx: ^3.5.6
vue-docgen-api: ^4.40.0
Expand Down Expand Up @@ -15742,6 +15744,27 @@ fsevents@^1.2.7:
languageName: node
linkType: hard

"react-docgen@npm:^6.0.0-alpha.0":
version: 6.0.0-alpha.0
resolution: "react-docgen@npm:6.0.0-alpha.0"
dependencies:
"@babel/core": ^7.7.5
"@babel/generator": ^7.12.11
"@babel/runtime": ^7.7.6
ast-types: ^0.14.2
commander: ^2.19.0
doctrine: ^3.0.0
estree-to-babel: ^3.1.0
neo-async: ^2.6.1
node-dir: ^0.1.10
resolve: ^1.17.0
strip-indent: ^3.0.0
bin:
react-docgen: bin/react-docgen.js
checksum: b1e7ad594a6191ca9e83d1d94e103db6d7e643ef69e9a5d7ca593f0f94e124bcbd48f89aaae8d4952b465781025c74fc7ee2dd7d433136e07a6f3e9529411416
languageName: node
linkType: hard

"react-dom@npm:16.14.0, react-dom@npm:^16.4.14":
version: 16.14.0
resolution: "react-dom@npm:16.14.0"
Expand Down Expand Up @@ -16313,7 +16336,7 @@ fsevents@^1.2.7:
languageName: node
linkType: hard

"resolve@npm:^1.22.0":
"resolve@npm:^1.17.0, resolve@npm:^1.22.0":
version: 1.22.0
resolution: "resolve@npm:1.22.0"
dependencies:
Expand All @@ -16336,7 +16359,7 @@ fsevents@^1.2.7:
languageName: node
linkType: hard

"resolve@patch:resolve@^1.22.0#~builtin<compat/resolve>":
"resolve@patch:resolve@^1.17.0#~builtin<compat/resolve>, resolve@patch:resolve@^1.22.0#~builtin<compat/resolve>":
version: 1.22.0
resolution: "resolve@patch:resolve@npm%3A1.22.0#~builtin<compat/resolve>::version=1.22.0&hash=07638b"
dependencies:
Expand Down

0 comments on commit 1e9cbc5

Please sign in to comment.