diff --git a/.eslintignore b/.eslintignore index bb38ea59a30..8cf2efeda69 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,5 +1,4 @@ src/__tests__/fixtures/ dist/ flow-typed/ -website/node_modules -website/dist/ +node_modules diff --git a/.eslintrc.js b/.eslintrc.js index bde79e5a23c..1e4823c034d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,17 +1,17 @@ +'use strict'; + module.exports = { - parser: 'babel-eslint', - extends: ['eslint:recommended', 'prettier'], - plugins: ['prettier'], + parser: '@babel/eslint-parser', + extends: ['eslint:recommended', 'plugin:prettier/recommended'], parserOptions: { ecmaVersion: 2020, - sourceType: "module" + sourceType: 'module', }, rules: { strict: ['error', 'never'], 'no-shadow': 'error', 'no-var': 'error', 'prefer-const': 'error', - 'prettier/prettier': 'error', }, env: { node: true, @@ -24,11 +24,13 @@ module.exports = { }, overrides: [ { - files: 'website/**/*.js', - env: { browser: true }, + files: ['*rc.js', '*.config.js'], + parserOptions: { + ecmaVersion: 2019, + sourceType: 'script', + }, rules: { - // conflicts with jsx - 'no-unused-vars': 'off', + strict: ['error', 'global'], }, }, { diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000000..5ba8ebe371a --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,76 @@ +name: CI + +on: + push: + branches: + - 5.x + - renovate/** + pull_request: + branches: + - 5.x + +env: + NODE_VERSION: 18 + +jobs: + tests: + strategy: + matrix: + node: ['12', '14', '16', '18'] + name: Unit tests (Node v${{ matrix.node }}) + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node }} + cache: 'yarn' + + - name: Cache node_modules + uses: actions/cache@v3 + id: cache-nodemodules + with: + path: node_modules + key: ${{ runner.os }}-${{ env.NODE_VERSION }}-nodemodules-${{ hashFiles('**/yarn.lock') }} + + - name: Install dependencies + if: steps.cache-nodemodules.outputs.cache-hit != 'true' + run: yarn install --frozen-lockfile --non-interactive + + - name: Build + run: yarn build + + - name: Unit tests + run: yarn test --ci + + lint: + name: Lint + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'yarn' + + - name: Cache node_modules + uses: actions/cache@v3 + id: cache-nodemodules + with: + path: node_modules + key: ${{ runner.os }}-${{ env.NODE_VERSION }}-nodemodules-${{ hashFiles('**/yarn.lock') }} + + - name: Install dependencies + if: steps.cache-nodemodules.outputs.cache-hit != 'true' + run: yarn install --frozen-lockfile --non-interactive + + - name: Lint + run: yarn lint diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 00000000000..5f867eed1b8 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,39 @@ +on: + push: + branches: + - 5.x +name: release-please +env: + NODE_VERSION: 18 +jobs: + release-please: + runs-on: ubuntu-latest + steps: + - uses: google-github-actions/release-please-action@v3 + id: release + with: + release-type: node + package-name: react-docgen + default-branch: 5.x + token: ${{ secrets.RELEASE_PLEASE_TOKEN }} + + - name: Checkout + if: ${{ steps.release.outputs.release_created }} + uses: actions/checkout@v3 + + - name: Setup Node.js + if: ${{ steps.release.outputs.release_created }} + uses: actions/setup-node@v3 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'yarn' + registry-url: 'https://registry.npmjs.org/' + + - name: Install dependencies + if: ${{ steps.release.outputs.release_created }} + run: yarn install --frozen-lockfile --non-interactive + + - run: npm publish + env: + NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} + if: ${{ steps.release.outputs.release_created }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 129dee2946a..1990c7d9a70 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,4 @@ node_modules/ .idea/ coverage/ -/website/dist yarn-error.log diff --git a/.gitpod.yml b/.gitpod.yml new file mode 100644 index 00000000000..5535a025f61 --- /dev/null +++ b/.gitpod.yml @@ -0,0 +1,8 @@ +# This configuration file was automatically generated by Gitpod. +# Please adjust to your needs (see https://www.gitpod.io/docs/config-gitpod-file) +# and commit this file to your remote git repository to share the goodness with others. + +tasks: + - init: yarn install && yarn run build + + diff --git a/.prettierrc b/.prettierrc index 572790d721e..bbf374f1716 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,20 +1,19 @@ { - "arrowParens": "avoid", - "trailingComma": "all", - "useTabs": false, - "semi": true, - "singleQuote": true, - "bracketSpacing": true, - "jsxBracketSameLine": false, - "tabWidth": 2, - "printWidth": 80, - "overrides": [{ - "files": [ - "bin/react-docgen.js", - "bin/__tests__/example/**/*.js" - ], + "arrowParens": "avoid", + "trailingComma": "all", + "useTabs": false, + "semi": true, + "singleQuote": true, + "bracketSpacing": true, + "bracketSameLine": false, + "tabWidth": 2, + "printWidth": 80, + "overrides": [ + { + "files": ["bin/react-docgen.js", "bin/__tests__/example/**/*.js"], "options": { "trailingComma": "es5" } - }] - } \ No newline at end of file + } + ] +} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 319219ae861..00000000000 --- a/.travis.yml +++ /dev/null @@ -1,24 +0,0 @@ -language: node_js -node_js: - - "13" - - "12" - - "10" -cache: - yarn: true - directories: - - ".eslintcache" - - "node_modules" -script: - - yarn test:ci - # Build the website in PRs so we make sure that PRs do not break the website - - if [ "$TRAVIS_PULL_REQUEST" != "false" ] && [ "$TRAVIS_NODE_VERSION" = "10" ]; then yarn build:website; fi - -before_deploy: yarn build:website -deploy: - provider: pages - skip_cleanup: true - github_token: $GITHUB_DEPLOY_TOKEN - local_dir: website/dist - on: - branch: master - node: "12" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000000..ee543a9ba3c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ +# Changelog + +## [5.4.3](https://github.com/reactjs/react-docgen/compare/v5.4.2...v5.4.3) (2022-07-02) + + +### Bug Fixes + +* Add support for TSAsExpressions when trying to stringify expressions ([#634](https://github.com/reactjs/react-docgen/issues/634)) ([8b34e26](https://github.com/reactjs/react-docgen/commit/8b34e262e4da5f4b6fdbae5b2b810e761e17da8b)) +* Fix for expressionTo with Spread and Methods ([0c3c38f](https://github.com/reactjs/react-docgen/commit/0c3c38f0ffc4155ff4c037e28e85320fd3f61cb9)) + +## [5.4.2](https://github.com/reactjs/react-docgen/compare/v5.4.1...v5.4.2) (2022-06-13) + + +### Bug Fixes + +* Fix wrong detection of forwardRef in combination with memo ([#592](https://github.com/reactjs/react-docgen/issues/592)) ([522e8f5](https://github.com/reactjs/react-docgen/commit/522e8f55ccde49354a3949a777e52cc92d6b0330)) diff --git a/benchmark/fixtures/CircularProgress.js b/benchmark/fixtures/CircularProgress.js deleted file mode 100644 index e2c42bdab43..00000000000 --- a/benchmark/fixtures/CircularProgress.js +++ /dev/null @@ -1,249 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import clsx from 'clsx'; -import { chainPropTypes } from '@material-ui/utils'; -import withStyles from '../styles/withStyles'; -import { capitalize } from '../utils/helpers'; - -const SIZE = 44; - -function getRelativeValue(value, min, max) { - const clampedValue = Math.min(Math.max(min, value), max); - return (clampedValue - min) / (max - min); -} - -function easeOut(t) { - t = getRelativeValue(t, 0, 1); - // https://gist.github.com/gre/1650294 - t = (t -= 1) * t * t + 1; - return t; -} - -function easeIn(t) { - return t * t; -} - -export const styles = theme => ({ - /* Styles applied to the root element. */ - root: { - display: 'inline-block', - lineHeight: 1, // Keep the progress centered - }, - /* Styles applied to the root element if `variant="static"`. */ - static: { - transition: theme.transitions.create('transform'), - }, - /* Styles applied to the root element if `variant="indeterminate"`. */ - indeterminate: { - animation: 'mui-progress-circular-rotate 1.4s linear infinite', - // Backward compatible logic between JSS v9 and v10. - // To remove with the release of Material-UI v4 - animationName: '$mui-progress-circular-rotate', - }, - /* Styles applied to the root element if `color="primary"`. */ - colorPrimary: { - color: theme.palette.primary.main, - }, - /* Styles applied to the root element if `color="secondary"`. */ - colorSecondary: { - color: theme.palette.secondary.main, - }, - /* Styles applied to the `svg` element. */ - svg: {}, - /* Styles applied to the `circle` svg path. */ - circle: { - stroke: 'currentColor', - // Use butt to follow the specification, by chance, it's already the default CSS value. - // strokeLinecap: 'butt', - }, - /* Styles applied to the `circle` svg path if `variant="static"`. */ - circleStatic: { - transition: theme.transitions.create('stroke-dashoffset'), - }, - /* Styles applied to the `circle` svg path if `variant="indeterminate"`. */ - circleIndeterminate: { - animation: 'mui-progress-circular-dash 1.4s ease-in-out infinite', - // Backward compatible logic between JSS v9 and v10. - // To remove with the release of Material-UI v4 - animationName: '$mui-progress-circular-dash', - // Some default value that looks fine waiting for the animation to kicks in. - strokeDasharray: '80px, 200px', - strokeDashoffset: '0px', // Add the unit to fix a Edge 16 and below bug. - }, - '@keyframes mui-progress-circular-rotate': { - '100%': { - transform: 'rotate(360deg)', - }, - }, - '@keyframes mui-progress-circular-dash': { - '0%': { - strokeDasharray: '1px, 200px', - strokeDashoffset: '0px', - }, - '50%': { - strokeDasharray: '100px, 200px', - strokeDashoffset: '-15px', - }, - '100%': { - strokeDasharray: '100px, 200px', - strokeDashoffset: '-125px', - }, - }, - /* Styles applied to the `circle` svg path if `disableShrink={true}`. */ - circleDisableShrink: { - animation: 'none', - }, -}); - -/** - * ## ARIA - * - * If the progress bar is describing the loading progress of a particular region of a page, - * you should use `aria-describedby` to point to the progress bar, and set the `aria-busy` - * attribute to `true` on that region until it has finished loading. - */ -const CircularProgress = React.forwardRef(function CircularProgress( - props, - ref, -) { - const { - classes, - className, - color, - disableShrink, - size, - style, - thickness, - value, - variant, - ...other - } = props; - - const circleStyle = {}; - const rootStyle = {}; - const rootProps = {}; - - if (variant === 'determinate' || variant === 'static') { - const circumference = 2 * Math.PI * ((SIZE - thickness) / 2); - circleStyle.strokeDasharray = circumference.toFixed(3); - rootProps['aria-valuenow'] = Math.round(value); - - if (variant === 'static') { - circleStyle.strokeDashoffset = `${( - ((100 - value) / 100) * - circumference - ).toFixed(3)}px`; - rootStyle.transform = 'rotate(-90deg)'; - } else { - circleStyle.strokeDashoffset = `${( - easeIn((100 - value) / 100) * circumference - ).toFixed(3)}px`; - rootStyle.transform = `rotate(${(easeOut(value / 70) * 270).toFixed( - 3, - )}deg)`; - } - } - - return ( -
- - - -
- ); -}); - -CircularProgress.propTypes = { - /** - * Override or extend the styles applied to the component. - * See [CSS API](#css) below for more details. - */ - classes: PropTypes.object.isRequired, - /** - * @ignore - */ - className: PropTypes.string, - /** - * The color of the component. It supports those theme colors that make sense for this component. - */ - color: PropTypes.oneOf(['primary', 'secondary', 'inherit']), - /** - * If `true`, the shrink animation is disabled. - * This only works if variant is `indeterminate`. - */ - disableShrink: chainPropTypes(PropTypes.bool, props => { - if (props.disableShrink && props.variant !== 'indeterminate') { - return new Error( - 'Material-UI: you have provided the `disableShrink` property ' + - 'with a variant other than `indeterminate`. This will have no effect.', - ); - } - - return null; - }), - /** - * The size of the circle. - */ - size: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - /** - * @ignore - */ - style: PropTypes.object, - /** - * The thickness of the circle. - */ - thickness: PropTypes.number, - /** - * The value of the progress indicator for the determinate and static variants. - * Value between 0 and 100. - */ - value: PropTypes.number, - /** - * The variant to use. - * Use indeterminate when there is no progress value. - */ - variant: PropTypes.oneOf(['determinate', 'indeterminate', 'static']), -}; - -CircularProgress.defaultProps = { - color: 'primary', - disableShrink: false, - size: 40, - thickness: 3.6, - value: 0, - variant: 'indeterminate', -}; - -export default withStyles(styles, { name: 'MuiCircularProgress', flip: false })( - CircularProgress, -); diff --git a/benchmark/index.js b/benchmark/index.js deleted file mode 100644 index 51b24ee87fb..00000000000 --- a/benchmark/index.js +++ /dev/null @@ -1,60 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const Table = require('cli-table'); -const Benchmark = require('benchmark'); -const { parse } = require('..'); - -console.log(`Node: ${process.version}`); - -const head = ['fixture', 'timing']; - -const files = ['./fixtures/CircularProgress.js']; - -const table = new Table({ - head, - style: { - head: ['bold'], - }, -}); - -if (!global.gc) { - console.error( - 'Garbage collection unavailable. Pass --expose-gc ' + - 'when launching node to enable forced garbage collection.', - ); - process.exit(); -} - -files.forEach(file => { - const code = fs.readFileSync(path.join(__dirname, file), 'utf-8'); - const suite = new Benchmark.Suite(file.replace(/\.\/fixtures\//, '')); - const options = { filename: file, babelrc: false, configFile: false }; - - // warmup - parse(code, null, null, options); - global.gc(); - suite.add(0, () => { - parse(code, null, null, options); - }); - const result = [suite.name]; - suite.on('cycle', function(event) { - { - // separate scope so we can cleanup all this afterwards - const bench = event.target; - const factor = bench.hz < 100 ? 100 : 1; - const msg = `${Math.round(bench.hz * factor) / - factor} ops/sec ±${Math.round(bench.stats.rme * 100) / - 100}% (${Math.round(bench.stats.mean * 1000)}ms)`; - result.push(msg); - } - global.gc(); - }); - - console.log(`Running benchmark for ${suite.name} ...`); - global.gc(); - suite.run({ async: false }); - global.gc(); // gc is disabled so ensure we run it - table.push(result); -}); -global.gc(); // gc is disabled so ensure we run it -console.log(table.toString()); diff --git a/example/components/Component.js b/bin/__tests__/example/Component.js similarity index 86% rename from example/components/Component.js rename to bin/__tests__/example/Component.js index 4ee200ce902..e896a124b89 100644 --- a/example/components/Component.js +++ b/bin/__tests__/example/Component.js @@ -15,13 +15,13 @@ const Component = React.createClass({ bar: React.PropTypes.number, }, - getDefaultProps: function() { + getDefaultProps: function () { return { bar: 21, }; }, - render: function() { + render: function () { // ... }, }); diff --git a/bin/__tests__/example/MultipleComponents.js b/bin/__tests__/example/MultipleComponents.js index 694a5b1791a..58e677f4e63 100644 --- a/bin/__tests__/example/MultipleComponents.js +++ b/bin/__tests__/example/MultipleComponents.js @@ -10,14 +10,14 @@ const React = require('react'); exports.ComponentA = React.createClass({ displayName: 'ComponentA', - render: function() { + render: function () { // ... }, }); exports.ComponentB = React.createClass({ displayName: 'ComponentB', - render: function() { + render: function () { // ... }, }); diff --git a/bin/__tests__/example/customResolver.js b/bin/__tests__/example/customResolver.js index 54362d6de40..eacc54b1a2b 100644 --- a/bin/__tests__/example/customResolver.js +++ b/bin/__tests__/example/customResolver.js @@ -18,7 +18,7 @@ const code = ` const { NodePath } = require('ast-types'); -module.exports = function(ast, parser) { +module.exports = function (ast, parser) { return new NodePath(parser.parse(code)).get( 'program', 'body', diff --git a/bin/__tests__/react-docgen-test.js b/bin/__tests__/react-docgen-test.js index e7fc35fd432..b662f7f4643 100644 --- a/bin/__tests__/react-docgen-test.js +++ b/bin/__tests__/react-docgen-test.js @@ -35,9 +35,7 @@ function run(args, stdin) { }); } -const component = fs.readFileSync( - path.join(__dirname, '../../example/components/Component.js'), -); +const component = fs.readFileSync(path.join(__dirname, 'example/Component.js')); describe('react-docgen CLI', () => { let tempDir; @@ -246,22 +244,22 @@ describe('react-docgen CLI', () => { () => { return Promise.all([ // No option passed: same as --resolver=findExportedComponentDefinition - run([ - path.join(__dirname, '../../example/components/Component.js'), - ]).then(([stdout]) => { - expect(stdout).toContain('Component'); - }), + run([path.join(__dirname, 'example/Component.js')]).then( + ([stdout]) => { + expect(stdout).toContain('Component'); + }, + ), run([ '--resolver=findExportedComponentDefinition', - path.join(__dirname, '../../example/components/Component.js'), + path.join(__dirname, 'example/Component.js'), ]).then(([stdout]) => { expect(stdout).toContain('Component'); }), run([ '--resolver=findAllComponentDefinitions', - path.join(__dirname, './example/MultipleComponents.js'), + path.join(__dirname, 'example/MultipleComponents.js'), ]).then(([stdout]) => { expect(stdout).toContain('ComponentA'); expect(stdout).toContain('ComponentB'); @@ -277,7 +275,7 @@ describe('react-docgen CLI', () => { return Promise.all([ run([ '--resolver=' + path.join(__dirname, 'example/customResolver.js'), - path.join(__dirname, '../../example/components/Component.js'), + path.join(__dirname, 'example/Component.js'), ]).then(([stdout]) => { expect(stdout).toContain('Custom'); }), diff --git a/bin/react-docgen.js b/bin/react-docgen.js index f20ba5e4633..f7397e5194f 100755 --- a/bin/react-docgen.js +++ b/bin/react-docgen.js @@ -137,7 +137,7 @@ function traverseDir(filePath, result, done) { exclude: excludePatterns, excludeDir: ignoreDir, }, - function(error, content, filename, next) { + function (error, content, filename, next) { if (error) { throw error; } @@ -148,7 +148,7 @@ function traverseDir(filePath, result, done) { } next(); }, - function(error) { + function (error) { if (error) { throw error; } @@ -170,14 +170,14 @@ if (errorMessage) { let source = ''; process.stdin.setEncoding('utf8'); process.stdin.resume(); - const timer = setTimeout(function() { + const timer = setTimeout(function () { process.stderr.write('Still waiting for std input...'); }, 5000); - process.stdin.on('data', function(chunk) { + process.stdin.on('data', function (chunk) { clearTimeout(timer); source += chunk; }); - process.stdin.on('end', function() { + process.stdin.on('end', function () { try { writeResult(parse(source)); } catch (error) { @@ -191,8 +191,8 @@ if (errorMessage) { const result = Object.create(null); async.eachSeries( paths, - function(filePath, done) { - fs.stat(filePath, function(error, stats) { + function (filePath, done) { + fs.stat(filePath, function (error, stats) { if (error) { writeError(error, filePath); done(); @@ -216,14 +216,14 @@ if (errorMessage) { } }); }, - function() { + function () { const resultsPaths = Object.keys(result); if (resultsPaths.length === 0) { // we must have gotten an error process.exitCode = 1; } else if (paths.length === 1) { // a single path? - fs.stat(paths[0], function(error, stats) { + fs.stat(paths[0], function (error, stats) { writeResult(stats.isDirectory() ? result : result[resultsPaths[0]]); }); } else { diff --git a/example/buildDocs.sh b/example/buildDocs.sh deleted file mode 100755 index 89a9f61745a..00000000000 --- a/example/buildDocs.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env node - -/** - * This example script expects a JSON blob generated by react-docgen as input, - * e.g. react-docgen components/* | buildDocs.sh - */ - -var fs = require('fs'); -var generateMarkdown = require('./generateMarkdown'); -var path = require('path'); - -var json = ''; -process.stdin.setEncoding('utf8'); -process.stdin.on('readable', function() { - var chunk = process.stdin.read(); - if (chunk !== null) { - json += chunk; - } -}); - -process.stdin.on('end', function() { - buildDocs(JSON.parse(json)); -}); - -function buildDocs(api) { - // api is an object keyed by filepath. We use the file name as component name. - for (var filepath in api) { - var name = getComponentName(filepath); - var markdown = generateMarkdown(name, api[filepath]); - fs.writeFileSync(name + '.md', markdown); - process.stdout.write(filepath + ' -> ' + name + '.md\n'); - } -} - -function getComponentName(filepath) { - var name = path.basename(filepath); - // check for index.js - if (name === 'index.js') { - const dirs = path.dirname(filepath).split('/'); - name = dirs[dirs.length - 1]; - } - var ext; - while ((ext = path.extname(name))) { - name = name.substring(0, name.length - ext.length); - } - return name; -} diff --git a/example/components/NoComponent.js b/example/components/NoComponent.js deleted file mode 100644 index 24b93a2112d..00000000000 --- a/example/components/NoComponent.js +++ /dev/null @@ -1,4 +0,0 @@ -/** - * An example for a module that is not a component. - */ -module.exports = 'abc'; diff --git a/example/generateMarkdown.js b/example/generateMarkdown.js deleted file mode 100644 index cd45a10a070..00000000000 --- a/example/generateMarkdown.js +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -function stringOfLength(string, length) { - let newString = ''; - for (let i = 0; i < length; i++) { - newString += string; - } - return newString; -} - -function generateTitle(name) { - const title = '`' + name + '` (component)'; - return title + '\n' + stringOfLength('=', title.length) + '\n'; -} - -function generateDesciption(description) { - return description + '\n'; -} - -function generatePropType(type) { - let values; - if (Array.isArray(type.value)) { - values = - '(' + - type.value - .map(function(typeValue) { - return typeValue.name || typeValue.value; - }) - .join('|') + - ')'; - } else { - values = type.value; - } - - return 'type: `' + type.name + (values ? values : '') + '`\n'; -} - -function generatePropDefaultValue(value) { - return 'defaultValue: `' + value.value + '`\n'; -} - -function generateProp(propName, prop) { - return ( - '### `' + - propName + - '`' + - (prop.required ? ' (required)' : '') + - '\n' + - '\n' + - (prop.description ? prop.description + '\n\n' : '') + - (prop.type ? generatePropType(prop.type) : '') + - (prop.defaultValue ? generatePropDefaultValue(prop.defaultValue) : '') + - '\n' - ); -} - -function generateProps(props) { - if (!props) return '\n'; - const title = 'Props'; - - return ( - title + - '\n' + - stringOfLength('-', title.length) + - '\n' + - '\n' + - Object.keys(props) - .sort() - .map(function(propName) { - return generateProp(propName, props[propName]); - }) - .join('\n') - ); -} - -function generateMarkdown(name, reactAPI) { - const markdownString = - generateTitle(name) + - '\n' + - generateDesciption(reactAPI.description) + - '\n' + - generateProps(reactAPI.props); - - return markdownString; -} - -module.exports = generateMarkdown; diff --git a/package.json b/package.json index 2881b7667b0..23b7cc66344 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-docgen", - "version": "5.3.1", + "version": "5.4.3", "description": "A CLI and toolkit to extract information from React components for documentation generation.", "repository": { "type": "git", @@ -22,12 +22,10 @@ "main": "dist/main.js", "scripts": { "build": "rimraf dist/ && babel src/ --out-dir dist/ --ignore **/__tests__,**/__mocks__,**/src/types.js", - "build:website": "cd website/ && yarn && yarn build", "lint": "eslint . --report-unused-disable-directives", "fix": "eslint . --fix --report-unused-disable-directives", "prepare": "yarn build", "preversion": "yarn lint", - "start": "cd website && yarn && yarn start", "test": "jest", "test:ci": "yarn lint && yarn flow && yarn test --runInBand", "watch": "yarn build --watch" @@ -42,34 +40,34 @@ "license": "MIT", "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", "strip-indent": "^3.0.0" }, "devDependencies": { - "@babel/cli": "^7.7.5", - "@babel/plugin-transform-runtime": "^7.7.6", - "@babel/preset-env": "^7.7.6", - "@babel/preset-flow": "^7.7.4", - "babel-eslint": "^10.0.3", - "babel-jest": "^25.1.0", - "benchmark": "^2.1.4", - "cli-table": "^0.3.1", - "cross-spawn": "^7.0.1", - "eslint": "^6.8.0", - "eslint-config-prettier": "^6.9.0", - "eslint-plugin-prettier": "^3.1.1", - "flow-bin": "^0.98.0", - "jest": "^25.1.0", - "jest-diff": "^25.1.0", - "jest-matcher-utils": "^25.1.0", - "prettier": "^1.19.1", - "rimraf": "^3.0.0", - "temp": "^0.9.1" + "@babel/cli": "7.19.3", + "@babel/eslint-parser": "7.19.1", + "@babel/plugin-transform-runtime": "7.19.6", + "@babel/preset-env": "7.20.2", + "@babel/preset-flow": "7.18.6", + "babel-jest": "28.1.3", + "cross-spawn": "7.0.3", + "eslint": "8.29.0", + "eslint-config-prettier": "8.5.0", + "eslint-plugin-prettier": "4.2.1", + "flow-bin": "0.195.2", + "jest": "28.1.3", + "jest-diff": "28.1.3", + "jest-matcher-utils": "28.1.3", + "prettier": "2.8.1", + "rimraf": "3.0.2", + "temp": "0.9.4" }, "jest": { "snapshotSerializers": [ diff --git a/src/__tests__/__snapshots__/main-test.js.snap b/src/__tests__/__snapshots__/main-test.js.snap index 30c6605c57d..a75c318dae4 100644 --- a/src/__tests__/__snapshots__/main-test.js.snap +++ b/src/__tests__/__snapshots__/main-test.js.snap @@ -1577,3 +1577,75 @@ Object { }, } `; + +exports[`main fixtures processes component "component_41.tsx" without errors 1`] = ` +Object { + "description": "", + "displayName": "MyComponent", + "methods": Array [], + "props": Object { + "value": Object { + "description": "String value of a number", + "required": false, + "tsType": Object { + "name": "STRING_VALS[number]", + "raw": "typeof STRING_VALS[number]", + }, + }, + }, +} +`; + +exports[`main fixtures processes component "component_42.js" without errors 1`] = ` +Object { + "description": "", + "methods": Array [], + "props": Object { + "foo": Object { + "defaultValue": Object { + "computed": false, + "value": "'bar'", + }, + "required": false, + }, + }, +} +`; + +exports[`main fixtures processes component "component_43.tsx" without errors 1`] = ` +Object { + "description": "", + "displayName": "MenuItem", + "methods": Array [], + "props": Object { + "children": Object { + "description": "Menu item contents.", + "required": false, + "type": Object { + "name": "node", + }, + }, + "classes": Object { + "description": "Override or extend the styles applied to the component. See CSS API below for more details.", + "required": false, + "type": Object { + "name": "object", + }, + }, + "component": Object { + "defaultValue": Object { + "computed": false, + "value": "'li'", + }, + "required": false, + }, + "disableGutters": Object { + "defaultValue": Object { + "computed": false, + "value": "false", + }, + "required": false, + }, + }, +} +`; diff --git a/src/__tests__/fixtures/component_41.tsx b/src/__tests__/fixtures/component_41.tsx new file mode 100644 index 00000000000..ecdac2609ea --- /dev/null +++ b/src/__tests__/fixtures/component_41.tsx @@ -0,0 +1,24 @@ +import React from 'react'; + +export const STRING_VALS = [ + 'one', + 'two', + 'three' +]; + +interface IProps { + /** + * String value of a number + */ + value?: typeof STRING_VALS[number]; +} + +const MyComponent = (props: IProps) => { + return ( +
+ {props.value} +
+ ); +} + +export default MyComponent; diff --git a/src/__tests__/fixtures/component_42.js b/src/__tests__/fixtures/component_42.js new file mode 100644 index 00000000000..6e40462f356 --- /dev/null +++ b/src/__tests__/fixtures/component_42.js @@ -0,0 +1,3 @@ +import React, {memo, forwardRef} from 'react'; + +export default memo(forwardRef(({ foo = 'bar' }, ref) =>
{foo}
)); \ No newline at end of file diff --git a/src/__tests__/fixtures/component_43.tsx b/src/__tests__/fixtures/component_43.tsx new file mode 100644 index 00000000000..d8bd07bce92 --- /dev/null +++ b/src/__tests__/fixtures/component_43.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import MatMenuItem, { + MenuItemProps as MatMenuItemProps, +} from '@mui/material/MenuItem'; + +export type ComponentObjectOf< + T extends React.ElementType, + P = React.ComponentProps +> = React.FunctionComponent

; + +export type MenuItemProps< + D extends React.ElementType, + P = {} +> = MatMenuItemProps; + +const MenuItem = React.forwardRef>( + ( + props: MenuItemProps, + ref: React.Ref + ) => { + return ; + } +) as unknown as ( + props: MenuItemProps +) => React.ReactElement; + +(MenuItem as ComponentObjectOf).propTypes = { + /** Menu item contents. */ + children: PropTypes.node, + /** Override or extend the styles applied to the component. See CSS API below for more details. */ + classes: PropTypes.object, +}; + +(MenuItem as ComponentObjectOf).defaultProps = { + component: 'li', + disableGutters: false, +}; + +export default MenuItem; diff --git a/src/__tests__/parse-test.js b/src/__tests__/parse-test.js index 75d07628c7c..0bc4411d0d6 100644 --- a/src/__tests__/parse-test.js +++ b/src/__tests__/parse-test.js @@ -66,7 +66,7 @@ describe('parse', () => { ], }, }), - ).toThrowError(/.*Unexpected token \(1:13\).*/); + ).toThrowError(/.*Missing initializer in const declaration. \(1:13\).*/); }); it('supports custom parserOptions without plugins', () => { diff --git a/src/handlers/__tests__/componentDocblockHandler-test.js b/src/handlers/__tests__/componentDocblockHandler-test.js index 61952acc5ef..356e6450a0e 100644 --- a/src/handlers/__tests__/componentDocblockHandler-test.js +++ b/src/handlers/__tests__/componentDocblockHandler-test.js @@ -245,6 +245,13 @@ describe('componentDocblockHandler', () => { ); }); + describe('inline implementation with memo', () => { + test(` + React.memo(React.forwardRef((props, ref) => {})); + import React from "react";`, src => + beforeLastStatement(src).get('expression')); + }); + describe('out of line implementation', () => { test( [ diff --git a/src/handlers/__tests__/componentMethodsHandler-test.js b/src/handlers/__tests__/componentMethodsHandler-test.js index da27ef77ab0..812329937aa 100644 --- a/src/handlers/__tests__/componentMethodsHandler-test.js +++ b/src/handlers/__tests__/componentMethodsHandler-test.js @@ -156,6 +156,25 @@ describe('componentMethodsHandler', () => { expect(documentation.methods).toMatchSnapshot(); }); + it('should handle and ignore private properties', () => { + const src = ` + class Test extends React.Component { + #privateProperty = () => { + console.log('Do something'); + } + + componentDidMount() {} + + render() { + return null; + } + } + `; + + componentMethodsHandler(documentation, parse(src).get('body', 0)); + expect(documentation.methods.length).toBe(0); + }); + describe('function components', () => { it('no methods', () => { const src = ` diff --git a/src/handlers/__tests__/propTypeCompositionHandler-test.js b/src/handlers/__tests__/propTypeCompositionHandler-test.js index b77ea521d7c..3eacd3a1785 100644 --- a/src/handlers/__tests__/propTypeCompositionHandler-test.js +++ b/src/handlers/__tests__/propTypeCompositionHandler-test.js @@ -21,8 +21,8 @@ describe('propTypeCompositionHandler', () => { jest.mock('../../utils/getPropType'); documentation = new (require('../../Documentation'))(); - propTypeCompositionHandler = require('../propTypeCompositionHandler') - .default; + propTypeCompositionHandler = + require('../propTypeCompositionHandler').default; }); function test(getSrc, parse) { diff --git a/src/handlers/componentMethodsHandler.js b/src/handlers/componentMethodsHandler.js index 31f3a11b4bb..9e4e3a6f836 100644 --- a/src/handlers/componentMethodsHandler.js +++ b/src/handlers/componentMethodsHandler.js @@ -17,6 +17,11 @@ import match from '../utils/match'; import { traverseShallow } from '../utils/traverse'; import resolveToValue from '../utils/resolveToValue'; +function isPublicClassProperty(path) { + return ( + t.ClassProperty.check(path.node) && !t.ClassPrivateProperty.check(path.node) + ); +} /** * The following values/constructs are considered methods: * @@ -28,7 +33,7 @@ import resolveToValue from '../utils/resolveToValue'; function isMethod(path) { const isProbablyMethod = (t.MethodDefinition.check(path.node) && path.node.kind !== 'constructor') || - ((t.ClassProperty.check(path.node) || t.Property.check(path.node)) && + ((isPublicClassProperty(path) || t.Property.check(path.node)) && t.Function.check(path.get('value').node)); return isProbablyMethod && !isReactComponentMethod(path); @@ -45,7 +50,7 @@ function findAssignedMethods(scope, idPath) { const idScope = idPath.scope.lookup(idPath.node.name); traverseShallow((scope: any).path, { - visitAssignmentExpression: function(path) { + visitAssignmentExpression: function (path) { const node = path.node; if ( match(node.left, { diff --git a/src/handlers/defaultPropsHandler.js b/src/handlers/defaultPropsHandler.js index 638f08d7781..17acd90321a 100644 --- a/src/handlers/defaultPropsHandler.js +++ b/src/handlers/defaultPropsHandler.js @@ -49,11 +49,11 @@ function getDefaultValue(path: NodePath) { } function getStatelessPropsPath(componentDefinition): NodePath { - const value = resolveToValue(componentDefinition); + let value = resolveToValue(componentDefinition); if (isReactForwardRefCall(value)) { - const inner = resolveToValue(value.get('arguments', 0)); - return inner.get('params', 0); + value = resolveToValue(value.get('arguments', 0)); } + return value.get('params', 0); } @@ -74,9 +74,8 @@ function getDefaultPropsPath(componentDefinition: NodePath): ?NodePath { if (t.FunctionExpression.check(defaultPropsPath.node)) { // Find the value that is returned from the function and process it if it is // an object literal. - const returnValue = resolveFunctionDefinitionToReturnValue( - defaultPropsPath, - ); + const returnValue = + resolveFunctionDefinitionToReturnValue(defaultPropsPath); if (returnValue && t.ObjectExpression.check(returnValue.node)) { defaultPropsPath = returnValue; } diff --git a/src/handlers/propTypeCompositionHandler.js b/src/handlers/propTypeCompositionHandler.js index 17d289e32a5..bce9bd7d268 100644 --- a/src/handlers/propTypeCompositionHandler.js +++ b/src/handlers/propTypeCompositionHandler.js @@ -25,7 +25,7 @@ function amendComposes(documentation, path) { } function processObjectExpression(documentation, path) { - path.get('properties').each(function(propertyPath) { + path.get('properties').each(function (propertyPath) { switch (propertyPath.node.type) { case t.SpreadElement.name: amendComposes( diff --git a/src/handlers/propTypeHandler.js b/src/handlers/propTypeHandler.js index 5aa5b7e5816..30c5e1eddd7 100644 --- a/src/handlers/propTypeHandler.js +++ b/src/handlers/propTypeHandler.js @@ -64,7 +64,7 @@ function amendPropTypes(getDescriptor, path) { } function getPropTypeHandler(propName: string) { - return function(documentation: Documentation, path: NodePath) { + return function (documentation: Documentation, path: NodePath) { let propTypesPath = getMemberValuePath(path, propName); if (!propTypesPath) { return; diff --git a/src/resolver/findAllComponentDefinitions.js b/src/resolver/findAllComponentDefinitions.js index 3e3396e8c7e..900860153ca 100644 --- a/src/resolver/findAllComponentDefinitions.js +++ b/src/resolver/findAllComponentDefinitions.js @@ -45,7 +45,7 @@ export default function findAllReactCreateClassCalls( visitArrowFunctionExpression: statelessVisitor, visitClassExpression: classVisitor, visitClassDeclaration: classVisitor, - visitCallExpression: function(path) { + visitCallExpression: function (path) { if (isReactForwardRefCall(path)) { // If the the inner function was previously identified as a component // replace it with the parent node diff --git a/src/resolver/findAllExportedComponentDefinitions.js b/src/resolver/findAllExportedComponentDefinitions.js index a0f26d02b53..f2924d8ad15 100644 --- a/src/resolver/findAllExportedComponentDefinitions.js +++ b/src/resolver/findAllExportedComponentDefinitions.js @@ -114,7 +114,7 @@ export default function findExportedComponentDefinitions( visitExportNamedDeclaration: exportDeclaration, visitExportDefaultDeclaration: exportDeclaration, - visitAssignmentExpression: function(path: NodePath): ?boolean { + visitAssignmentExpression: function (path: NodePath): ?boolean { // Ignore anything that is not `exports.X = ...;` or // `module.exports = ...;` if (!isExportsOrModuleAssignment(path)) { diff --git a/src/resolver/findExportedComponentDefinition.js b/src/resolver/findExportedComponentDefinition.js index 0ad8b0028a0..2211383fbb9 100644 --- a/src/resolver/findExportedComponentDefinition.js +++ b/src/resolver/findExportedComponentDefinition.js @@ -118,7 +118,7 @@ export default function findExportedComponentDefinition( visitExportNamedDeclaration: exportDeclaration, visitExportDefaultDeclaration: exportDeclaration, - visitAssignmentExpression: function(path) { + visitAssignmentExpression: function (path) { // Ignore anything that is not `exports.X = ...;` or // `module.exports = ...;` if (!isExportsOrModuleAssignment(path)) { diff --git a/src/utils/__tests__/__snapshots__/getFlowType-test.js.snap b/src/utils/__tests__/__snapshots__/getFlowType-test.js.snap new file mode 100644 index 00000000000..e1e91b901bb --- /dev/null +++ b/src/utils/__tests__/__snapshots__/getFlowType-test.js.snap @@ -0,0 +1,107 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getFlowType handles ObjectTypeSpreadProperty 1`] = ` +Object { + "name": "signature", + "raw": "{| apple: string, banana: string, ...OtherFruits |}", + "signature": Object { + "properties": Array [ + Object { + "key": "apple", + "value": Object { + "name": "string", + "required": true, + }, + }, + Object { + "key": "banana", + "value": Object { + "name": "string", + "required": true, + }, + }, + Object { + "key": "orange", + "value": Object { + "name": "string", + "required": true, + }, + }, + ], + }, + "type": "object", +} +`; + +exports[`getFlowType handles nested ObjectTypeSpreadProperty 1`] = ` +Object { + "name": "signature", + "raw": "{| apple: string, banana: string, ...BreakfastFruits |}", + "signature": Object { + "properties": Array [ + Object { + "key": "apple", + "value": Object { + "name": "string", + "required": true, + }, + }, + Object { + "key": "banana", + "value": Object { + "name": "string", + "required": true, + }, + }, + Object { + "key": "mango", + "value": Object { + "name": "string", + "required": true, + }, + }, + Object { + "key": "orange", + "value": Object { + "name": "string", + "required": true, + }, + }, + Object { + "key": "lemon", + "value": Object { + "name": "string", + "required": true, + }, + }, + ], + }, + "type": "object", +} +`; + +exports[`getFlowType handles unresolved ObjectTypeSpreadProperty 1`] = ` +Object { + "name": "signature", + "raw": "{| apple: string, banana: string, ...MyType |}", + "signature": Object { + "properties": Array [ + Object { + "key": "apple", + "value": Object { + "name": "string", + "required": true, + }, + }, + Object { + "key": "banana", + "value": Object { + "name": "string", + "required": true, + }, + }, + ], + }, + "type": "object", +} +`; diff --git a/src/utils/__tests__/__snapshots__/getMembers-test.js.snap b/src/utils/__tests__/__snapshots__/getMembers-test.js.snap index 685744318fb..8014dac0dc9 100644 --- a/src/utils/__tests__/__snapshots__/getMembers-test.js.snap +++ b/src/utils/__tests__/__snapshots__/getMembers-test.js.snap @@ -12,6 +12,8 @@ Array [ "column": 12, "line": 1, }, + "filename": undefined, + "identifierName": undefined, "start": Position { "column": 9, "line": 1, @@ -31,6 +33,8 @@ Array [ "column": 8, "line": 1, }, + "filename": undefined, + "identifierName": undefined, "start": Position { "column": 1, "line": 1, @@ -43,6 +47,7 @@ Array [ "column": 4, "line": 1, }, + "filename": undefined, "identifierName": "foo", "start": Position { "column": 1, @@ -53,6 +58,7 @@ Array [ "start": 1, "type": "Identifier", }, + "optional": false, "property": Node { "end": 8, "loc": SourceLocation { @@ -60,6 +66,7 @@ Array [ "column": 8, "line": 1, }, + "filename": undefined, "identifierName": "bar", "start": Position { "column": 5, @@ -79,11 +86,14 @@ Array [ "column": 13, "line": 1, }, + "filename": undefined, + "identifierName": undefined, "start": Position { "column": 1, "line": 1, }, }, + "optional": false, "start": 1, "type": "CallExpression", }, @@ -95,6 +105,7 @@ Array [ "column": 8, "line": 1, }, + "filename": undefined, "identifierName": "bar", "start": Position { "column": 5, @@ -116,6 +127,7 @@ Array [ "column": 22, "line": 1, }, + "filename": undefined, "identifierName": "baz", "start": Position { "column": 19, @@ -137,6 +149,8 @@ Array [ "column": 26, "line": 1, }, + "filename": undefined, + "identifierName": undefined, "start": Position { "column": 24, "line": 1, diff --git a/src/utils/__tests__/__snapshots__/resolveGenericTypeAnnotations-test.js.snap b/src/utils/__tests__/__snapshots__/resolveGenericTypeAnnotations-test.js.snap new file mode 100644 index 00000000000..49f80e5323b --- /dev/null +++ b/src/utils/__tests__/__snapshots__/resolveGenericTypeAnnotations-test.js.snap @@ -0,0 +1,86 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`resolveGenericTypeAnnotation resolves type 1`] = ` +Node { + "callProperties": Array [], + "end": 57, + "exact": false, + "indexers": Array [], + "inexact": false, + "internalSlots": Array [], + "loc": SourceLocation { + "end": Position { + "column": 34, + "line": 3, + }, + "filename": undefined, + "identifierName": undefined, + "start": Position { + "column": 21, + "line": 3, + }, + }, + "properties": Array [ + Node { + "end": 55, + "key": Node { + "end": 47, + "loc": SourceLocation { + "end": Position { + "column": 24, + "line": 3, + }, + "filename": undefined, + "identifierName": "x", + "start": Position { + "column": 23, + "line": 3, + }, + }, + "name": "x", + "start": 46, + "type": "Identifier", + }, + "kind": "init", + "loc": SourceLocation { + "end": Position { + "column": 32, + "line": 3, + }, + "filename": undefined, + "identifierName": undefined, + "start": Position { + "column": 23, + "line": 3, + }, + }, + "method": false, + "optional": false, + "proto": false, + "start": 46, + "static": false, + "type": "ObjectTypeProperty", + "value": Node { + "end": 55, + "loc": SourceLocation { + "end": Position { + "column": 32, + "line": 3, + }, + "filename": undefined, + "identifierName": undefined, + "start": Position { + "column": 26, + "line": 3, + }, + }, + "start": 49, + "type": "StringTypeAnnotation", + }, + "variance": null, + }, + ], + "start": 44, + "type": "ObjectTypeAnnotation", +} +`; diff --git a/src/utils/__tests__/expressionTo-test.js b/src/utils/__tests__/expressionTo-test.js new file mode 100644 index 00000000000..44b5a142e23 --- /dev/null +++ b/src/utils/__tests__/expressionTo-test.js @@ -0,0 +1,54 @@ +import { expression, noopImporter } from '../../../tests/utils'; +import { Array as expressionToArray } from '../expressionTo'; + +describe('expressionTo', () => { + describe('MemberExpression', () => { + it('with only identifiers', () => { + expect( + expressionToArray(expression('foo.bar.baz'), noopImporter), + ).toEqual(['foo', 'bar', 'baz']); + }); + + it('with one computed literal', () => { + expect( + expressionToArray(expression('foo["bar"].baz'), noopImporter), + ).toEqual(['foo', '"bar"', 'baz']); + }); + + it('with one computed identifier', () => { + expect( + expressionToArray(expression('foo[bar].baz'), noopImporter), + ).toEqual(['foo', 'bar', 'baz']); + }); + + it('with one computed object', () => { + expect( + expressionToArray(expression('foo[{ a: "true"}].baz'), noopImporter), + ).toEqual(['foo', '{a: "true"}', 'baz']); + }); + + it('with one computed object with spread', () => { + expect( + expressionToArray(expression('foo[{ ...a }].baz'), noopImporter), + ).toEqual(['foo', '{...a}', 'baz']); + }); + + it('with one computed object with method', () => { + expect( + expressionToArray(expression('foo[{ a(){} }].baz'), noopImporter), + ).toEqual(['foo', '{a: }', 'baz']); + }); + + it('with TSAsExpression', () => { + expect( + expressionToArray( + expression('(baz as X).prop', { + filename: 'file.ts', + parserOptions: { plugins: ['typescript'] }, + }), + noopImporter, + ), + ).toEqual(['baz', 'prop']); + }); + }); +}); diff --git a/src/utils/__tests__/getFlowType-test.js b/src/utils/__tests__/getFlowType-test.js index 367c974ecb4..41207f3427d 100644 --- a/src/utils/__tests__/getFlowType-test.js +++ b/src/utils/__tests__/getFlowType-test.js @@ -745,4 +745,43 @@ describe('getFlowType', () => { raw: '{ subAction: SubAction }', }); }); + + it('handles ObjectTypeSpreadProperty', () => { + const typePath = statement(` + var x: {| apple: string, banana: string, ...OtherFruits |} = 2; + type OtherFruits = { orange: string } + `) + .get('declarations', 0) + .get('id') + .get('typeAnnotation') + .get('typeAnnotation'); + + expect(getFlowType(typePath)).toMatchSnapshot(); + }); + + it('handles unresolved ObjectTypeSpreadProperty', () => { + const typePath = statement(` + var x: {| apple: string, banana: string, ...MyType |} = 2; + `) + .get('declarations', 0) + .get('id') + .get('typeAnnotation') + .get('typeAnnotation'); + + expect(getFlowType(typePath)).toMatchSnapshot(); + }); + + it('handles nested ObjectTypeSpreadProperty', () => { + const typePath = statement(` + var x: {| apple: string, banana: string, ...BreakfastFruits |} = 2; + type BreakfastFruits = { mango: string, ...CitrusFruits }; + type CitrusFruits = { orange: string, lemon: string }; + `) + .get('declarations', 0) + .get('id') + .get('typeAnnotation') + .get('typeAnnotation'); + + expect(getFlowType(typePath)).toMatchSnapshot(); + }); }); diff --git a/src/utils/__tests__/getTSType-test.js b/src/utils/__tests__/getTSType-test.js index bc8a8914814..80216689f6c 100644 --- a/src/utils/__tests__/getTSType-test.js +++ b/src/utils/__tests__/getTSType-test.js @@ -377,6 +377,26 @@ describe('getTSType', () => { }); }); + it('resolves indexed access of array', () => { + const typePath = statement(` + var x: typeof STRING_VALS[number]; + + const STRING_VALS = [ + 'one', + 'two', + 'three' + ]; + `) + .get('declarations', 0) + .get('id') + .get('typeAnnotation') + .get('typeAnnotation'); + expect(getTSType(typePath)).toEqual({ + name: 'STRING_VALS[number]', + raw: 'typeof STRING_VALS[number]', + }); + }); + it('resolves types in scope', () => { const typePath = statement(` var x: MyType = 2; diff --git a/src/utils/__tests__/isReactForwardRefCall-test.js b/src/utils/__tests__/isReactForwardRefCall-test.js index 2395f7cba9d..d0e63e9b595 100644 --- a/src/utils/__tests__/isReactForwardRefCall-test.js +++ b/src/utils/__tests__/isReactForwardRefCall-test.js @@ -78,6 +78,14 @@ describe('isReactForwardRefCall', () => { expect(isReactForwardRefCall(def)).toBe(true); }); + it('does not accept forwardRef if not outer call', () => { + const def = parsePath(` + import { forwardRef, memo } from "react"; + memo(forwardRef({})); + `); + expect(isReactForwardRefCall(def)).toBe(false); + }); + it('accepts forwardRef called on imported aliased value', () => { const def = parsePath(` import { forwardRef as foo } from "react"; diff --git a/src/utils/__tests__/isStatelessComponent-test.js b/src/utils/__tests__/isStatelessComponent-test.js index 10de014a609..b020bed8a1f 100644 --- a/src/utils/__tests__/isStatelessComponent-test.js +++ b/src/utils/__tests__/isStatelessComponent-test.js @@ -88,9 +88,8 @@ describe('isStatelessComponent', () => { const returnExpr = componentIdentifiers[componentIdentifierName]; describe(componentIdentifierName, () => { Object.keys(componentStyle).forEach(componentName => { - const [componentFactory, componentSelector] = componentStyle[ - componentName - ]; + const [componentFactory, componentSelector] = + componentStyle[componentName]; describe(componentName, () => { Object.keys(modifiers).forEach(modifierName => { const modifierFactory = modifiers[modifierName]; diff --git a/src/utils/__tests__/isUnreachableFlowType-test.js b/src/utils/__tests__/isUnreachableFlowType-test.js new file mode 100644 index 00000000000..6edd0df1e45 --- /dev/null +++ b/src/utils/__tests__/isUnreachableFlowType-test.js @@ -0,0 +1,20 @@ +import { expression, statement } from '../../../tests/utils'; +import isUnreachableFlowType from '../isUnreachableFlowType'; + +describe('isUnreachableFlowType', () => { + it('considers Identifier as unreachable', () => { + expect(isUnreachableFlowType(expression('foo'))).toBe(true); + }); + + it('considers ImportDeclaration as unreachable', () => { + expect(isUnreachableFlowType(statement('import x from "";'))).toBe(true); + }); + + it('considers CallExpression as unreachable', () => { + expect(isUnreachableFlowType(expression('foo()'))).toBe(true); + }); + + it('considers VariableDeclaration not as unreachable', () => { + expect(isUnreachableFlowType(statement('const x = 1;'))).toBe(false); + }); +}); diff --git a/src/utils/__tests__/printValue-test.js b/src/utils/__tests__/printValue-test.js index b5856c71770..bcbd29d1efa 100644 --- a/src/utils/__tests__/printValue-test.js +++ b/src/utils/__tests__/printValue-test.js @@ -6,6 +6,7 @@ * */ +import { builders, NodePath } from 'ast-types'; import { parse } from '../../../tests/utils'; import printValue from '../printValue'; @@ -21,4 +22,32 @@ describe('printValue', () => { it('does not print trailing comments', () => { expect(printValue(pathFromSource('bar//foo'))).toEqual('bar'); }); + + it('handles arbitrary generated nodes', () => { + expect( + printValue( + new NodePath( + builders.arrayExpression([ + builders.literal('bar'), + builders.literal('foo'), + builders.literal(1), + builders.literal(2), + builders.literal(3), + builders.literal(null), + builders.memberExpression( + builders.identifier('foo'), + builders.identifier('bar'), + ), + builders.jsxElement( + builders.jsxOpeningElement( + builders.jsxIdentifier('Baz'), + [], + true, + ), + ), + ]), + ), + ), + ).toEqual('["bar", "foo", 1, 2, 3, null, foo.bar, ]'); + }); }); diff --git a/src/utils/__tests__/resolveGenericTypeAnnotations-test.js b/src/utils/__tests__/resolveGenericTypeAnnotations-test.js new file mode 100644 index 00000000000..94fe5874455 --- /dev/null +++ b/src/utils/__tests__/resolveGenericTypeAnnotations-test.js @@ -0,0 +1,23 @@ +import { statement, noopImporter } from '../../../tests/utils'; +import resolveGenericTypeAnnotation from '../resolveGenericTypeAnnotation'; + +describe('resolveGenericTypeAnnotation', () => { + it('resolves type', () => { + const code = ` + var x: Props; + type Props = { x: string }; + `; + expect( + resolveGenericTypeAnnotation( + statement(code).get( + 'declarations', + 0, + 'id', + 'typeAnnotation', + 'typeAnnotation', + ), + noopImporter, + ), + ).toMatchSnapshot(); + }); +}); diff --git a/src/utils/__tests__/resolveObjectValuesToArray-test.js b/src/utils/__tests__/resolveObjectValuesToArray-test.js index e14a7fa9026..f11b3c03fe2 100644 --- a/src/utils/__tests__/resolveObjectValuesToArray-test.js +++ b/src/utils/__tests__/resolveObjectValuesToArray-test.js @@ -116,6 +116,18 @@ describe('resolveObjectValuesToArray', () => { ); }); + it('resolves Object.values when using methods', () => { + const path = parse( + ['var foo = { boo: 1, foo: 2, bar(e) {} };', 'Object.values(foo);'].join( + '\n', + ), + ); + + expect(resolveObjectValuesToArray(path)).toEqualASTNode( + builders.arrayExpression([builders.literal(1), builders.literal(2)]), + ); + }); + it('resolves Object.values but ignores duplicates', () => { const path = parse( [ diff --git a/src/utils/expressionTo.js b/src/utils/expressionTo.js index 609f8fd2478..3aacee682c5 100644 --- a/src/utils/expressionTo.js +++ b/src/utils/expressionTo.js @@ -42,29 +42,36 @@ function toArray(path: NodePath): Array { } else if (t.Identifier.check(node)) { result.push(node.name); continue; + } else if (t.TSAsExpression.check(node)) { + if (t.Identifier.check(node.expression)) { + result.push(node.expression.name); + } + continue; } else if (t.Literal.check(node)) { result.push(node.raw); continue; + } else if (t.FunctionExpression.check(node)) { + result.push(''); + continue; } else if (t.ThisExpression.check(node)) { result.push('this'); continue; } else if (t.ObjectExpression.check(node)) { - const properties = path.get('properties').map(function(property) { - return ( - toString(property.get('key')) + ': ' + toString(property.get('value')) - ); + const properties = path.get('properties').map(function (property) { + if (t.SpreadElement.check(property.node)) { + return `...${toString(property.get('argument'))}`; + } else { + return ( + toString(property.get('key')) + + ': ' + + toString(property.get('value')) + ); + } }); result.push('{' + properties.join(', ') + '}'); continue; } else if (t.ArrayExpression.check(node)) { - result.push( - '[' + - path - .get('elements') - .map(toString) - .join(', ') + - ']', - ); + result.push('[' + path.get('elements').map(toString).join(', ') + ']'); continue; } } diff --git a/src/utils/getFlowType.js b/src/utils/getFlowType.js index 5eb1ad7ff7c..621c5d46310 100644 --- a/src/utils/getFlowType.js +++ b/src/utils/getFlowType.js @@ -201,6 +201,18 @@ function handleObjectTypeAnnotation( key: ((getPropertyName(param): any): string), value: getFlowTypeWithRequirements(param.get('value'), typeParams), }); + } else if (t.ObjectTypeSpreadProperty.check(param.node)) { + let spreadObject = resolveToValue(param.get('argument')); + if (t.GenericTypeAnnotation.check(spreadObject.node)) { + const typeAlias = resolveToValue(spreadObject.get('id')); + if (t.ObjectTypeAnnotation.check(typeAlias.get('right').node)) { + spreadObject = resolveToValue(typeAlias.get('right')); + } + } + if (t.ObjectTypeAnnotation.check(spreadObject.node)) { + const props = handleObjectTypeAnnotation(spreadObject, typeParams); + type.signature.properties.push(...props.signature.properties); + } } }); diff --git a/src/utils/getPropType.js b/src/utils/getPropType.js index febf24e65f4..85ce4084013 100644 --- a/src/utils/getPropType.js +++ b/src/utils/getPropType.js @@ -23,7 +23,7 @@ import type { PropTypeDescriptor, PropDescriptor } from '../types'; function getEnumValues(path) { const values = []; - path.get('elements').each(function(elementPath) { + path.get('elements').each(function (elementPath) { if (t.SpreadElement.check(elementPath.node)) { const value = resolveToValue(elementPath.get('argument')); @@ -75,7 +75,7 @@ function getPropTypeOneOfType(argumentPath) { type.computed = true; type.value = printValue(argumentPath); } else { - type.value = argumentPath.get('elements').map(function(itemPath) { + type.value = argumentPath.get('elements').map(function (itemPath) { const descriptor: PropTypeDescriptor = getPropType(itemPath); const docs = getDocblock(itemPath); if (docs) { @@ -136,7 +136,7 @@ function getPropTypeShapish(name, argumentPath) { if (t.ObjectExpression.check(argumentPath.node)) { const value = {}; - argumentPath.get('properties').each(function(propertyPath) { + argumentPath.get('properties').each(function (propertyPath) { if (propertyPath.get('type').value === t.SpreadElement.name) { // It is impossible to resolve a name for a spread element return; diff --git a/src/utils/getTSType.js b/src/utils/getTSType.js index 83f9117e56d..0a78f6f1049 100644 --- a/src/utils/getTSType.js +++ b/src/utils/getTSType.js @@ -164,10 +164,7 @@ function handleTSTypeLiteral( } else if (t.TSIndexSignature.check(param.node)) { type.signature.properties.push({ key: getTSTypeWithResolvedTypes( - param - .get('parameters') - .get(0) - .get('typeAnnotation'), + param.get('parameters').get(0).get('typeAnnotation'), typeParams, ), value: getTSTypeWithRequirements( @@ -352,10 +349,8 @@ function handleTSIndexedAccessType( typeParams: ?TypeParameters, ): FlowSimpleType { // eslint-disable-next-line no-undef - const objectType: $Shape = getTSTypeWithResolvedTypes( - path.get('objectType'), - typeParams, - ); + const objectType: $Shape = + getTSTypeWithResolvedTypes(path.get('objectType'), typeParams); // eslint-disable-next-line no-undef const indexType: $Shape = getTSTypeWithResolvedTypes( path.get('indexType'), @@ -365,12 +360,14 @@ function handleTSIndexedAccessType( // We only get the signature if the objectType is a type (vs interface) if (!objectType.signature) return { - name: `${objectType.name}[${indexType.value.toString()}]`, + name: `${objectType.name}[${ + indexType.value ? indexType.value.toString() : indexType.name + }]`, raw: printValue(path), }; const resolvedType = objectType.signature.properties.find(p => { // indexType.value = "'foo'" - return p.key === indexType.value.replace(/['"]+/g, ''); + return indexType.value && p.key === indexType.value.replace(/['"]+/g, ''); }); if (!resolvedType) { return { name: 'unknown' }; diff --git a/src/utils/isReactBuiltinCall.js b/src/utils/isReactBuiltinCall.js index 2cd492f808a..e3bd77e7033 100644 --- a/src/utils/isReactBuiltinCall.js +++ b/src/utils/isReactBuiltinCall.js @@ -42,10 +42,13 @@ export default function isReactBuiltinCall( // `import { createElement } from 'react'` (t.ImportDeclaration.check(value.node) && value.node.specifiers.some( - specifier => specifier.imported && specifier.imported.name === name, + specifier => + specifier.imported?.name === name && + specifier.local?.name === path.node.callee.name, )) ) { const module = resolveToModule(value); + return Boolean(module && isReactModuleName(module)); } } diff --git a/src/utils/isReactComponentClass.js b/src/utils/isReactComponentClass.js index 6bf3c682a14..6abf342a32e 100644 --- a/src/utils/isReactComponentClass.js +++ b/src/utils/isReactComponentClass.js @@ -59,7 +59,7 @@ export default function isReactComponentClass(path: NodePath): boolean { // check for @extends React.Component in docblock if (path.parentPath && path.parentPath.value) { const classDeclaration = Array.isArray(path.parentPath.value) - ? path.parentPath.value.find(function(declaration) { + ? path.parentPath.value.find(function (declaration) { return declaration.type === 'ClassDeclaration'; }) : path.parentPath.value; @@ -67,7 +67,7 @@ export default function isReactComponentClass(path: NodePath): boolean { if ( classDeclaration && classDeclaration.leadingComments && - classDeclaration.leadingComments.some(function(comment) { + classDeclaration.leadingComments.some(function (comment) { return /@extends\s+React\.Component/.test(comment.value); }) ) { diff --git a/src/utils/isReactComponentMethod.js b/src/utils/isReactComponentMethod.js index d568f523a10..9dffc68403f 100644 --- a/src/utils/isReactComponentMethod.js +++ b/src/utils/isReactComponentMethod.js @@ -35,7 +35,7 @@ const componentMethods = [ /** * Returns if the method path is a Component method. */ -export default function(methodPath: NodePath): boolean { +export default function (methodPath: NodePath): boolean { if ( !t.MethodDefinition.check(methodPath.node) && !t.Property.check(methodPath.node) diff --git a/src/utils/isReactCreateClassCall.js b/src/utils/isReactCreateClassCall.js index 1bdabce18c6..31b0243fae4 100644 --- a/src/utils/isReactCreateClassCall.js +++ b/src/utils/isReactCreateClassCall.js @@ -8,7 +8,6 @@ */ import { namedTypes as t } from 'ast-types'; -import match from './match'; import resolveToModule from './resolveToModule'; import isReactBuiltinCall from './isReactBuiltinCall'; @@ -24,7 +23,7 @@ function isReactCreateClassCallModular(path: NodePath): boolean { path = path.get('expression'); } - if (!match(path.node, { type: 'CallExpression' })) { + if (!t.CallExpression.check(path.node)) { return false; } const module = resolveToModule(path); diff --git a/src/utils/isReactModuleName.js b/src/utils/isReactModuleName.js index 2b2c0d2288c..c45f596ae62 100644 --- a/src/utils/isReactModuleName.js +++ b/src/utils/isReactModuleName.js @@ -20,7 +20,7 @@ const reactModules = [ * module name. */ export default function isReactModuleName(moduleName: string): boolean { - return reactModules.some(function(reactModuleName) { + return reactModules.some(function (reactModuleName) { return reactModuleName === moduleName.toLowerCase(); }); } diff --git a/src/utils/normalizeClassDefinition.js b/src/utils/normalizeClassDefinition.js index 9ccc04e1fe9..085279d4bc1 100644 --- a/src/utils/normalizeClassDefinition.js +++ b/src/utils/normalizeClassDefinition.js @@ -75,7 +75,7 @@ export default function normalizeClassDefinition( visitClassExpression: ignore, visitForInStatement: ignore, visitForStatement: ignore, - visitAssignmentExpression: function(path) { + visitAssignmentExpression: function (path) { if (t.MemberExpression.check(path.node.left)) { const first = getMemberExpressionRoot(path.get('left')); if ( diff --git a/src/utils/postProcessDocumentation.js b/src/utils/postProcessDocumentation.js index a624ea09fe0..21b6d82dfb4 100644 --- a/src/utils/postProcessDocumentation.js +++ b/src/utils/postProcessDocumentation.js @@ -20,7 +20,7 @@ function postProcessProps(props) { }); } -export default function( +export default function ( documentation: DocumentationObject, ): DocumentationObject { const props = documentation.props; diff --git a/src/utils/printValue.js b/src/utils/printValue.js index 288bc42023d..bcf0cc19867 100644 --- a/src/utils/printValue.js +++ b/src/utils/printValue.js @@ -8,6 +8,8 @@ */ import strip from 'strip-indent'; +import toBabel from 'estree-to-babel'; +import generate from '@babel/generator'; function deindent(code: string): string { const firstNewLine = code.indexOf('\n'); @@ -35,15 +37,28 @@ function getSrcFromAst(path: NodePath): string { */ export default function printValue(path: NodePath): string { if (path.node.start == null) { - // This only happens when we use AST builders to create nodes that do not actually - // exist in the source (e.g. when resolving Object.keys()). We might need to enhance - // this if we start using builders from `ast-types` more. - if (path.node.type === 'Literal') { - return `"${path.node.value}"`; + try { + const nodeCopy = { + ...path.node, + }; + + // `estree-to-babel` expects the `comments` property to exist on the top-level node + if (!nodeCopy.comments) { + nodeCopy.comments = []; + } + + return generate(toBabel(nodeCopy), { + comments: false, + concise: true, + }).code; + } catch (err) { + throw new Error( + `Cannot print raw value for type '${path.node.type}'. Please report this with an example at https://github.com/reactjs/react-docgen/issues. + +Original error: +${err.stack}`, + ); } - throw new Error( - `Cannot print raw value for type '${path.node.type}'. Please report this with an example at https://github.com/reactjs/react-docgen/issues`, - ); } const src = getSrcFromAst(path); diff --git a/src/utils/resolveObjectValuesToArray.js b/src/utils/resolveObjectValuesToArray.js index fa9caed7dd4..6984fb2ce1d 100644 --- a/src/utils/resolveObjectValuesToArray.js +++ b/src/utils/resolveObjectValuesToArray.js @@ -67,7 +67,9 @@ export function resolveObjectToPropMap( if (error) return; const prop = propPath.value; - if (prop.kind === 'get' || prop.kind === 'set') return; + if (prop.kind === 'get' || prop.kind === 'set' || prop.method === true) { + return; + } if ( t.Property.check(prop) || diff --git a/src/utils/resolveToValue.js b/src/utils/resolveToValue.js index 039092c08d7..aee2edbdbc3 100644 --- a/src/utils/resolveToValue.js +++ b/src/utils/resolveToValue.js @@ -77,7 +77,7 @@ function findLastAssignedValue(scope, idPath) { const name = idPath.node.name; traverseShallow(scope.path, { - visitAssignmentExpression: function(path) { + visitAssignmentExpression: function (path) { const node = path.node; // Skip anything that is not an assignment to a variable with the // passed name. diff --git a/tests/setupTestFramework.js b/tests/setupTestFramework.js index be09e4733e0..8945089921e 100644 --- a/tests/setupTestFramework.js +++ b/tests/setupTestFramework.js @@ -5,7 +5,7 @@ const diff = require('jest-diff'); const utils = require('jest-matcher-utils'); const matchers = { - toEqualASTNode: function(received, expected) { + toEqualASTNode: function (received, expected) { if (!expected || typeof expected !== 'object') { throw new Error( 'Expected value must be an object representing an AST node.\n' + diff --git a/website/.browserslistrc b/website/.browserslistrc deleted file mode 100644 index 7304b74e548..00000000000 --- a/website/.browserslistrc +++ /dev/null @@ -1,7 +0,0 @@ -# Browsers that we support -last 2 Chrome versions -last 2 Firefox versions -last 2 Edge versions -last 2 Safari versions -last 2 Opera versions -Firefox ESR diff --git a/website/babel.config.js b/website/babel.config.js deleted file mode 100644 index bb789e23845..00000000000 --- a/website/babel.config.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-flow'], - plugins: ['@babel/plugin-proposal-class-properties'], -}; diff --git a/website/package.json b/website/package.json deleted file mode 100644 index 1306dea6e5b..00000000000 --- a/website/package.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "react-docgen-website", - "private": true, - "description": "Website of react-docgen", - "repository": { - "type": "git", - "url": "https://github.com/reactjs/react-docgen.git" - }, - "bugs": "https://github.com/reactjs/react-docgen/issues", - "scripts": { - "build": "NODE_ENV=production webpack", - "start": "webpack-dev-server --open" - }, - "dependencies": {}, - "devDependencies": { - "@babel/core": "^7.8.4", - "@babel/plugin-proposal-class-properties": "^7.8.3", - "@babel/preset-env": "^7.8.4", - "@babel/preset-flow": "^7.8.3", - "@babel/preset-react": "^7.8.3", - "babel-loader": "^8.0.6", - "codemirror": "^5.51.0", - "css-loader": "^3.4.2", - "html-loader": "^0.5.5", - "html-webpack-plugin": "^3.2.0", - "less": "^3.10.3", - "less-loader": "^5.0.0", - "mini-css-extract-plugin": "^0.9.0", - "optimize-css-assets-webpack-plugin": "^5.0.3", - "react": "^16.12.0", - "react-dom": "^16.12.0", - "terser-webpack-plugin": "^2.3.4", - "webpack": "^4.41.5", - "webpack-cli": "^3.3.10", - "webpack-dev-server": "^3.10.3" - } -} diff --git a/website/src/components/App.js b/website/src/components/App.js deleted file mode 100644 index 843eeb8d8a8..00000000000 --- a/website/src/components/App.js +++ /dev/null @@ -1,150 +0,0 @@ -import React from 'react'; -import Panel from './CodeMirrorPanel'; -import Header from './Header'; -import { parse } from 'react-docgen'; - -const codeSample = `import React, { Component } from 'react'; -import PropTypes from 'prop-types'; - -/** - * General component description. - */ -class MyComponent extends Component { - render() { - // ... - } -} - -MyComponent.propTypes = { - /** - * Description of prop "foo". - */ - foo: PropTypes.number, - /** - * Description of prop "bar" (a custom validation function). - */ - bar: function(props, propName, componentName) { - // ... - }, - baz: PropTypes.oneOfType([ - PropTypes.number, - PropTypes.string - ]).isRequired, -}; - -MyComponent.defaultProps = { - foo: 42, - bar: 21 -}; - -export default MyComponent; -`; - -const defaultPlugins = [ - 'jsx', - 'asyncGenerators', - 'bigInt', - 'classProperties', - 'classPrivateProperties', - 'classPrivateMethods', - ['decorators', { decoratorsBeforeExport: false }], - 'doExpressions', - 'dynamicImport', - 'exportDefaultFrom', - 'exportNamespaceFrom', - 'functionBind', - 'functionSent', - 'importMeta', - 'logicalAssignment', - 'nullishCoalescingOperator', - 'numericSeparator', - 'objectRestSpread', - 'optionalCatchBinding', - 'optionalChaining', - ['pipelineOperator', { proposal: 'minimal' }], - 'throwExpressions', - 'topLevelAwait', -]; - -export default class App extends React.Component { - constructor() { - super(); - this._jsonRef = React.createRef(); - - const options = this.buildOptions('js'); - this.state = { - value: this.compile(codeSample, options), - mode: 'application/json', - content: codeSample, - options, - }; - } - - compile(value, options) { - return JSON.stringify(parse(value, null, null, options), null, 2); - } - - handleChange = value => { - let result; - let mode = 'text/plain'; - - try { - result = this.compile(value, this.state.options); - mode = 'application/json'; - } catch (err) { - result = String(err); - } - this.setState({ value: result, mode, content: value }); - }; - - buildOptions(language) { - const options = { - babelrc: false, - babelrcRoots: false, - configFile: false, - filename: 'playground.js', - parserOptions: { - plugins: [...defaultPlugins], - }, - }; - switch (language) { - case 'ts': - options.parserOptions.plugins.push('typescript'); - options.filename = 'playground.tsx'; - break; - case 'flow': - options.parserOptions.plugins.push('flow'); - break; - } - - return options; - } - - handleLanguageChange = language => { - this.setState({ options: this.buildOptions(language) }, () => - this.handleChange(this.state.content), - ); - }; - - render() { - return ( - <> -

-
- - -
- - ); - } -} diff --git a/website/src/components/CodeMirrorPanel.js b/website/src/components/CodeMirrorPanel.js deleted file mode 100644 index 642d83a7274..00000000000 --- a/website/src/components/CodeMirrorPanel.js +++ /dev/null @@ -1,90 +0,0 @@ -import React from 'react'; -import CodeMirror from 'codemirror'; -import 'codemirror/mode/javascript/javascript'; -import 'codemirror/mode/jsx/jsx'; - -import 'codemirror/addon/fold/foldgutter'; -import 'codemirror/addon/fold/brace-fold'; -import 'codemirror/addon/fold/comment-fold'; -import 'codemirror/addon/fold/xml-fold'; -import 'codemirror/addon/fold/foldgutter.css'; - -export default class CodeMirrorPanel extends React.Component { - static defaultProps = { - lineNumbers: true, - tabSize: 2, - showCursorWhenSelecting: true, - autoCloseBrackets: true, - matchBrackets: true, - //keyMap: 'sublime', - }; - constructor() { - super(); - this._textareaRef = React.createRef(); - this._codeMirror = null; - this._cached = ''; - this.handleChange = this.handleChange.bind(this); - this.handleFocus = this.handleFocus.bind(this); - } - - componentDidMount() { - const options = Object.assign( - { - foldGutter: true, - gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], - }, - this.props, - ); - delete options.value; - delete options.onChange; - delete options.codeSample; - - this._codeMirror = CodeMirror.fromTextArea( - this._textareaRef.current, - options, - ); - this._codeMirror.on('change', this.handleChange); - this._codeMirror.on('focus', this.handleFocus); - - this.updateValue(this.props.value || ''); - } - - componentWillUnmount() { - this._codeMirror && this._codeMirror.toTextArea(); - } - - componentDidUpdate(prevProps) { - if (this.props.value !== this._cached && this.props.value != null) { - this.updateValue(this.props.value); - } - if (this.props.mode !== prevProps.mode && this.props.mode != null) { - this._codeMirror.setOption('mode', this.props.mode); - } - } - - updateValue(value) { - this._cached = value; - this._codeMirror.setValue(value); - } - - handleFocus(/* codeMirror, event */) { - if (this._codeMirror.getValue() === this.props.codeSample) { - this._codeMirror.execCommand('selectAll'); - } - } - - handleChange(doc, change) { - if (change.origin !== 'setValue') { - this._cached = doc.getValue(); - this.props.onChange(this._cached); - } - } - - render() { - return ( -
-