├── .circleci └── config.yml ├── .eslintignore ├── .eslintrc.js ├── .github └── pull_request_template.md ├── .gitignore ├── LICENSE ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── index.ts ├── template │ ├── base.pug │ └── snippet.pug └── types │ └── react-docgen.ts ├── test ├── __snapshots__ │ ├── flow.test.ts.snap │ ├── functional.test.ts.snap │ ├── propType.test.ts.snap │ └── typescript.test.ts.snap ├── flow.test.ts ├── functional.test.ts ├── helper │ └── logger.ts ├── propType.test.ts ├── samples │ ├── flow │ │ ├── FlowComponent.jsx │ │ ├── FlowComponentWithChildren.jsx │ │ ├── FlowComponentWithChildrenAndProps.jsx │ │ ├── FlowComponentWithProps.jsx │ │ └── MultiExportFlowComponentWithProps.jsx │ ├── jsx-class │ │ ├── Component.jsx │ │ ├── ComponentWithChildren.jsx │ │ ├── ComponentWithChildrenAndProps.jsx │ │ ├── ComponentWithMemoization.jsx │ │ ├── ComponentWithProps.jsx │ │ └── MultiExportComponentWithProps.jsx │ ├── jsx-functional │ │ ├── FunctionalComponent.jsx │ │ ├── FunctionalComponentWithChildren.jsx │ │ └── MultiExportFunctionalComponentWithChildren.jsx │ └── typescript │ │ ├── MultiExportTSComponentWithProps.tsx │ │ ├── TSComponent.tsx │ │ ├── TSComponentWithChildren.tsx │ │ ├── TSComponentWithChildrenAndProps.tsx │ │ ├── TSComponentWithImport.tsx │ │ ├── TSComponentWithProps.tsx │ │ └── TSOtherComponent.tsx ├── tsconfig.test.json └── typescript.test.ts └── tsconfig.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | defaults: &defaults 4 | working_directory: ~/repo 5 | docker: 6 | - image: circleci/node:15 7 | 8 | jobs: 9 | build_and_test: 10 | <<: *defaults 11 | steps: 12 | - checkout 13 | # Restore node_modules if it is cached and has the exact same package-lock.json 14 | - restore_cache: 15 | key: dependency-cache-{{ checksum "package-lock.json" }} 16 | paths: 17 | - ./node_modules 18 | - run: 19 | name: Install npm packages 20 | command: npm install 21 | - run: 22 | name: Run linter 23 | command: npm run lint 24 | # Save node_modules to the cache for current package-lock.json 25 | - save_cache: 26 | key: dependency-cache-{{ checksum "package-lock.json" }} 27 | paths: 28 | - ./node_modules 29 | - run: 30 | name: Run tests 31 | command: npm test 32 | - persist_to_workspace: 33 | root: ~/repo 34 | paths: . 35 | publish: 36 | <<: *defaults 37 | steps: 38 | - attach_workspace: 39 | at: ~/repo 40 | - run: 41 | name: Publish to NPM 42 | command: | 43 | echo "@zeplin:registry=https://registry.npmjs.org/" >> ~/.npmrc 44 | echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc 45 | npm publish --access public 46 | workflows: 47 | version: 2 48 | npm_publish: 49 | jobs: 50 | - build_and_test: 51 | # Run build_and_test on all branches and tags 52 | filters: 53 | tags: 54 | only: /.*/ 55 | - publish: 56 | requires: 57 | - build_and_test 58 | filters: 59 | # Ignore all branches 60 | branches: 61 | ignore: /.*/ 62 | # Run publish only on version tags 63 | tags: 64 | only: /v[0-9]+(\.[0-9]+)*/ 65 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | test/samples -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | jest: true 5 | }, 6 | extends: [ 7 | "@zeplin/eslint-config/node", 8 | "plugin:import/errors", 9 | "plugin:import/warnings", 10 | "plugin:import/typescript", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | parser: "@typescript-eslint/parser", 14 | parserOptions: { 15 | ecmaVersion: 2018, 16 | sourceType: "module" 17 | }, 18 | plugins: ["@typescript-eslint"], 19 | settings: { 20 | "import/resolver": { 21 | typescript: { 22 | directory: "./tsconfig.json" 23 | } 24 | } 25 | }, 26 | rules: { 27 | "capitalized-comments": "error", 28 | "arrow-body-style": ["error", "as-needed"], 29 | "no-process-exit": "off", 30 | "no-process-env": "off", 31 | "@typescript-eslint/no-explicit-any": ["error", { ignoreRestArgs: true }], 32 | "class-methods-use-this": "off" 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Change description 2 | 3 | > Description here 4 | 5 | ## Type of change 6 | - [ ] Bug fix (fixes an issue) 7 | - [ ] New feature (adds functionality) 8 | 9 | ## Related issues 10 | 11 | > Fix [#1]() 12 | 13 | ## Checklists 14 | 15 | ### Development 16 | 17 | - [ ] Lint rules pass locally 18 | - [ ] Application changes have been tested thoroughly 19 | - [ ] Automated tests covering modified code pass 20 | 21 | ### Security 22 | 23 | - [ ] Security impact of change has been considered 24 | - [ ] Code follows company security practices and guidelines 25 | 26 | ### Code review 27 | 28 | - [ ] Pull request has a descriptive title and context useful to a reviewer. Screenshots or screencasts are attached as necessary 29 | - [ ] "Ready for review" label attached and reviewers assigned 30 | - [ ] Changes have been reviewed by at least one other contributor 31 | - [ ] Pull request linked to task tracker where applicable 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .vscode 3 | .idea 4 | *.iml 5 | 6 | # Node 7 | node_modules 8 | logs/ 9 | node_modules 10 | npm-debug.log 11 | .env 12 | 13 | # Typescript 14 | dist -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Zeplin, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zeplin CLI React Plugin 2 | 3 | [Zeplin CLI](https://github.com/zeplin/cli) plugin to generate descriptions and code snippets for React components. 4 | 5 | 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). 6 | 7 | ## Installation 8 | 9 | Install the plugin using npm. 10 | 11 | ```sh 12 | npm install -g @zeplin/cli-connect-react-plugin 13 | ``` 14 | 15 | ## Usage 16 | 17 | Run CLI `connect` command using the plugin. 18 | 19 | ```sh 20 | zeplin connect -p @zeplin/cli-connect-react-plugin 21 | ``` 22 | 23 | ### Using react-docgen-typescript for Typescript components 24 | 25 | You can choose to use either `react-docgen` or `react-docgen-typescript` for TypeScript in your plugin configurations. 26 | 27 | ```jsonc 28 | { 29 | ... 30 | "plugins" : [{ 31 | "name": "@zeplin/cli-connect-react-plugin", 32 | "config": { 33 | "tsDocgen": "react-docgen-typescript", // Default: "react-docgen" 34 | "tsConfigPath": "/path/to/tsconfig.json" // Default: "./tsconfig.json" 35 | } 36 | }], 37 | ... 38 | } 39 | ``` 40 | 41 | ### Using react-docgen resolvers 42 | 43 | You can set which built-in `react-docgen` resolver to use. 44 | 45 | ```jsonc 46 | { 47 | ... 48 | "plugins" : [{ 49 | "name": "@zeplin/cli-connect-react-plugin", 50 | "config": { 51 | // Default: "findAllExportedComponentDefinitions" 52 | "reactDocgenResolver": "findExportedComponentDefinition", 53 | } 54 | }], 55 | ... 56 | } 57 | ``` 58 | 59 | ## About Connected Components 60 | 61 | [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. 🧩 62 | 63 | Check [Zeplin Connected Components Documentation](https://zpl.io/connected-components-docs) for getting started. 64 | 65 | [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. 66 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: [ 3 | "/src", 4 | "/test" 5 | ], 6 | transform: { 7 | "^.+\\.tsx?$": "ts-jest" 8 | } 9 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@zeplin/cli-connect-react-plugin", 3 | "version": "1.1.2", 4 | "description": "Zeplin CLI Connected Components - React Plugin", 5 | "main": "./dist/src/index", 6 | "scripts": { 7 | "test": "jest", 8 | "copy-templates": "copyfiles -u 1 src/template/** dist/src", 9 | "build:dev": "npm run build -- --sourceMap", 10 | "build": "rimraf dist/ && npm run copy-templates && tsc", 11 | "lint": "eslint --ext .js,.ts .", 12 | "prepare": "npm run build" 13 | }, 14 | "files": [ 15 | "dist" 16 | ], 17 | "husky": { 18 | "hooks": { 19 | "pre-commit": "npm run lint" 20 | } 21 | }, 22 | "license": "MIT", 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/zeplin/cli-connect-react-plugin.git" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/zeplin/cli-connect-react-plugin/issues" 29 | }, 30 | "homepage": "https://github.com/zeplin/cli-connect-react-plugin#readme", 31 | "devDependencies": { 32 | "@types/fs-extra": "^8.1.0", 33 | "@types/jest": "^25.2.1", 34 | "@types/node": "^13.13.5", 35 | "@types/pug": "^2.0.4", 36 | "@types/react": "^17.0.0", 37 | "@types/update-notifier": "^5.0.0", 38 | "@typescript-eslint/eslint-plugin": "^3.10.1", 39 | "@typescript-eslint/parser": "^3.10.1", 40 | "@zeplin/cli": "^1.1.2", 41 | "@zeplin/eslint-config": "^2.2.0", 42 | "copyfiles": "^2.2.0", 43 | "eslint": "^7.0.0", 44 | "eslint-import-resolver-typescript": "^2.0.0", 45 | "husky": "^4.2.5", 46 | "jest": "^26.1.0", 47 | "prop-types": "^15.7.2", 48 | "rimraf": "^3.0.2", 49 | "ts-jest": "^26.1.3", 50 | "typescript": "^4.3.0" 51 | }, 52 | "dependencies": { 53 | "fs-extra": "^9.0.1", 54 | "pug": "^2.0.4", 55 | "react-docgen": "^5.3.1", 56 | "react-docgen-typescript": "^2.2.2", 57 | "update-notifier": "^5.0.1" 58 | }, 59 | "peerDependencies": { 60 | "typescript": ">= 4.x" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ConnectPlugin, ComponentConfig, ComponentData, PrismLang, PluginContext, Logger 3 | } from "@zeplin/cli"; 4 | import path from "path"; 5 | import pug from "pug"; 6 | import { readFile, pathExists } from "fs-extra"; 7 | import { ComponentDoc, parse, PreparedComponentDoc, Props } from "react-docgen"; 8 | import { 9 | withCustomConfig, 10 | withDefaultConfig, 11 | ParserOptions as TSParserOptions, 12 | Props as TSProps, 13 | ComponentDoc as TSComponentDoc 14 | } from "react-docgen-typescript"; 15 | import updateNotifier from "update-notifier"; 16 | import { name as packageName, version as packageVersion } from "../package.json"; 17 | 18 | interface ReactPluginConfig { 19 | tsDocgen: "react-docgen" | "react-docgen-typescript"; 20 | tsConfigPath: string; 21 | reactDocgenResolver?: string; 22 | } 23 | 24 | const defaultReactDocgenResolver = "findAllExportedComponentDefinitions"; 25 | const availableReactDocgenResolvers = [ 26 | "findAllExportedComponentDefinitions", 27 | "findExportedComponentDefinition", 28 | "findAllComponentDefinitions" 29 | ]; 30 | 31 | updateNotifier({ 32 | pkg: { 33 | name: packageName, 34 | version: packageVersion 35 | }, 36 | updateCheckInterval: 0, 37 | shouldNotifyInNpmScript: true 38 | }).notify(); 39 | 40 | export default class implements ConnectPlugin { 41 | supportedFileExtensions = [".js", ".jsx", ".ts", ".tsx"]; 42 | tsExtensions = [".ts", ".tsx"]; 43 | logger?: Logger; 44 | config: ReactPluginConfig = { 45 | tsDocgen: "react-docgen", 46 | tsConfigPath: "./tsconfig.json" 47 | }; 48 | reactTsDocgen: { 49 | withCustomConfig: typeof withCustomConfig; 50 | withDefaultConfig: typeof withDefaultConfig; 51 | } | null = null; 52 | // eslint-disable-next-line @typescript-eslint/no-var-requires 53 | resolver = require(`react-docgen/dist/resolver/${defaultReactDocgenResolver}`).default 54 | 55 | template = pug.compileFile(path.join(__dirname, "template/snippet.pug")); 56 | 57 | // eslint-disable-next-line require-await 58 | async init(pluginContext: PluginContext): Promise { 59 | Object.assign(this.config, pluginContext.config); 60 | this.logger = pluginContext.logger; 61 | if (this.config.reactDocgenResolver) { 62 | const { reactDocgenResolver } = this.config; 63 | if (availableReactDocgenResolvers.includes(reactDocgenResolver) && 64 | reactDocgenResolver !== "findAllExportedComponentDefinitions") { 65 | this.logger.debug(`Setting react-docgen resolver to ${reactDocgenResolver}`); 66 | // eslint-disable-next-line @typescript-eslint/no-var-requires 67 | this.resolver = require(`react-docgen/dist/resolver/${reactDocgenResolver}`).default; 68 | } 69 | } 70 | } 71 | 72 | async process(context: ComponentConfig): Promise { 73 | const filePath = path.resolve(context.path); 74 | 75 | const file = await readFile(filePath); 76 | 77 | let rawReactDocs: TSComponentDoc[] | ComponentDoc[]; 78 | let propsFilter: (props: TSProps | Props, name: string) => boolean; 79 | 80 | if (this.config.tsDocgen === "react-docgen-typescript" && this.tsExtensions.includes(path.extname(filePath))) { 81 | this.logger?.debug(`Using react-docgen-typescript for ${filePath}`); 82 | ({ rawReactDocs, propsFilter } = await this.parseUsingReactDocgenTypescript(filePath)); 83 | } else { 84 | this.logger?.debug(`Using react-docgen for ${filePath}`); 85 | ({ rawReactDocs, propsFilter } = this.parseUsingReactDocgen(file, filePath)); 86 | } 87 | 88 | const snippets: string[] = (rawReactDocs as Array) 89 | .map(rrd => { 90 | const rawProps = rrd.props || {}; 91 | 92 | const props = Object.keys(rawProps) 93 | .filter(name => name !== "children") 94 | .filter(name => propsFilter(rawProps, name)) 95 | .map(name => { 96 | const prop = rawProps[name]; 97 | if (prop.type) { 98 | // Required to remove \" from typescript literal types 99 | prop.type.name = prop.type.name.replace(/"/g, "'"); 100 | if ("raw" in prop.type && prop.type.raw) { 101 | prop.type.raw = prop.type.raw.replace(/"/g, "'"); 102 | } 103 | } 104 | 105 | return { name, value: prop }; 106 | }); 107 | 108 | const hasChildren = !!rawProps.children; 109 | 110 | const snippet = this.generateSnippet({ 111 | description: rrd.description, 112 | componentName: rrd.displayName, 113 | props, 114 | hasChildren 115 | }); 116 | 117 | return snippet; 118 | }); 119 | 120 | const snippet = snippets.join("\n\n"); 121 | 122 | const [{ description }] = rawReactDocs; 123 | const lang = this.tsExtensions.includes(path.extname(context.path)) 124 | ? PrismLang.ReactTSX 125 | : PrismLang.ReactJSX; 126 | 127 | return { description, snippet, lang }; 128 | } 129 | 130 | supports(x: ComponentConfig): boolean { 131 | const fileExtension = path.extname(x.path); 132 | 133 | return this.supportedFileExtensions.includes(fileExtension); 134 | } 135 | 136 | private generateSnippet(preparedComponentDoc: PreparedComponentDoc): string { 137 | return this.template(preparedComponentDoc); 138 | } 139 | 140 | private parseUsingReactDocgen(file: Buffer, filePath: string): { 141 | rawReactDocs: ComponentDoc[]; 142 | propsFilter: (props: TSProps | Props, name: string) => boolean; 143 | } { 144 | const rawReactDocs = parse(file, this.resolver, null, { 145 | filename: filePath, 146 | babelrc: false 147 | }); 148 | 149 | const propsFilter = (props: Props, name: string): boolean => 150 | !!(props[name].type || props[name].tsType || props[name].flowType); 151 | 152 | return { 153 | rawReactDocs: Array.isArray(rawReactDocs) ? rawReactDocs : [rawReactDocs], 154 | propsFilter 155 | }; 156 | } 157 | 158 | private async parseUsingReactDocgenTypescript(filePath: string): Promise<{ 159 | rawReactDocs: TSComponentDoc[]; 160 | propsFilter: (props: TSProps | Props, name: string) => boolean; 161 | }> { 162 | const tsConfigPath = path.resolve(this.config.tsConfigPath); 163 | 164 | const parserOpts: TSParserOptions = { 165 | shouldExtractLiteralValuesFromEnum: true, 166 | shouldRemoveUndefinedFromOptional: true, 167 | propFilter: { 168 | skipPropsWithoutDoc: false 169 | } 170 | }; 171 | 172 | let parser; 173 | 174 | if (!this.reactTsDocgen) { 175 | this.logger?.debug("Importing react-docgen-typescript package"); 176 | this.reactTsDocgen = await import("react-docgen-typescript"); 177 | } 178 | 179 | if (await pathExists(tsConfigPath)) { 180 | parser = this.reactTsDocgen.withCustomConfig(tsConfigPath, parserOpts); 181 | } else { 182 | parser = this.reactTsDocgen.withDefaultConfig(parserOpts); 183 | } 184 | const rawReactDocs = parser.parse(filePath); 185 | 186 | const propsFilter = (props: TSProps | Props, name: string): boolean => !!props[name].type; 187 | 188 | return { 189 | rawReactDocs, 190 | propsFilter 191 | }; 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/template/base.pug: -------------------------------------------------------------------------------- 1 | mixin propertyValue(type) 2 | if type.name === "arrayOf" || type.name === "objectOf" 3 | | !{type.name}[!{type.value.name}] 4 | else if type.name === "instanceOf" 5 | | instanceOf(!{type.value}) 6 | else if type.raw 7 | | !{type.raw} 8 | else 9 | | !{type.name} 10 | 11 | mixin propertyValues(values) 12 | - var list = values.length ? values : [values] 13 | each value, index in list 14 | +propertyValue(value) 15 | | !{(index < list.length - 1) ? '|' : ''} 16 | 17 | mixin propType(name, propType) 18 | if propType.name === "union" 19 | | !{name}={union[ 20 | +propertyValues(propType.value) 21 | | ]} 22 | else 23 | | !{name}={ 24 | +propertyValue(propType) 25 | | } 26 | 27 | mixin tsOrFlowType(name, type) 28 | | !{name}={ 29 | +propertyValue(type) 30 | | } 31 | 32 | mixin property(prop) 33 | if prop.value.flowType 34 | +tsOrFlowType(prop.name, prop.value.flowType) 35 | else if prop.value.tsType 36 | +tsOrFlowType(prop.name, prop.value.tsType) 37 | else 38 | +propType(prop.name, prop.value.type) 39 | 40 | block snippet -------------------------------------------------------------------------------- /src/template/snippet.pug: -------------------------------------------------------------------------------- 1 | extends base 2 | 3 | block snippet 4 | | 0 6 | | 7 | | 8 | each prop, index in props 9 | +property(prop) 10 | if index !== (props.length -1) 11 | | 12 | | 13 | if hasChildren 14 | | > 15 | | {children} 16 | | 17 | else 18 | | /> 19 | -------------------------------------------------------------------------------- /src/types/react-docgen.ts: -------------------------------------------------------------------------------- 1 | declare module "react-docgen" { 2 | export interface Props { 3 | [key: string]: PropDescriptor; 4 | } 5 | 6 | export interface ComponentDoc { 7 | displayName: string; 8 | description: string; 9 | props?: Props; 10 | } 11 | 12 | export interface PreparedProp { 13 | name: string; 14 | value: Omit; 15 | } 16 | 17 | export type PreparedComponentDoc = { 18 | componentName: string; 19 | description: string; 20 | props: PreparedProp[]; 21 | hasChildren: boolean; 22 | } 23 | 24 | export type PropTypeDescriptor = { 25 | name: 26 | | "arrayOf" 27 | | "custom" 28 | | "enum" 29 | | "array" 30 | | "bool" 31 | | "func" 32 | | "number" 33 | | "object" 34 | | "string" 35 | | "any" 36 | | "element" 37 | | "node" 38 | | "symbol" 39 | | "objectOf" 40 | | "shape" 41 | | "exact" 42 | | "union" 43 | | "elementType"; 44 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 45 | value?: any; 46 | raw?: string; 47 | computed?: boolean; 48 | description?: string; 49 | required?: boolean; 50 | }; 51 | 52 | export type FlowBaseType = { 53 | required?: boolean; 54 | nullable?: boolean; 55 | alias?: string; 56 | }; 57 | 58 | export type FlowSimpleType = FlowBaseType & { 59 | name: string; 60 | raw?: string; 61 | }; 62 | 63 | export type FlowLiteralType = FlowBaseType & { 64 | name: "literal"; 65 | value: string; 66 | }; 67 | 68 | export type FlowElementsType = FlowBaseType & { 69 | name: string; 70 | raw: string; 71 | elements: Array; 72 | }; 73 | 74 | export type FlowFunctionArgumentType = { 75 | name: string; 76 | type?: FlowTypeDescriptor; 77 | rest?: boolean; 78 | }; 79 | 80 | export type FlowFunctionSignatureType = FlowBaseType & { 81 | name: "signature"; 82 | type: "function"; 83 | raw: string; 84 | signature: { 85 | arguments: Array; 86 | return: FlowTypeDescriptor; 87 | }; 88 | }; 89 | 90 | export type FlowObjectSignatureType = FlowBaseType & { 91 | name: "signature"; 92 | type: "object"; 93 | raw: string; 94 | signature: { 95 | properties: Array<{ 96 | key: string | FlowTypeDescriptor; 97 | value: FlowTypeDescriptor; 98 | }>; 99 | }; 100 | }; 101 | 102 | export type FlowTypeDescriptor = 103 | | FlowSimpleType 104 | | FlowLiteralType 105 | | FlowElementsType 106 | | FlowFunctionSignatureType 107 | | FlowObjectSignatureType; 108 | 109 | export type PropDescriptor = { 110 | type?: PropTypeDescriptor | FlowTypeDescriptor; 111 | flowType?: FlowTypeDescriptor; 112 | tsType?: FlowTypeDescriptor; 113 | required?: boolean; 114 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 115 | defaultValue?: any; 116 | description?: string; 117 | }; 118 | 119 | // Just minimal type definition of the method to use it 120 | export function parse(src: string | Buffer, 121 | resolver?: string | unknown, 122 | handlers?: null, 123 | options?: { filename: string; babelrc: boolean }): ComponentDoc | ComponentDoc[]; 124 | } 125 | -------------------------------------------------------------------------------- /test/__snapshots__/flow.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Connected Components React Plugin - Flow FlowComponent.jsx snippet creation 1`] = ` 4 | Object { 5 | "description": "General component description.", 6 | "lang": "jsx", 7 | "snippet": "", 8 | } 9 | `; 10 | 11 | exports[`Connected Components React Plugin - Flow FlowComponentWithChildren.jsx snippet creation 1`] = ` 12 | Object { 13 | "description": "General component description.", 14 | "lang": "jsx", 15 | "snippet": " 16 | {children} 17 | ", 18 | } 19 | `; 20 | 21 | exports[`Connected Components React Plugin - Flow FlowComponentWithChildrenAndProps.jsx snippet creation 1`] = ` 22 | Object { 23 | "description": "General component description.", 24 | "lang": "jsx", 25 | "snippet": "} 29 | func={(value: string) => void} 30 | noParameterName={string => void} 31 | obj={{ subvalue: boolean }}> 32 | {children} 33 | ", 34 | } 35 | `; 36 | 37 | exports[`Connected Components React Plugin - Flow FlowComponentWithProps.jsx snippet creation 1`] = ` 38 | Object { 39 | "description": "General component description.", 40 | "lang": "jsx", 41 | "snippet": "} 45 | func={(value: string) => void} 46 | noParameterName={string => void} 47 | obj={{ subvalue: boolean }} />", 48 | } 49 | `; 50 | 51 | exports[`Connected Components React Plugin - Flow MultiExportFlowComponentWithProps.jsx snippet creation 1`] = ` 52 | Object { 53 | "description": "Component 1 description. Only this one should be shown", 54 | "lang": "jsx", 55 | "snippet": "} 59 | func={(value: string) => void} 60 | noParameterName={string => void} 61 | obj={{ subvalue: boolean }} /> 62 | 63 | } />", 67 | } 68 | `; 69 | -------------------------------------------------------------------------------- /test/__snapshots__/functional.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Connected Components React Plugin - Functional FunctionalComponentWithChildren.jsx snippet creation 1`] = ` 4 | Object { 5 | "description": "", 6 | "lang": "jsx", 7 | "snippet": " 8 | {children} 9 | ", 10 | } 11 | `; 12 | 13 | exports[`Connected Components React Plugin - Functional FunctionalComponents.jsx snippet creation 1`] = ` 14 | Object { 15 | "description": "", 16 | "lang": "jsx", 17 | "snippet": "", 18 | } 19 | `; 20 | 21 | exports[`Connected Components React Plugin - Functional MultiExportFunctionalComponentWithChildren.jsx snippet creation 1`] = ` 22 | Object { 23 | "description": "Component 1 description. Only this one should be shown", 24 | "lang": "jsx", 25 | "snippet": " 26 | {children} 27 | 28 | 29 | 31 | {children} 32 | ", 33 | } 34 | `; 35 | -------------------------------------------------------------------------------- /test/__snapshots__/propType.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Connected Components React Plugin - PropTypes Component.jsx snippet creation 1`] = ` 4 | Object { 5 | "description": "Component description.", 6 | "lang": "jsx", 7 | "snippet": "", 8 | } 9 | `; 10 | 11 | exports[`Connected Components React Plugin - PropTypes ComponentWithChildren.jsx snippet creation 1`] = ` 12 | Object { 13 | "description": "Component description.", 14 | "lang": "jsx", 15 | "snippet": " 16 | {children} 17 | ", 18 | } 19 | `; 20 | 21 | exports[`Connected Components React Plugin - PropTypes ComponentWithChildrenAndProps.jsx snippet creation 1`] = ` 22 | Object { 23 | "description": "Component description.", 24 | "lang": "jsx", 25 | "snippet": " {}} 46 | customArrayProp={arrayOf[custom]} 47 | customObjectOfProp={objectOf[custom]}> 48 | {children} 49 | ", 50 | } 51 | `; 52 | 53 | exports[`Connected Components React Plugin - PropTypes ComponentWithChildrenAndProps.jsx snippet creation with single export resolver 1`] = ` 54 | Object { 55 | "description": "Component description.", 56 | "lang": "jsx", 57 | "snippet": " {}} 78 | customArrayProp={arrayOf[custom]} 79 | customObjectOfProp={objectOf[custom]}> 80 | {children} 81 | ", 82 | } 83 | `; 84 | 85 | exports[`Connected Components React Plugin - PropTypes ComponentWithMemoization.jsx snippet creation 1`] = ` 86 | Object { 87 | "description": "Component description.", 88 | "lang": "jsx", 89 | "snippet": " {}} 110 | customArrayProp={arrayOf[custom]} 111 | customObjectOfProp={objectOf[custom]}> 112 | {children} 113 | ", 114 | } 115 | `; 116 | 117 | exports[`Connected Components React Plugin - PropTypes ComponentWithProps.jsx snippet creation 1`] = ` 118 | Object { 119 | "description": "Component description.", 120 | "lang": "jsx", 121 | "snippet": " {}} 142 | customArrayProp={arrayOf[custom]} 143 | customObjectOfProp={objectOf[custom]} />", 144 | } 145 | `; 146 | 147 | exports[`Connected Components React Plugin - PropTypes MultiExportComponentWithProps.jsx snippet creation 1`] = ` 148 | Object { 149 | "description": "Component 1 description. Only this one should be shown", 150 | "lang": "jsx", 151 | "snippet": " {}} 172 | customArrayProp={arrayOf[custom]} 173 | customObjectOfProp={objectOf[custom]} /> 174 | 175 | ", 183 | } 184 | `; 185 | -------------------------------------------------------------------------------- /test/__snapshots__/typescript.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Connected Components React Plugin - TypeScript Using react-docgen TSComponent.tsx snippet creation 1`] = ` 4 | Object { 5 | "description": "General component description.", 6 | "lang": "tsx", 7 | "snippet": "", 8 | } 9 | `; 10 | 11 | exports[`Connected Components React Plugin - TypeScript Using react-docgen TSComponentWithChildren.tsx snippet creation 1`] = ` 12 | Object { 13 | "description": "General component description.", 14 | "lang": "tsx", 15 | "snippet": " 16 | {children} 17 | ", 18 | } 19 | `; 20 | 21 | exports[`Connected Components React Plugin - TypeScript Using react-docgen TSComponentWithChildrenAndProps.tsx snippet creation 1`] = ` 22 | Object { 23 | "description": "General component description.", 24 | "lang": "tsx", 25 | "snippet": ") => void} 43 | onChange={(id: number) => void} 44 | optional={OptionalType}> 45 | {children} 46 | ", 47 | } 48 | `; 49 | 50 | exports[`Connected Components React Plugin - TypeScript Using react-docgen TSComponentWithImport.tsx snippet creation 1`] = ` 51 | Object { 52 | "description": "General component description.", 53 | "lang": "tsx", 54 | "snippet": " 56 | {children} 57 | ", 58 | } 59 | `; 60 | 61 | exports[`Connected Components React Plugin - TypeScript Using react-docgen TSComponentWithProps.tsx snippet creation 1`] = ` 62 | Object { 63 | "description": "General component description.", 64 | "lang": "tsx", 65 | "snippet": ") => void} 83 | onChange={(id: number) => void} 84 | optional={OptionalType} />", 85 | } 86 | `; 87 | 88 | exports[`Connected Components React Plugin - TypeScript Using react-docgen-typescript MultiExportTSComponentWithProps.tsx snippet creation 1`] = ` 89 | Object { 90 | "description": "General component description.", 91 | "lang": "tsx", 92 | "snippet": ") => void} 104 | onChange={(id: number) => void} 105 | optional={OptionalType} /> 106 | 107 | ", 115 | } 116 | `; 117 | 118 | exports[`Connected Components React Plugin - TypeScript Using react-docgen-typescript TSComponent.tsx snippet creation 1`] = ` 119 | Object { 120 | "description": "General component description.", 121 | "lang": "tsx", 122 | "snippet": "", 123 | } 124 | `; 125 | 126 | exports[`Connected Components React Plugin - TypeScript Using react-docgen-typescript TSComponentWithChildren.tsx snippet creation 1`] = ` 127 | Object { 128 | "description": "General component description.", 129 | "lang": "tsx", 130 | "snippet": "", 131 | } 132 | `; 133 | 134 | exports[`Connected Components React Plugin - TypeScript Using react-docgen-typescript TSComponentWithChildrenAndProps.tsx snippet creation 1`] = ` 135 | Object { 136 | "description": "General component description.", 137 | "lang": "tsx", 138 | "snippet": ") => void} 150 | hele={(event: MouseEvent) => void} 151 | onChange={(id: number) => void} 152 | optional={OptionalType} />", 153 | } 154 | `; 155 | 156 | exports[`Connected Components React Plugin - TypeScript Using react-docgen-typescript TSComponentWithImport.tsx snippet creation 1`] = ` 157 | Object { 158 | "description": "General component description.", 159 | "lang": "tsx", 160 | "snippet": "", 163 | } 164 | `; 165 | 166 | exports[`Connected Components React Plugin - TypeScript Using react-docgen-typescript TSComponentWithProps.tsx snippet creation 1`] = ` 167 | Object { 168 | "description": "General component description.", 169 | "lang": "tsx", 170 | "snippet": ") => void} 182 | onChange={(id: number) => void} 183 | optional={OptionalType} />", 184 | } 185 | `; 186 | -------------------------------------------------------------------------------- /test/flow.test.ts: -------------------------------------------------------------------------------- 1 | import Plugin from "../src"; 2 | 3 | describe("Connected Components React Plugin - Flow", () => { 4 | test("FlowComponent.jsx snippet creation", async () => { 5 | const processor = new Plugin(); 6 | 7 | const componentCode = await processor.process( 8 | { 9 | path: "test/samples/flow/FlowComponent.jsx", 10 | zeplinNames: [] 11 | } 12 | ); 13 | 14 | expect(componentCode).toMatchSnapshot(); 15 | }); 16 | 17 | test("FlowComponentWithProps.jsx snippet creation", async () => { 18 | const processor = new Plugin(); 19 | 20 | const componentCode = await processor.process( 21 | { 22 | path: "test/samples/flow/FlowComponentWithProps.jsx", 23 | zeplinNames: [] 24 | } 25 | ); 26 | 27 | expect(componentCode).toMatchSnapshot(); 28 | }); 29 | 30 | test("FlowComponentWithChildren.jsx snippet creation", async () => { 31 | const processor = new Plugin(); 32 | 33 | const componentCode = await processor.process( 34 | { 35 | path: "test/samples/flow/FlowComponentWithChildren.jsx", 36 | zeplinNames: [] 37 | } 38 | ); 39 | 40 | expect(componentCode).toMatchSnapshot(); 41 | }); 42 | 43 | test("FlowComponentWithChildrenAndProps.jsx snippet creation", async () => { 44 | const processor = new Plugin(); 45 | 46 | const componentCode = await processor.process( 47 | { 48 | path: "test/samples/flow/FlowComponentWithChildrenAndProps.jsx", 49 | zeplinNames: [] 50 | } 51 | ); 52 | 53 | expect(componentCode).toMatchSnapshot(); 54 | }); 55 | 56 | test("MultiExportFlowComponentWithProps.jsx snippet creation", async () => { 57 | const processor = new Plugin(); 58 | 59 | const componentCode = await processor.process( 60 | { 61 | path: "test/samples/flow/MultiExportFlowComponentWithProps.jsx", 62 | zeplinNames: [] 63 | } 64 | ); 65 | 66 | expect(componentCode).toMatchSnapshot(); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/functional.test.ts: -------------------------------------------------------------------------------- 1 | import Plugin from "../src"; 2 | 3 | describe("Connected Components React Plugin - Functional", () => { 4 | test("FunctionalComponents.jsx snippet creation", async () => { 5 | const plugin = new Plugin(); 6 | 7 | const componentCode = await plugin.process( 8 | { 9 | path: "test/samples/jsx-functional/FunctionalComponent.jsx", 10 | zeplinNames: [] 11 | } 12 | ); 13 | 14 | expect(componentCode).toMatchSnapshot(); 15 | }); 16 | 17 | test("FunctionalComponentWithChildren.jsx snippet creation", async () => { 18 | const processor = new Plugin(); 19 | 20 | const componentCode = await processor.process( 21 | { 22 | path: "test/samples/jsx-functional/FunctionalComponentWithChildren.jsx", 23 | zeplinNames: [] 24 | } 25 | ); 26 | 27 | expect(componentCode).toMatchSnapshot(); 28 | }); 29 | 30 | test("MultiExportFunctionalComponentWithChildren.jsx snippet creation", async () => { 31 | const processor = new Plugin(); 32 | 33 | const componentCode = await processor.process( 34 | { 35 | path: "test/samples/jsx-functional/MultiExportFunctionalComponentWithChildren.jsx", 36 | zeplinNames: [] 37 | } 38 | ); 39 | 40 | expect(componentCode).toMatchSnapshot(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /test/helper/logger.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from "@zeplin/cli"; 2 | 3 | export const logger: Logger = { 4 | error: jest.fn(), 5 | warn: jest.fn(), 6 | info: jest.fn(), 7 | debug: jest.fn() 8 | }; -------------------------------------------------------------------------------- /test/propType.test.ts: -------------------------------------------------------------------------------- 1 | import Plugin from "../src"; 2 | import { logger } from "./helper/logger"; 3 | 4 | describe("Connected Components React Plugin - PropTypes", () => { 5 | test("Component.jsx snippet creation", async () => { 6 | const plugin = new Plugin(); 7 | 8 | const componentCode = await plugin.process( 9 | { 10 | path: "test/samples/jsx-class/Component.jsx", 11 | zeplinNames: [] 12 | } 13 | ); 14 | 15 | expect(componentCode).toMatchSnapshot(); 16 | }); 17 | 18 | test("ComponentWithChildren.jsx snippet creation", async () => { 19 | const processor = new Plugin(); 20 | 21 | const componentCode = await processor.process( 22 | { 23 | path: "test/samples/jsx-class/ComponentWithChildren.jsx", 24 | zeplinNames: [] 25 | } 26 | ); 27 | 28 | expect(componentCode).toMatchSnapshot(); 29 | }); 30 | 31 | test("ComponentWithProps.jsx snippet creation", async () => { 32 | const processor = new Plugin(); 33 | 34 | const componentCode = await processor.process( 35 | { 36 | path: "test/samples/jsx-class/ComponentWithProps.jsx", 37 | zeplinNames: [] 38 | } 39 | ); 40 | 41 | expect(componentCode).toMatchSnapshot(); 42 | }); 43 | 44 | test("ComponentWithChildrenAndProps.jsx snippet creation", async () => { 45 | const processor = new Plugin(); 46 | 47 | const componentCode = await processor.process( 48 | { 49 | path: "test/samples/jsx-class/ComponentWithChildrenAndProps.jsx", 50 | zeplinNames: [] 51 | } 52 | ); 53 | 54 | expect(componentCode).toMatchSnapshot(); 55 | }); 56 | 57 | test("ComponentWithMemoization.jsx snippet creation", async () => { 58 | const processor = new Plugin(); 59 | 60 | const componentCode = await processor.process( 61 | { 62 | path: "test/samples/jsx-class/ComponentWithMemoization.jsx", 63 | zeplinNames: [] 64 | } 65 | ); 66 | 67 | expect(componentCode).toMatchSnapshot(); 68 | }); 69 | 70 | test("MultiExportComponentWithProps.jsx snippet creation", async () => { 71 | const processor = new Plugin(); 72 | 73 | const componentCode = await processor.process( 74 | { 75 | path: "test/samples/jsx-class/MultiExportComponentWithProps.jsx", 76 | zeplinNames: [] 77 | } 78 | ); 79 | 80 | expect(componentCode).toMatchSnapshot(); 81 | }); 82 | 83 | test("ComponentWithChildrenAndProps.jsx snippet creation with single export resolver", async () => { 84 | const processor = new Plugin(); 85 | await processor.init({ 86 | components: [], 87 | logger, 88 | config: { 89 | reactDocgenResolver: "findExportedComponentDefinition" 90 | } 91 | }); 92 | 93 | const componentCode = await processor.process( 94 | { 95 | path: "test/samples/jsx-class/ComponentWithChildrenAndProps.jsx", 96 | zeplinNames: [] 97 | } 98 | ); 99 | 100 | expect(componentCode).toMatchSnapshot(); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /test/samples/flow/FlowComponent.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | 4 | /** 5 | * General component description. 6 | */ 7 | export default class MyComponent extends React.Component { 8 | 9 | render(): ReactElement { 10 | // ... 11 | } 12 | } -------------------------------------------------------------------------------- /test/samples/flow/FlowComponentWithChildren.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type Props = { 4 | children?: React.Node 5 | }; 6 | 7 | /** 8 | * General component description. 9 | */ 10 | export default class MyComponent extends React.Component { 11 | props: Props; 12 | 13 | render(): ReactElement { 14 | // ... 15 | } 16 | } -------------------------------------------------------------------------------- /test/samples/flow/FlowComponentWithChildrenAndProps.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type Props = { 4 | children?: React.Node, 5 | primitive: number, 6 | literalsAndUnion: 'string' | 'otherstring' | number, 7 | arr: Array, 8 | func?: (value: string) => void, 9 | noParameterName?: string => void, 10 | obj?: { subvalue: boolean } 11 | }; 12 | 13 | /** 14 | * General component description. 15 | */ 16 | export default class MyComponent extends React.Component { 17 | props: Props; 18 | 19 | render(): ReactElement { 20 | // ... 21 | } 22 | } -------------------------------------------------------------------------------- /test/samples/flow/FlowComponentWithProps.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type Props = { 4 | primitive: number, 5 | literalsAndUnion: 'string' | 'otherstring' | number, 6 | arr: Array, 7 | func?: (value: string) => void, 8 | noParameterName?: string => void, 9 | obj?: { subvalue: boolean } 10 | }; 11 | 12 | /** 13 | * General component description. 14 | */ 15 | export default class MyComponent extends React.Component { 16 | props: Props; 17 | 18 | render(): ReactElement { 19 | // ... 20 | } 21 | } -------------------------------------------------------------------------------- /test/samples/flow/MultiExportFlowComponentWithProps.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type Props1 = { 4 | primitive: number, 5 | literalsAndUnion: 'string' | 'otherstring' | number, 6 | arr: Array, 7 | func?: (value: string) => void, 8 | noParameterName?: string => void, 9 | obj?: { subvalue: boolean } 10 | }; 11 | 12 | type Props2 = { 13 | primitive: number, 14 | literalsAndUnion: 'string' | 'otherstring' | number, 15 | arr: Array 16 | }; 17 | 18 | /** Component 1 description. Only this one should be shown */ 19 | export class MyComponent extends React.Component { 20 | props: Props1; 21 | 22 | render(): ReactElement { 23 | // ... 24 | } 25 | } 26 | 27 | /** Component 2 description. This one should not be shown */ 28 | export class MyOtherComponent extends React.Component { 29 | props: Props2; 30 | 31 | render(): ReactElement { 32 | // ... 33 | } 34 | } -------------------------------------------------------------------------------- /test/samples/jsx-class/Component.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | 3 | class Foo {} 4 | 5 | /** Component description. */ 6 | class MyComponent extends React.Component { 7 | render() { 8 | return ""; 9 | } 10 | } 11 | 12 | export default MyComponent; -------------------------------------------------------------------------------- /test/samples/jsx-class/ComponentWithChildren.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | 3 | class Foo {} 4 | 5 | /** Component description. */ 6 | class MyComponent extends React.Component { 7 | render() { 8 | return ""; 9 | } 10 | } 11 | 12 | MyComponent.propTypes = { 13 | children: PropTypes.element.isRequired 14 | }; 15 | 16 | export default MyComponent; -------------------------------------------------------------------------------- /test/samples/jsx-class/ComponentWithChildrenAndProps.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | 3 | class Foo {} 4 | 5 | /** Component description. */ 6 | class MyComponent extends React.Component { 7 | render() { 8 | return ""; 9 | } 10 | } 11 | // All examples of propTypes listed official documentation 12 | MyComponent.propTypes = { 13 | children: PropTypes.element.isRequired, 14 | optionalArray: PropTypes.array, 15 | optionalBool: PropTypes.bool, 16 | optionalFunc: PropTypes.func, 17 | optionalNumber: PropTypes.number, 18 | optionalObject: PropTypes.object, 19 | optionalString: PropTypes.string, 20 | optionalSymbol: PropTypes.symbol, 21 | optionalNode: PropTypes.node, 22 | optionalElement: PropTypes.element, 23 | optionalElementType: PropTypes.elementType, 24 | optionalFoo: PropTypes.instanceOf(Foo), 25 | optionalEnum: PropTypes.oneOf(["News", "Photos"]), 26 | optionalUnion: PropTypes.oneOfType([ 27 | PropTypes.string, 28 | PropTypes.number, 29 | PropTypes.instanceOf(Foo) 30 | ]), 31 | optionalArrayOf: PropTypes.arrayOf(PropTypes.number), 32 | optionalObjectOf: PropTypes.objectOf(PropTypes.number), 33 | optionalObjectWithShape: PropTypes.shape({ 34 | color: PropTypes.string, 35 | fontSize: PropTypes.number 36 | }), 37 | optionalObjectWithStrictShape: PropTypes.exact({ 38 | name: PropTypes.string, 39 | quantity: PropTypes.number 40 | }), 41 | requiredFunc: PropTypes.func.isRequired, 42 | requiredAny: PropTypes.any.isRequired, 43 | customProp: () => {}, 44 | customArrayProp: PropTypes.arrayOf(() => {}), 45 | customObjectOfProp: PropTypes.objectOf(() => {}) 46 | }; 47 | 48 | export default MyComponent; -------------------------------------------------------------------------------- /test/samples/jsx-class/ComponentWithMemoization.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | 3 | class Foo {} 4 | 5 | /** Component description. */ 6 | class MyComponent extends React.Component { 7 | render() { 8 | return ""; 9 | } 10 | } 11 | // All examples of propTypes listed official documentation 12 | MyComponent.propTypes = { 13 | children: PropTypes.element.isRequired, 14 | optionalArray: PropTypes.array, 15 | optionalBool: PropTypes.bool, 16 | optionalFunc: PropTypes.func, 17 | optionalNumber: PropTypes.number, 18 | optionalObject: PropTypes.object, 19 | optionalString: PropTypes.string, 20 | optionalSymbol: PropTypes.symbol, 21 | optionalNode: PropTypes.node, 22 | optionalElement: PropTypes.element, 23 | optionalElementType: PropTypes.elementType, 24 | optionalFoo: PropTypes.instanceOf(Foo), 25 | optionalEnum: PropTypes.oneOf(["News", "Photos"]), 26 | optionalUnion: PropTypes.oneOfType([ 27 | PropTypes.string, 28 | PropTypes.number, 29 | PropTypes.instanceOf(Foo) 30 | ]), 31 | optionalArrayOf: PropTypes.arrayOf(PropTypes.number), 32 | optionalObjectOf: PropTypes.objectOf(PropTypes.number), 33 | optionalObjectWithShape: PropTypes.shape({ 34 | color: PropTypes.string, 35 | fontSize: PropTypes.number 36 | }), 37 | optionalObjectWithStrictShape: PropTypes.exact({ 38 | name: PropTypes.string, 39 | quantity: PropTypes.number 40 | }), 41 | requiredFunc: PropTypes.func.isRequired, 42 | requiredAny: PropTypes.any.isRequired, 43 | customProp: () => {}, 44 | customArrayProp: PropTypes.arrayOf(() => {}), 45 | customObjectOfProp: PropTypes.objectOf(() => {}) 46 | }; 47 | 48 | function someKindOfComparator(prevProps, nextProps) { 49 | } 50 | 51 | export default React.memo(MyComponent, someKindOfComparator); -------------------------------------------------------------------------------- /test/samples/jsx-class/ComponentWithProps.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | 3 | class Foo {} 4 | 5 | /** Component description. */ 6 | class MyComponent extends React.Component { 7 | render() { 8 | return ""; 9 | } 10 | } 11 | // All examples of propTypes listed official documentation 12 | MyComponent.propTypes = { 13 | optionalArray: PropTypes.array, 14 | optionalBool: PropTypes.bool, 15 | optionalFunc: PropTypes.func, 16 | optionalNumber: PropTypes.number, 17 | optionalObject: PropTypes.object, 18 | optionalString: PropTypes.string, 19 | optionalSymbol: PropTypes.symbol, 20 | optionalNode: PropTypes.node, 21 | optionalElement: PropTypes.element, 22 | optionalElementType: PropTypes.elementType, 23 | optionalFoo: PropTypes.instanceOf(Foo), 24 | optionalEnum: PropTypes.oneOf(["News", "Photos"]), 25 | optionalUnion: PropTypes.oneOfType([ 26 | PropTypes.string, 27 | PropTypes.number, 28 | PropTypes.instanceOf(Foo) 29 | ]), 30 | optionalArrayOf: PropTypes.arrayOf(PropTypes.number), 31 | optionalObjectOf: PropTypes.objectOf(PropTypes.number), 32 | optionalObjectWithShape: PropTypes.shape({ 33 | color: PropTypes.string, 34 | fontSize: PropTypes.number 35 | }), 36 | optionalObjectWithStrictShape: PropTypes.exact({ 37 | name: PropTypes.string, 38 | quantity: PropTypes.number 39 | }), 40 | requiredFunc: PropTypes.func.isRequired, 41 | requiredAny: PropTypes.any.isRequired, 42 | customProp: () => {}, 43 | customArrayProp: PropTypes.arrayOf(() => {}), 44 | customObjectOfProp: PropTypes.objectOf(() => {}) 45 | }; 46 | 47 | export default MyComponent; -------------------------------------------------------------------------------- /test/samples/jsx-class/MultiExportComponentWithProps.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | 3 | class Foo {} 4 | 5 | /** Component 1 description. Only this one should be shown */ 6 | class MyComponent1 extends React.Component { 7 | render() { 8 | return ""; 9 | } 10 | } 11 | 12 | /** Component 2 description. This one should not be shown */ 13 | class MyComponent2 extends React.Component { 14 | render() { 15 | return ""; 16 | } 17 | } 18 | 19 | MyComponent1.propTypes = { 20 | optionalArray: PropTypes.array, 21 | optionalBool: PropTypes.bool, 22 | optionalFunc: PropTypes.func, 23 | optionalNumber: PropTypes.number, 24 | optionalObject: PropTypes.object, 25 | optionalString: PropTypes.string, 26 | optionalSymbol: PropTypes.symbol, 27 | optionalNode: PropTypes.node, 28 | optionalElement: PropTypes.element, 29 | optionalElementType: PropTypes.elementType, 30 | optionalFoo: PropTypes.instanceOf(Foo), 31 | optionalEnum: PropTypes.oneOf(["News", "Photos"]), 32 | optionalUnion: PropTypes.oneOfType([ 33 | PropTypes.string, 34 | PropTypes.number, 35 | PropTypes.instanceOf(Foo) 36 | ]), 37 | optionalArrayOf: PropTypes.arrayOf(PropTypes.number), 38 | optionalObjectOf: PropTypes.objectOf(PropTypes.number), 39 | optionalObjectWithShape: PropTypes.shape({ 40 | color: PropTypes.string, 41 | fontSize: PropTypes.number 42 | }), 43 | optionalObjectWithStrictShape: PropTypes.exact({ 44 | name: PropTypes.string, 45 | quantity: PropTypes.number 46 | }), 47 | requiredFunc: PropTypes.func.isRequired, 48 | requiredAny: PropTypes.any.isRequired, 49 | customProp: () => {}, 50 | customArrayProp: PropTypes.arrayOf(() => {}), 51 | customObjectOfProp: PropTypes.objectOf(() => {}) 52 | }; 53 | 54 | MyComponent2.propTypes = { 55 | optionalArray: PropTypes.array, 56 | optionalBool: PropTypes.bool, 57 | optionalFunc: PropTypes.func, 58 | optionalNumber: PropTypes.number, 59 | optionalObject: PropTypes.object, 60 | optionalString: PropTypes.string, 61 | optionalSymbol: PropTypes.symbol 62 | } 63 | 64 | export const MyComponent = MyComponent1; 65 | 66 | export const MyOtherComponent = MyComponent2; -------------------------------------------------------------------------------- /test/samples/jsx-functional/FunctionalComponent.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const MyComponent = () => { 4 | const thing = { 5 | stuff: 'stuff', 6 | }; 7 | 8 | const string = thing?.stuff; 9 | 10 | return ( 11 |
12 | {string} 13 |
14 | ); 15 | }; 16 | 17 | export default MyComponent; 18 | -------------------------------------------------------------------------------- /test/samples/jsx-functional/FunctionalComponentWithChildren.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const MyComponent = () => { 4 | const thing = { 5 | stuff: 'stuff', 6 | }; 7 | 8 | const string = thing?.stuff; 9 | 10 | return ( 11 |
12 | {string} 13 |
14 | ); 15 | }; 16 | 17 | MyComponent.propTypes = { 18 | children: PropTypes.element.isRequired 19 | }; 20 | 21 | export default MyComponent; 22 | -------------------------------------------------------------------------------- /test/samples/jsx-functional/MultiExportFunctionalComponentWithChildren.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /** Component 1 description. Only this one should be shown */ 4 | const MyComponent1 = () => { 5 | const thing = { 6 | stuff: 'stuff', 7 | }; 8 | 9 | const string = thing?.stuff; 10 | 11 | return ( 12 |
13 | {string} 14 |
15 | ); 16 | }; 17 | 18 | MyComponent1.propTypes = { 19 | children: PropTypes.element.isRequired 20 | }; 21 | 22 | /** Component 2 description. This one should not be shown */ 23 | const MyComponent2 = () => { 24 | const thing = { 25 | stuff: 'stuff', 26 | }; 27 | 28 | const string = thing?.stuff; 29 | 30 | return ( 31 |
32 | {string} 33 |
34 | ); 35 | }; 36 | 37 | MyComponent2.propTypes = { 38 | children: PropTypes.element.isRequired, 39 | someParameter: PropTypes.string.isRequired 40 | }; 41 | 42 | export const MyComponent = MyComponent1; 43 | export const MyOtherComponent = MyComponent2; 44 | -------------------------------------------------------------------------------- /test/samples/typescript/MultiExportTSComponentWithProps.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | 3 | interface OptionalType {}; 4 | 5 | type Props1 = { 6 | message: string; 7 | count: number; 8 | disabled: boolean; 9 | names: string[]; 10 | status: "waiting" | "success"; 11 | obj: object; 12 | obj2: {}; 13 | obj3: { 14 | id: string; 15 | title: string; 16 | }; 17 | objArr: { 18 | id: string; 19 | title: string; 20 | }[]; 21 | onSomething: Function; 22 | onClick: (event: React.MouseEvent) => void; 23 | onChange: (id: number) => void; 24 | optional?: OptionalType; 25 | }; 26 | 27 | type Props2 = { 28 | message: string; 29 | count: number; 30 | disabled: boolean; 31 | names: string[]; 32 | status: "waiting" | "success"; 33 | obj: object; 34 | obj2: {}; 35 | }; 36 | 37 | /** 38 | * General component description. 39 | */ 40 | export class MyComponent extends React.Component { 41 | props: Props1; 42 | 43 | render(): ReactNode { 44 | // ... 45 | return; 46 | } 47 | } 48 | 49 | /** 50 | * General component description. 51 | */ 52 | export class MyOtherComponent extends React.Component { 53 | props: Props2; 54 | 55 | render(): ReactNode { 56 | // ... 57 | return; 58 | } 59 | } -------------------------------------------------------------------------------- /test/samples/typescript/TSComponent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /** 4 | * General component description. 5 | */ 6 | export default class MyComponent extends React.Component<{}, {}> { 7 | 8 | render() { 9 | // ... 10 | return; 11 | } 12 | } -------------------------------------------------------------------------------- /test/samples/typescript/TSComponentWithChildren.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type Props = { 4 | children?: React.ReactNode; 5 | }; 6 | 7 | /** 8 | * General component description. 9 | */ 10 | export default class MyComponent extends React.Component { 11 | props: Props; 12 | 13 | render() { 14 | // ... 15 | return; 16 | } 17 | } -------------------------------------------------------------------------------- /test/samples/typescript/TSComponentWithChildrenAndProps.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface OptionalType {}; 4 | 5 | type Props = { 6 | children?: React.ReactNode; 7 | message: string; 8 | count: number; 9 | disabled: boolean; 10 | names: string[]; 11 | status: "waiting" | "success"; 12 | obj: object; 13 | obj2: {}; 14 | obj3: { 15 | id: string; 16 | title: string; 17 | }; 18 | objArr: { 19 | id: string; 20 | title: string; 21 | }[]; 22 | onSomething: Function; 23 | onClick: (event: React.MouseEvent) => void; 24 | hele(event: React.MouseEvent): void; 25 | onChange: (id: number) => void; 26 | optional?: OptionalType; 27 | }; 28 | 29 | /** 30 | * General component description. 31 | */ 32 | export default class MyComponent extends React.Component { 33 | props: Props; 34 | 35 | render() { 36 | // ... 37 | return; 38 | } 39 | } -------------------------------------------------------------------------------- /test/samples/typescript/TSComponentWithImport.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import OtherComponent, { OtherProps } from './TSOtherComponent'; 3 | 4 | type Props = OtherProps & { 5 | children?: React.ReactNode; 6 | message: string; 7 | }; 8 | 9 | /** 10 | * General component description. 11 | */ 12 | export default class MyComponent extends OtherComponent { 13 | props: Props; 14 | 15 | render() { 16 | // ... 17 | return; 18 | } 19 | } -------------------------------------------------------------------------------- /test/samples/typescript/TSComponentWithProps.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | interface OptionalType {}; 4 | 5 | type Props = { 6 | message: string; 7 | count: number; 8 | disabled: boolean; 9 | names: string[]; 10 | status: "waiting" | "success"; 11 | obj: object; 12 | obj2: {}; 13 | obj3: { 14 | id: string; 15 | title: string; 16 | }; 17 | objArr: { 18 | id: string; 19 | title: string; 20 | }[]; 21 | onSomething: Function; 22 | onClick: (event: React.MouseEvent) => void; 23 | onChange: (id: number) => void; 24 | optional?: OptionalType; 25 | }; 26 | 27 | /** 28 | * General component description. 29 | */ 30 | export default class MyComponent extends React.Component { 31 | props: Props; 32 | 33 | render() { 34 | // ... 35 | return; 36 | } 37 | } -------------------------------------------------------------------------------- /test/samples/typescript/TSOtherComponent.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export type OtherProps = { 4 | other: string; 5 | }; 6 | 7 | /** 8 | * General component description. 9 | */ 10 | export default class MyComponent extends React.Component { 11 | props: OtherProps; 12 | 13 | render() { 14 | // ... 15 | return; 16 | } 17 | } -------------------------------------------------------------------------------- /test/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "lib": ["es5", "es6", "dom"], 6 | "noImplicitAny": false, 7 | "esModuleInterop": true, 8 | "resolveJsonModule": true, 9 | "sourceMap": true, 10 | "outDir": "lib", 11 | "jsx": "react" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/typescript.test.ts: -------------------------------------------------------------------------------- 1 | import Plugin from "../src"; 2 | import { logger } from "./helper/logger"; 3 | 4 | const pluginContext = { 5 | components: [], 6 | logger, 7 | config: { 8 | tsDocgen: "react-docgen-typescript", 9 | tsConfigPath: "./test/tsconfig.test.json" 10 | } 11 | }; 12 | 13 | describe("Connected Components React Plugin - TypeScript", () => { 14 | describe("Using react-docgen", () => { 15 | test("TSComponent.tsx snippet creation", async () => { 16 | const processor = new Plugin(); 17 | 18 | const componentCode = await processor.process( 19 | { 20 | path: "test/samples/typescript/TSComponent.tsx", 21 | zeplinNames: [] 22 | } 23 | ); 24 | 25 | expect(componentCode).toMatchSnapshot(); 26 | }); 27 | 28 | test("TSComponentWithProps.tsx snippet creation", async () => { 29 | const processor = new Plugin(); 30 | 31 | const componentCode = await processor.process( 32 | { 33 | path: "test/samples/typescript/TSComponentWithProps.tsx", 34 | zeplinNames: [] 35 | } 36 | ); 37 | 38 | expect(componentCode).toMatchSnapshot(); 39 | }); 40 | 41 | test("TSComponentWithChildren.tsx snippet creation", async () => { 42 | const processor = new Plugin(); 43 | 44 | const componentCode = await processor.process( 45 | { 46 | path: "test/samples/typescript/TSComponentWithChildren.tsx", 47 | zeplinNames: [] 48 | } 49 | ); 50 | 51 | expect(componentCode).toMatchSnapshot(); 52 | }); 53 | 54 | test("TSComponentWithChildrenAndProps.tsx snippet creation", async () => { 55 | const processor = new Plugin(); 56 | 57 | const componentCode = await processor.process( 58 | { 59 | path: "test/samples/typescript/TSComponentWithChildrenAndProps.tsx", 60 | zeplinNames: [] 61 | } 62 | ); 63 | 64 | expect(componentCode).toMatchSnapshot(); 65 | }); 66 | 67 | test("TSComponentWithImport.tsx snippet creation", async () => { 68 | const processor = new Plugin(); 69 | 70 | const componentCode = await processor.process( 71 | { 72 | path: "test/samples/typescript/TSComponentWithImport.tsx", 73 | zeplinNames: [] 74 | } 75 | ); 76 | 77 | expect(componentCode).toMatchSnapshot(); 78 | }); 79 | }); 80 | 81 | describe("Using react-docgen-typescript", () => { 82 | test("TSComponent.tsx snippet creation", async () => { 83 | const processor = new Plugin(); 84 | 85 | await processor.init(pluginContext); 86 | 87 | processor.config = { 88 | tsDocgen: "react-docgen-typescript", 89 | tsConfigPath: "./test/tsconfig.test.json" 90 | }; 91 | 92 | const componentCode = await processor.process( 93 | { 94 | path: "test/samples/typescript/TSComponent.tsx", 95 | zeplinNames: [] 96 | } 97 | ); 98 | 99 | expect(componentCode).toMatchSnapshot(); 100 | }); 101 | 102 | test("TSComponentWithProps.tsx snippet creation", async () => { 103 | const processor = new Plugin(); 104 | 105 | await processor.init(pluginContext); 106 | 107 | const componentCode = await processor.process( 108 | { 109 | path: "test/samples/typescript/TSComponentWithProps.tsx", 110 | zeplinNames: [] 111 | } 112 | ); 113 | 114 | expect(componentCode).toMatchSnapshot(); 115 | }); 116 | 117 | test("TSComponentWithChildren.tsx snippet creation", async () => { 118 | const processor = new Plugin(); 119 | 120 | await processor.init(pluginContext); 121 | 122 | const componentCode = await processor.process( 123 | { 124 | path: "test/samples/typescript/TSComponentWithChildren.tsx", 125 | zeplinNames: [] 126 | } 127 | ); 128 | 129 | expect(componentCode).toMatchSnapshot(); 130 | }); 131 | 132 | test("TSComponentWithChildrenAndProps.tsx snippet creation", async () => { 133 | const processor = new Plugin(); 134 | 135 | processor.config = { 136 | tsDocgen: "react-docgen-typescript", 137 | tsConfigPath: "./test/tsconfig.test.json" 138 | }; 139 | 140 | const componentCode = await processor.process( 141 | { 142 | path: "test/samples/typescript/TSComponentWithChildrenAndProps.tsx", 143 | zeplinNames: [] 144 | } 145 | ); 146 | 147 | expect(componentCode).toMatchSnapshot(); 148 | }); 149 | 150 | test("TSComponentWithImport.tsx snippet creation", async () => { 151 | const processor = new Plugin(); 152 | 153 | await processor.init(pluginContext); 154 | 155 | const componentCode = await processor.process( 156 | { 157 | path: "test/samples/typescript/TSComponentWithImport.tsx", 158 | zeplinNames: [] 159 | } 160 | ); 161 | 162 | expect(componentCode).toMatchSnapshot(); 163 | }); 164 | 165 | test("MultiExportTSComponentWithProps.tsx snippet creation", async () => { 166 | const processor = new Plugin(); 167 | 168 | await processor.init(pluginContext); 169 | 170 | const componentCode = await processor.process( 171 | { 172 | path: "test/samples/typescript/MultiExportTSComponentWithProps.tsx", 173 | zeplinNames: [] 174 | } 175 | ); 176 | 177 | expect(componentCode).toMatchSnapshot(); 178 | }); 179 | }); 180 | }); 181 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "outDir": "dist", 6 | "declaration": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "noUnusedLocals": true, 10 | "noUnusedParameters": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "esModuleInterop": true, 14 | "resolveJsonModule": true, 15 | "moduleResolution": "node", 16 | "baseUrl": ".", 17 | "paths": { 18 | "*": [ 19 | "node_modules/*", 20 | "src/types/*" 21 | ] 22 | } 23 | }, 24 | "include": [ 25 | "src/**/*", 26 | "jest.config.js" 27 | ] 28 | } --------------------------------------------------------------------------------