Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add multi export support #33

Merged
merged 3 commits into from
Jan 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

[Zeplin CLI](https://github.com/zeplin/cli) plugin to generate descriptions and code snippets for React components.

Zeplin CLI React Plugin uses [react-docgen](https://github.com/reactjs/react-docgen) and [react-docgen-typescript](https://github.com/styleguidist/react-docgen-typescript) to analyze and collect information from React components. For more details about the supported formats, see `react-docgen` [guidelines](https://github.com/reactjs/react-docgen#guidelines-for-default-resolvers-and-handlers) and `react-docgen-typescript` [examples](https://github.com/styleguidist/react-docgen-typescript#example).

## Installation

Install the plugin using npm.
Expand All @@ -18,7 +20,7 @@ Run CLI `connect` command using the plugin.
zeplin connect -p @zeplin/cli-connect-react-plugin
```

Zeplin CLI React Plugin uses [react-docgen](https://github.com/reactjs/react-docgen) and [react-docgen-typescript](https://github.com/styleguidist/react-docgen-typescript) to analyze and collect information from React components. For more details about the supported formats, see `react-docgen` [guidelines](https://github.com/reactjs/react-docgen#guidelines-for-default-resolvers-and-handlers) and `react-docgen-typescript` [examples](https://github.com/styleguidist/react-docgen-typescript#example).
### Using react-docgen-typescript for Typescript components

You can choose to use either `react-docgen` or `react-docgen-typescript` for TypeScript in your plugin configurations.

Expand All @@ -28,8 +30,26 @@ You can choose to use either `react-docgen` or `react-docgen-typescript` for Typ
"plugins" : [{
"name": "@zeplin/cli-connect-react-plugin",
"config": {
"tsDocgen": "react-docgen-typescript", // Uses react-docgen by default
"tsConfigPath": "/path/to/tsconfig.json" // Defaults to ./tsconfig.json
"tsDocgen": "react-docgen-typescript", // Default: "react-docgen"
"tsConfigPath": "/path/to/tsconfig.json" // Default: "./tsconfig.json"
}
}],
...
}
```

### Using react-docgen resolvers

You can set which built-in `react-docgen` resolver to use.

```jsonc
{
...
"plugins" : [{
"name": "@zeplin/cli-connect-react-plugin",
"config": {
// Default: "findAllExportedComponentDefinitions"
"reactDocgenResolver": "findExportedComponentDefinition",
}
}],
...
Expand All @@ -40,4 +60,6 @@ You can choose to use either `react-docgen` or `react-docgen-typescript` for Typ

[Connected Components](https://blog.zeplin.io/introducing-connected-components-components-in-design-and-code-in-harmony-aa894ed5bd95) in Zeplin lets you access components in your codebase directly on designs in Zeplin, with links to Storybook, GitHub and any other source of documentation based on your workflow. 🧩

Check [Zeplin Connected Components Documentation](https://zpl.io/connected-components-docs) for getting started.

[Zeplin CLI](https://github.com/zeplin/cli) uses plugins like this one to analyze component source code and publishes a high-level overview to be displayed in Zeplin.
104 changes: 63 additions & 41 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,19 @@ import {
import updateNotifier from "update-notifier";
import { name as packageName, version as packageVersion } from "../package.json";

let reactTsDocgen: {
withCustomConfig: typeof withCustomConfig;
withDefaultConfig: typeof withDefaultConfig;
};

interface ReactPluginConfig {
tsDocgen: "react-docgen" | "react-docgen-typescript";
tsConfigPath: string;
reactDocgenResolver?: string;
}

const defaultReactDocgenResolver = "findAllExportedComponentDefinitions";
const availableReactDocgenResolvers = [
"findAllExportedComponentDefinitions",
"findExportedComponentDefinition",
"findAllComponentDefinitions"
];

updateNotifier({
pkg: {
name: packageName,
Expand All @@ -42,21 +45,34 @@ export default class implements ConnectPlugin {
tsDocgen: "react-docgen",
tsConfigPath: "./tsconfig.json"
};
reactTsDocgen: {
withCustomConfig: typeof withCustomConfig;
withDefaultConfig: typeof withDefaultConfig;
} | null = null;
resolver = require(`react-docgen/dist/resolver/${defaultReactDocgenResolver}`).default

template = pug.compileFile(path.join(__dirname, "template/snippet.pug"));

// eslint-disable-next-line require-await
async init(pluginContext: PluginContext): Promise<void> {
Object.assign(this.config, pluginContext.config);
this.logger = pluginContext.logger;
if (this.config.reactDocgenResolver) {
const { reactDocgenResolver } = this.config;
if (availableReactDocgenResolvers.includes(reactDocgenResolver) &&
reactDocgenResolver !== "findAllExportedComponentDefinitions") {
this.logger.debug(`Setting react-docgen resolver to ${reactDocgenResolver}`);
this.resolver = require(`react-docgen/dist/resolver/${reactDocgenResolver}`).default;
}
}
}

async process(context: ComponentConfig): Promise<ComponentData> {
const filePath = path.resolve(context.path);

const file = await readFile(filePath);

let rawReactDocs: TSComponentDoc | ComponentDoc;
let rawReactDocs: TSComponentDoc[] | ComponentDoc[];
let propsFilter: (props: TSProps | Props, name: string) => boolean;

if (this.config.tsDocgen === "react-docgen-typescript" && this.tsExtensions.includes(path.extname(filePath))) {
Expand All @@ -67,35 +83,41 @@ export default class implements ConnectPlugin {
({ rawReactDocs, propsFilter } = this.parseUsingReactDocgen(file, filePath));
}

const rawProps = rawReactDocs.props || {};

const props = Object.keys(rawProps)
.filter(name => name !== "children")
.filter(name => propsFilter(rawProps, name))
.map(name => {
const prop = rawProps[name];
if (prop.type) {
// Required to remove \" from typescript literal types
prop.type.name = prop.type.name.replace(/"/g, "'");
if ("raw" in prop.type && prop.type.raw) {
prop.type.raw = prop.type.raw.replace(/"/g, "'");
}
}

return { name, value: prop };
const snippets: string[] = (rawReactDocs as Array<TSComponentDoc | ComponentDoc>)
.map(rrd => {
const rawProps = rrd.props || {};

const props = Object.keys(rawProps)
.filter(name => name !== "children")
.filter(name => propsFilter(rawProps, name))
.map(name => {
const prop = rawProps[name];
if (prop.type) {
// Required to remove \" from typescript literal types
prop.type.name = prop.type.name.replace(/"/g, "'");
if ("raw" in prop.type && prop.type.raw) {
prop.type.raw = prop.type.raw.replace(/"/g, "'");
}
}

return { name, value: prop };
});

const hasChildren = !!rawProps.children;

const snippet = this.generateSnippet({
description: rrd.description,
componentName: rrd.displayName,
props,
hasChildren
});

return snippet;
});

const hasChildren = !!rawProps.children;
const snippet = snippets.join("\n\n");

const snippet = this.generateSnippet({
description: rawReactDocs.description,
componentName: rawReactDocs.displayName,
props,
hasChildren
});

// TODO maybe generate a markdown propTable as description?
const { description } = rawReactDocs;
const [{ description }] = rawReactDocs;
const lang = this.tsExtensions.includes(path.extname(context.path))
? PrismLang.ReactTSX
: PrismLang.ReactJSX;
Expand All @@ -114,10 +136,10 @@ export default class implements ConnectPlugin {
}

private parseUsingReactDocgen(file: Buffer, filePath: string): {
rawReactDocs: ComponentDoc;
rawReactDocs: ComponentDoc[];
propsFilter: (props: TSProps | Props, name: string) => boolean;
} {
const rawReactDocs = parse(file, null, null, {
const rawReactDocs = parse(file, this.resolver, null, {
filename: filePath,
babelrc: false
});
Expand All @@ -126,13 +148,13 @@ export default class implements ConnectPlugin {
!!(props[name].type || props[name].tsType || props[name].flowType);

return {
rawReactDocs,
rawReactDocs: Array.isArray(rawReactDocs) ? rawReactDocs : [rawReactDocs],
propsFilter
};
}

private async parseUsingReactDocgenTypescript(filePath: string): Promise<{
rawReactDocs: TSComponentDoc;
rawReactDocs: TSComponentDoc[];
propsFilter: (props: TSProps | Props, name: string) => boolean;
}> {
const tsConfigPath = path.resolve(this.config.tsConfigPath);
Expand All @@ -147,17 +169,17 @@ export default class implements ConnectPlugin {

let parser;

if (!reactTsDocgen) {
if (!this.reactTsDocgen) {
this.logger?.debug("Importing react-docgen-typescript package");
reactTsDocgen = await import("react-docgen-typescript");
this.reactTsDocgen = await import("react-docgen-typescript");
}

if (await pathExists(tsConfigPath)) {
parser = reactTsDocgen.withCustomConfig(tsConfigPath, parserOpts);
parser = this.reactTsDocgen.withCustomConfig(tsConfigPath, parserOpts);
} else {
parser = reactTsDocgen.withDefaultConfig(parserOpts);
parser = this.reactTsDocgen.withDefaultConfig(parserOpts);
}
const [rawReactDocs] = parser.parse(filePath);
const rawReactDocs = parser.parse(filePath);

const propsFilter = (props: TSProps | Props, name: string): boolean => !!props[name].type;

Expand Down
4 changes: 2 additions & 2 deletions src/types/react-docgen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ declare module "react-docgen" {

// Just minimal type definition of the method to use it
export function parse(src: string | Buffer,
resolver?: null,
resolver?: string | unknown,
handlers?: null,
options?: { filename: string; babelrc: boolean }): ComponentDoc;
options?: { filename: string; babelrc: boolean }): ComponentDoc | ComponentDoc[];
}
19 changes: 19 additions & 0 deletions test/__snapshots__/flow.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,22 @@ Object {
obj={{ subvalue: boolean }} />",
}
`;

exports[`Connected Components React Plugin - Flow MultiExportFlowComponentWithProps.jsx snippet creation 1`] = `
Object {
"description": "Component 1 description. Only this one should be shown",
"lang": "jsx",
"snippet": "<MyComponent
primitive={number}
literalsAndUnion={'string' | 'otherstring' | number}
arr={Array<any>}
func={(value: string) => void}
noParameterName={string => void}
obj={{ subvalue: boolean }} />

<MyOtherComponent
primitive={number}
literalsAndUnion={'string' | 'otherstring' | number}
arr={Array<any>} />",
}
`;
17 changes: 16 additions & 1 deletion test/__snapshots__/functional.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Connected Components React Plugin - Functional ComponentWithChildren.jsx snippet creation 1`] = `
exports[`Connected Components React Plugin - Functional FunctionalComponentWithChildren.jsx snippet creation 1`] = `
Object {
"description": "",
"lang": "jsx",
Expand All @@ -17,3 +17,18 @@ Object {
"snippet": "<MyComponent />",
}
`;

exports[`Connected Components React Plugin - Functional MultiExportFunctionalComponentWithChildren.jsx snippet creation 1`] = `
Object {
"description": "Component 1 description. Only this one should be shown",
"lang": "jsx",
"snippet": "<MyComponent1>
{children}
</MyComponent1>

<MyComponent2
someParameter={PropTypes.string.isRequired}>
{children}
</MyComponent2>",
}
`;
71 changes: 71 additions & 0 deletions test/__snapshots__/propType.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,38 @@ Object {
}
`;

exports[`Connected Components React Plugin - PropTypes ComponentWithChildrenAndProps.jsx snippet creation with single export resolver 1`] = `
Object {
"description": "Component description.",
"lang": "jsx",
"snippet": "<MyComponent
optionalArray={array}
optionalBool={bool}
optionalFunc={func}
optionalNumber={number}
optionalObject={object}
optionalString={string}
optionalSymbol={symbol}
optionalNode={node}
optionalElement={element}
optionalElementType={elementType}
optionalFoo={instanceOf(Foo)}
optionalEnum={enum}
optionalUnion={union[string|number|instanceOf(Foo)]}
optionalArrayOf={arrayOf[number]}
optionalObjectOf={objectOf[number]}
optionalObjectWithShape={shape}
optionalObjectWithStrictShape={exact}
requiredFunc={func}
requiredAny={any}
customProp={() => {}}
customArrayProp={arrayOf[custom]}
customObjectOfProp={objectOf[custom]}>
{children}
</MyComponent>",
}
`;

exports[`Connected Components React Plugin - PropTypes ComponentWithMemoization.jsx snippet creation 1`] = `
Object {
"description": "Component description.",
Expand Down Expand Up @@ -111,3 +143,42 @@ Object {
customObjectOfProp={objectOf[custom]} />",
}
`;

exports[`Connected Components React Plugin - PropTypes MultiExportComponentWithProps.jsx snippet creation 1`] = `
Object {
"description": "Component 1 description. Only this one should be shown",
"lang": "jsx",
"snippet": "<MyComponent1
optionalArray={array}
optionalBool={bool}
optionalFunc={func}
optionalNumber={number}
optionalObject={object}
optionalString={string}
optionalSymbol={symbol}
optionalNode={node}
optionalElement={element}
optionalElementType={elementType}
optionalFoo={instanceOf(Foo)}
optionalEnum={enum}
optionalUnion={union[string|number|instanceOf(Foo)]}
optionalArrayOf={arrayOf[number]}
optionalObjectOf={objectOf[number]}
optionalObjectWithShape={shape}
optionalObjectWithStrictShape={exact}
requiredFunc={func}
requiredAny={any}
customProp={() => {}}
customArrayProp={arrayOf[custom]}
customObjectOfProp={objectOf[custom]} />

<MyComponent2
optionalArray={array}
optionalBool={bool}
optionalFunc={func}
optionalNumber={number}
optionalObject={object}
optionalString={string}
optionalSymbol={symbol} />",
}
`;
Loading