Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[core] Generate PropTypes from type definitions #16642

Merged
merged 30 commits into from
Aug 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
980f5c8
[core] Generate proptypes from type declarations
merceyz Jul 20, 2019
e85e04c
[styles] Add JSDoc to StyledComponentProps['classes']
merceyz Jul 20, 2019
4d20e1d
AppBar
merceyz Jul 20, 2019
f8103b6
Portal
merceyz Jul 20, 2019
3de175b
Backdrop
merceyz Jul 20, 2019
30ccf41
Badge
merceyz Jul 21, 2019
318490f
generateMarkdown - instanceOf
merceyz Jul 21, 2019
482cd9e
Portal - Fallback to object
merceyz Jul 21, 2019
6b4c90d
docs:api
merceyz Jul 21, 2019
ac4b01d
Portal - Use ReactNode
merceyz Jul 21, 2019
7289e88
BottomNavigation
merceyz Jul 21, 2019
cca45a5
Disable no-redundant-jsdoc
merceyz Jul 22, 2019
aab0b7b
Ignore tsconfig
merceyz Jul 22, 2019
04e2dfd
Don't resolve theme or props
merceyz Jul 22, 2019
c49722b
Update portal
merceyz Jul 23, 2019
5b10cf5
Update config.yml
merceyz Jul 23, 2019
9997c1b
Bump typescript-to-proptypes
merceyz Jul 23, 2019
dc5d54d
[Portal] Set children as optional
merceyz Jul 23, 2019
9d61dc0
Bump typescript-to-proptypes
merceyz Jul 23, 2019
2c0629c
[Badge] Remove inherit from color
merceyz Jul 24, 2019
eb56847
yarn proptypes
merceyz Jul 24, 2019
0039cf1
Exit code on error
merceyz Jul 24, 2019
11c368b
docs:api
merceyz Jul 24, 2019
9143483
fixLineEndings - replace all
merceyz Jul 24, 2019
47ff563
Bump typescript-to-proptypes
merceyz Jul 24, 2019
613ded5
JSX.Element | null
merceyz Jul 24, 2019
5591026
Merge branch 'master' into pr/merceyz/16642
eps1lon Aug 5, 2019
9b64ba7
Merge branch 'master' into pr/merceyz/16642
eps1lon Aug 5, 2019
912d24d
codecov doesnt care about merge commits
eps1lon Aug 5, 2019
3a51fe1
Remove dangling prop type
eps1lon Aug 5, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ jobs:
steps:
- checkout
- install_js
- run:
name: Generate PropTypes
command: yarn proptypes --disable-cache
- run:
name: '`yarn proptypes` changes committed?'
command: git diff --exit-code
- run:
name: Can we generate the documentation?
command: yarn docs:api
Expand Down
2 changes: 1 addition & 1 deletion docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
"redux": "^4.0.4",
"redux-logger": "^3.0.6",
"styled-components": "^4.3.2",
"typescript-to-proptypes": "^1.1.0",
"typescript-to-proptypes": "^1.2.3",
"url-loader": "^2.1.0",
"webfontloader": "^1.6.28"
},
Expand Down
6 changes: 3 additions & 3 deletions docs/pages/api/app-bar.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ import AppBar from '@material-ui/core/AppBar';

| Name | Type | Default | Description |
|:-----|:-----|:--------|:------------|
| <span class="prop-name required">children&nbsp;*</span> | <span class="prop-type">node</span> | | The content of the component. |
| <span class="prop-name">children</span> | <span class="prop-type">node</span> | | The content of the component. |
| <span class="prop-name">classes</span> | <span class="prop-type">object</span> | | Override or extend the styles applied to the component. See [CSS API](#css) below for more details. |
| <span class="prop-name">color</span> | <span class="prop-type">'inherit'<br>&#124;&nbsp;'primary'<br>&#124;&nbsp;'secondary'<br>&#124;&nbsp;'default'</span> | <span class="prop-default">'primary'</span> | The color of the component. It supports those theme colors that make sense for this component. |
| <span class="prop-name">position</span> | <span class="prop-type">'fixed'<br>&#124;&nbsp;'absolute'<br>&#124;&nbsp;'sticky'<br>&#124;&nbsp;'static'<br>&#124;&nbsp;'relative'</span> | <span class="prop-default">'fixed'</span> | The positioning type. The behavior of the different options is described [in the MDN web docs](https://developer.mozilla.org/en-US/docs/Learn/CSS/CSS_layout/Positioning). Note: `sticky` is not universally supported and will fall back to `static` when unavailable. |
| <span class="prop-name">color</span> | <span class="prop-type">'default'<br>&#124;&nbsp;'inherit'<br>&#124;&nbsp;'primary'<br>&#124;&nbsp;'secondary'</span> | <span class="prop-default">'primary'</span> | The color of the component. It supports those theme colors that make sense for this component. |
| <span class="prop-name">position</span> | <span class="prop-type">'absolute'<br>&#124;&nbsp;'fixed'<br>&#124;&nbsp;'relative'<br>&#124;&nbsp;'static'<br>&#124;&nbsp;'sticky'</span> | <span class="prop-default">'fixed'</span> | The positioning type. The behavior of the different options is described [in the MDN web docs](https://developer.mozilla.org/en-US/docs/Learn/CSS/CSS_layout/Positioning). Note: `sticky` is not universally supported and will fall back to `static` when unavailable. |

The `ref` is forwarded to the root element.

Expand Down
3 changes: 2 additions & 1 deletion docs/pages/api/backdrop.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ import Backdrop from '@material-ui/core/Backdrop';

| Name | Type | Default | Description |
|:-----|:-----|:--------|:------------|
| <span class="prop-name">children</span> | <span class="prop-type">node</span> | | The content of the component. |
| <span class="prop-name">classes</span> | <span class="prop-type">object</span> | | Override or extend the styles applied to the component. See [CSS API](#css) below for more details. |
| <span class="prop-name">invisible</span> | <span class="prop-type">bool</span> | <span class="prop-default">false</span> | If `true`, the backdrop is invisible. It can be used when rendering a popover or a custom select component. |
| <span class="prop-name required">open&nbsp;*</span> | <span class="prop-type">bool</span> | | If `true`, the backdrop is open. |
| <span class="prop-name">transitionDuration</span> | <span class="prop-type">number<br>&#124;&nbsp;{ enter?: number, exit?: number }</span> | | The duration for the transition, in milliseconds. You may specify a single timeout for all transitions, or individually with an object. |
| <span class="prop-name">transitionDuration</span> | <span class="prop-type">number<br>&#124;&nbsp;{ appear?: number, enter?: number, exit?: number }</span> | | The duration for the transition, in milliseconds. You may specify a single timeout for all transitions, or individually with an object. |

The `ref` is forwarded to the root element.

Expand Down
6 changes: 3 additions & 3 deletions docs/pages/api/badge.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ import Badge from '@material-ui/core/Badge';
| Name | Type | Default | Description |
|:-----|:-----|:--------|:------------|
| <span class="prop-name">badgeContent</span> | <span class="prop-type">node</span> | | The content rendered within the badge. |
| <span class="prop-name required">children&nbsp;*</span> | <span class="prop-type">node</span> | | The badge will be added relative to this node. |
| <span class="prop-name">children</span> | <span class="prop-type">node</span> | | The badge will be added relative to this node. |
| <span class="prop-name">classes</span> | <span class="prop-type">object</span> | | Override or extend the styles applied to the component. See [CSS API](#css) below for more details. |
| <span class="prop-name">color</span> | <span class="prop-type">'default'<br>&#124;&nbsp;'primary'<br>&#124;&nbsp;'secondary'<br>&#124;&nbsp;'error'</span> | <span class="prop-default">'default'</span> | The color of the component. It supports those theme colors that make sense for this component. |
| <span class="prop-name">color</span> | <span class="prop-type">'default'<br>&#124;&nbsp;'error'<br>&#124;&nbsp;'primary'<br>&#124;&nbsp;'secondary'</span> | <span class="prop-default">'default'</span> | The color of the component. It supports those theme colors that make sense for this component. |
| <span class="prop-name">component</span> | <span class="prop-type">elementType</span> | <span class="prop-default">'span'</span> | The component used for the root node. Either a string to use a DOM element or a component. |
| <span class="prop-name">invisible</span> | <span class="prop-type">bool</span> | | If `true`, the badge will be invisible. |
| <span class="prop-name">max</span> | <span class="prop-type">number</span> | <span class="prop-default">99</span> | Max count to show. |
| <span class="prop-name">showZero</span> | <span class="prop-type">bool</span> | <span class="prop-default">false</span> | Controls whether the badge is hidden when `badgeContent` is zero. |
| <span class="prop-name">variant</span> | <span class="prop-type">'standard'<br>&#124;&nbsp;'dot'</span> | <span class="prop-default">'standard'</span> | The variant to use. |
| <span class="prop-name">variant</span> | <span class="prop-type">'dot'<br>&#124;&nbsp;'standard'</span> | <span class="prop-default">'standard'</span> | The variant to use. |

The `ref` is forwarded to the root element.

Expand Down
2 changes: 1 addition & 1 deletion docs/pages/api/bottom-navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import BottomNavigation from '@material-ui/core/BottomNavigation';

| Name | Type | Default | Description |
|:-----|:-----|:--------|:------------|
| <span class="prop-name required">children&nbsp;*</span> | <span class="prop-type">node</span> | | The content of the component. |
| <span class="prop-name">children</span> | <span class="prop-type">node</span> | | The content of the component. |
| <span class="prop-name">classes</span> | <span class="prop-type">object</span> | | Override or extend the styles applied to the component. See [CSS API](#css) below for more details. |
| <span class="prop-name">component</span> | <span class="prop-type">elementType</span> | <span class="prop-default">'div'</span> | The component used for the root node. Either a string to use a DOM element or a component. |
| <span class="prop-name">onChange</span> | <span class="prop-type">func</span> | | Callback fired when the value changes.<br><br>**Signature:**<br>`function(event: object, value: any) => void`<br>*event:* The event source of the callback<br>*value:* We default to the index of the child |
Expand Down
4 changes: 2 additions & 2 deletions docs/pages/api/portal.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ that exists outside the DOM hierarchy of the parent component.

| Name | Type | Default | Description |
|:-----|:-----|:--------|:------------|
| <span class="prop-name required">children&nbsp;*</span> | <span class="prop-type">node</span> | | The children to render into the `container`. |
| <span class="prop-name">container</span> | <span class="prop-type">object<br>&#124;&nbsp;func</span> | | A node, component instance, or function that returns either. The `container` will have the portal children appended to it. By default, it uses the body of the top-level document object, so it's simply `document.body` most of the time. |
| <span class="prop-name">children</span> | <span class="prop-type">node</span> | | The children to render into the `container`. |
| <span class="prop-name">container</span> | <span class="prop-type">func<br>&#124;&nbsp;React.Component<br>&#124;&nbsp;Element</span> | | A node, component instance, or function that returns either. The `container` will have the portal children appended to it. By default, it uses the body of the top-level document object, so it's simply `document.body` most of the time. |
| <span class="prop-name">disablePortal</span> | <span class="prop-type">bool</span> | <span class="prop-default">false</span> | Disable the portal behavior. The children stay within it's parent DOM hierarchy. |
| <span class="prop-name">onRendered</span> | <span class="prop-type">func</span> | | Callback fired once the children has been mounted into the `container`.<br>This prop will be deprecated and removed in v5, the ref can be used instead. |

Expand Down
10 changes: 1 addition & 9 deletions docs/scripts/buildApi.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable no-console */

import { mkdir, readFileSync, writeFileSync } from 'fs';
import { EOL } from 'os';
import { getLineFeed } from './helpers';
import path from 'path';
import kebabCase from 'lodash/kebabCase';
import { defaultHandlers, parse as docgenParse } from 'react-docgen';
Expand Down Expand Up @@ -81,14 +81,6 @@ function getInheritance(testInfo, src) {
};
}

function getLineFeed(source) {
const match = source.match(/\r?\n/);
if (match === null) {
return EOL;
}
return match[0];
}

async function buildDocs(options) {
const { component: componentObject, pagesMarkdown } = options;
const src = readFileSync(componentObject.filename, 'utf8');
Expand Down
22 changes: 6 additions & 16 deletions docs/scripts/formattedTSDemos.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ const fse = require('fs-extra');
const path = require('path');
const babel = require('@babel/core');
const prettier = require('prettier');
const os = require('os');
const typescriptToProptypes = require('typescript-to-proptypes');
const { fixBabelGeneratorIssues, fixLineEndings } = require('./helpers');

const tsConfig = typescriptToProptypes.loadConfig(path.resolve(__dirname, '../tsconfig.json'));

Expand Down Expand Up @@ -61,19 +61,6 @@ async function getFiles(root) {
return files;
}

function getLineFeed(source) {
const match = source.match(/\r?\n/);
if (match === null) {
return os.EOL;
}
return match[0];
}

const fixBabelIssuesRegExp = new RegExp(/(?<=(\/>)|,)(\r?\n){2}/g);
function fixBabelGeneratorIssues(source) {
return source.replace(fixBabelIssuesRegExp, getLineFeed(source));
}

const TranspileResult = {
Success: 0,
Skipped: 1,
Expand All @@ -91,7 +78,9 @@ async function transpileFile(tsxPath, program, ignoreCache = false) {
}
}

const { code } = await babel.transformFileAsync(tsxPath, babelConfig);
const source = await fse.readFile(tsxPath, 'utf8');

const { code } = await babel.transformAsync(source, { ...babelConfig, filename: tsxPath });

if (/import \w* from 'prop-types'/.test(code)) {
throw new Error('TypeScript demo contains prop-types, please remove them');
Expand All @@ -110,8 +99,9 @@ async function transpileFile(tsxPath, program, ignoreCache = false) {

const prettified = prettier.format(codeWithPropTypes, { ...prettierConfig, filepath: tsxPath });
const formatted = fixBabelGeneratorIssues(prettified);
const correctedLineEndings = fixLineEndings(source, formatted);
merceyz marked this conversation as resolved.
Show resolved Hide resolved

await fse.writeFile(jsPath, formatted);
await fse.writeFile(jsPath, correctedLineEndings);
return TranspileResult.Success;
} catch (err) {
console.error('Something went wrong transpiling %s\n%s\n', tsxPath, err);
Expand Down
21 changes: 21 additions & 0 deletions docs/scripts/helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const os = require('os');

function getLineFeed(source) {
const match = source.match(/\r?\n/);
return match === null ? os.EOL : match[0];
}

const fixBabelIssuesRegExp = new RegExp(/(?<=(\/>)|,)(\r?\n){2}/g);
function fixBabelGeneratorIssues(source) {
return source.replace(fixBabelIssuesRegExp, '\n');
}

function fixLineEndings(source, target) {
return target.replace(/\r?\n/g, getLineFeed(source));
}

module.exports = {
getLineFeed,
fixBabelGeneratorIssues,
fixLineEndings,
};
8 changes: 8 additions & 0 deletions docs/src/modules/utils/generateMarkdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,14 @@ function generatePropType(type) {
case 'arrayOf': {
return `Array<${generatePropType(type.value)}>`;
}

case 'instanceOf': {
if (type.value.startsWith('typeof')) {
return /typeof (.*) ===/.exec(type.value)[1];
}
return type.value;
}

default:
return type.name;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ function renderInput(inputProps) {
}

renderInput.propTypes = {
/**
* Override or extend the styles applied to the component.
*/
classes: PropTypes.object.isRequired,
InputProps: PropTypes.object,
};
Expand Down
2 changes: 1 addition & 1 deletion docs/src/pages/components/drawers/ResponsiveDrawer.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ ResponsiveDrawer.propTypes = {
* Injected by the documentation to work in an iframe.
* You won't need it on your project.
*/
container: PropTypes.object,
container: PropTypes.instanceOf(typeof Element === 'undefined' ? Object : Element),
};

export default ResponsiveDrawer;
3 changes: 3 additions & 0 deletions docs/src/pages/styles/basics/AdaptingHOC.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ function MyButtonRaw(props) {
}

MyButtonRaw.propTypes = {
/**
* Override or extend the styles applied to the component.
*/
classes: PropTypes.object.isRequired,
color: PropTypes.oneOf(['blue', 'red']).isRequired,
};
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"version": "4.3.1",
"private": true,
"scripts": {
"proptypes": "ts-node --skip-project ./packages/material-ui/scripts/generateProptypes.ts",
"deduplicate": "node scripts/deduplicate.js",
"argos": "argos upload test/regressions/screenshots/chrome --token $ARGOS_TOKEN",
"docs:api": "rimraf ./docs/pages/api && cross-env BABEL_ENV=test babel-node ./docs/scripts/buildApi.js ./packages/material-ui/src ./docs/pages/api && cross-env BABEL_ENV=test babel-node ./docs/scripts/buildApi.js ./packages/material-ui-lab/src ./docs/pages/api",
Expand Down Expand Up @@ -56,9 +57,12 @@
"@types/chai": "^4.1.7",
"@types/chai-dom": "^0.0.7",
"@types/enzyme": "^3.10.3",
"@types/fs-extra": "^8.0.0",
"@types/glob": "^7.1.1",
"@types/jsdom": "^12.2.4",
"@types/lodash": "^4.14.136",
"@types/mocha": "^5.2.7",
"@types/prettier": "^1.18.0",
"@types/react": "^16.8.23",
"@types/sinon": "^7.0.13",
"argos-cli": "^0.1.1",
Expand Down Expand Up @@ -124,6 +128,7 @@
"rollup-plugin-terser": "^5.1.1",
"sinon": "^7.0.0",
"size-limit": "^0.21.0",
"ts-node": "^8.3.0",
"tslint": "5.14.0",
"typescript": "3.2.4",
"vrtest": "^0.2.0",
Expand Down
3 changes: 3 additions & 0 deletions packages/material-ui-styles/src/withStyles/withStyles.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ export type WithStyles<
} & PropsOfStyles<StylesType>;

export interface StyledComponentProps<ClassKey extends string = string> {
/**
* Override or extend the styles applied to the component.
*/
classes?: Partial<ClassNameMap<ClassKey>>;
innerRef?: React.Ref<any> | React.RefObject<any>;
}
Expand Down
130 changes: 130 additions & 0 deletions packages/material-ui/scripts/generateProptypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import * as path from 'path';
import * as fse from 'fs-extra';
import * as ttp from 'typescript-to-proptypes';
import * as prettier from 'prettier';
import * as glob from 'glob';
import * as _ from 'lodash';
import { fixBabelGeneratorIssues, fixLineEndings } from '../../../docs/scripts/helpers';

const ignoreCache = process.argv.includes('--disable-cache');
const verbose = process.argv.includes('--verbose');

enum GenerateResult {
Success,
Skipped,
NoComponent,
Failed,
}

const tsconfig = ttp.loadConfig(path.resolve(__dirname, '../tsconfig.json'));

const prettierConfig = prettier.resolveConfig.sync(process.cwd(), {
config: path.join(__dirname, '../../../prettier.config.js'),
});

async function generateProptypes(
tsFile: string,
jsFile: string,
program: ttp.ts.Program,
): Promise<GenerateResult> {
const proptypes = ttp.parseFromProgram(tsFile, program, {
shouldResolveObject: ({ name }) => {
if (name === 'classes' || name === 'theme' || name.endsWith('Props')) {
return false;
}
return undefined;
},
});

if (proptypes.body.length === 0) {
return GenerateResult.NoComponent;
}

proptypes.body.forEach(component => {
component.types.forEach(prop => {
if (prop.name === 'classes' && prop.jsDoc) {
prop.jsDoc += '\nSee [CSS API](#css) below for more details.';
} else if (prop.name === 'className') {
prop.jsDoc = '@ignore';
} else if (prop.name === 'children' && !prop.jsDoc) {
prop.jsDoc = 'The content of the component.';
}
});
});

const jsContent = await fse.readFile(jsFile, 'utf8');

const result = ttp.inject(proptypes, jsContent, {
removeExistingPropTypes: true,
shouldInclude: ({ prop }) => {
if (prop.name === 'children') {
return true;
}
return undefined;
},
});

if (!result) {
return GenerateResult.Failed;
}

const prettified = prettier.format(result, { ...prettierConfig, filepath: jsFile });
const formatted = fixBabelGeneratorIssues(prettified);
const correctedLineEndings = fixLineEndings(jsContent, formatted);

await fse.writeFile(jsFile, correctedLineEndings);
return GenerateResult.Success;
}

async function run() {
// Matches files where the folder and file both start with uppercase letters
// Example: AppBar/AppBar.d.ts
const files = glob
.sync('+([A-Z])*/+([A-Z])*.d.ts', {
absolute: true,
cwd: path.resolve(__dirname, '../src'),
})
// Filter out files where the directory name and filename doesn't match
// Example: Modal/ModalManager.d.ts
.filter(filePath => {
const folderName = path.basename(path.dirname(filePath));
const fileName = path.basename(filePath, '.d.ts');

return fileName === folderName;
});

const program = ttp.createProgram(files, tsconfig);

const promises = files.map<Promise<GenerateResult>>(async tsFile => {
const jsFile = tsFile.replace('.d.ts', '.js');

if (!ignoreCache && (await fse.stat(jsFile)).mtimeMs > (await fse.stat(tsFile)).mtimeMs) {
// Javascript version is newer, skip file
return GenerateResult.Skipped;
}

return generateProptypes(tsFile, jsFile, program);
});

const results = await Promise.all(promises);

if (verbose) {
files.forEach((file, index) => {
console.log('%s - %s', GenerateResult[results[index]], path.basename(file, '.d.ts'));
});
}

console.log('--- Summary ---');
const groups = _.groupBy(results, x => x);

_.forOwn(groups, (count, key) => {
console.log('%s: %d', GenerateResult[(key as unknown) as GenerateResult], count.length);
});

console.log('Total: %d', results.length);
}

run().catch(error => {
console.error(error);
process.exit(1);
});
Loading