Skip to content

feat(*): add params information to func proptypes where possible #79

Merged
merged 8 commits into from
Jun 19, 2018
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ before_script:

script:
- npm run lint
- npm test

after_script:
- greenkeeper-lockfile-upload
Expand Down
3 changes: 2 additions & 1 deletion gulp-tasks.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,8 @@ function createTasks(packageName, options = {}) {
}))
.pipe(filter(file => !fs.existsSync(
// ignore all files, that already emit d.ts file
path.join(options.publishDir, file.relative).replace(/\.ts$/, '.d.ts')
path.join(process.cwd(), options.publishDir, file.relative)
.replace(/\.tsx?$/, '.d.ts')
)))
.pipe(ts(tsOptions, ts.reporter.nullReporter())) // ignore all errors at compile time
.dts.pipe(gulp.dest(options.publishDir));
Expand Down
2 changes: 2 additions & 0 deletions gulp/component-docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ function componentDocs(libraryName) {
contents: Buffer.from(doc)
}));
} catch (e) {
// eslint-disable-next-line no-console
console.warn(`unable to build docs for ${file.path}`);
// eslint-disable-next-line no-console
console.warn(e);
callback(null);
}
Expand Down
3 changes: 2 additions & 1 deletion gulp/component-typings.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ const getReactComponentDefinitionsContent = require('../typings/index');
/**
* Gulp plugin to generate react typings and root package.json for each component.
*
* @param {String} libraryName Library name, will be used in typescript declarations.
* @returns {Function}
*/
function componentTypings() {
Expand All @@ -18,6 +17,7 @@ function componentTypings() {
const componentName = path.parse(file.path).name;
getReactComponentDefinitionsContent(file.path).then((definitionsContent) => {
if (!definitionsContent) {
// eslint-disable-next-line no-console
console.warn(`Unable to create typings for ${file.path}`);
return callback(null);
}
Expand All @@ -28,6 +28,7 @@ function componentTypings() {
contents: Buffer.from(definitionsContent)
}));
}).catch((e) => {
// eslint-disable-next-line no-console
console.error(e);
});
}
Expand Down
1 change: 1 addition & 0 deletions gulp/library-doc.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ function libraryDoc(libraryName) {
try {
components.push(structureForFile(content, componentName));
} catch (e) {
// eslint-disable-next-line no-console
console.warn(`unable to build docs for ${file.path}`);
}

Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,16 @@
"arui-presets": "4.11.9",
"conventional-changelog-cli": "1.3.14",
"conventional-github-releaser": "2.0.0",
"husky": "0.14.3"
"husky": "0.14.3",
"jest": "^23.1.0"
},
"scripts": {
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
"github-release": "conventional-github-releaser -p angular",
"lint": "eslint ./*.js ./gulp/*.js ./typings/*.js",
"test": "jest",
"postversion": "git push origin master && git push --tags && npm publish",
"precommit": "npm run lint",
"precommit": "npm run lint && npm run test",
"release-patch": "npm version patch -m 'chore(*): patch version'",
"release-minor": "npm version minor -m 'chore(*): minor version'",
"release-major": "npm version major -m 'chore(*): major version'",
Expand Down
36 changes: 36 additions & 0 deletions react-doc/component-prop-types-js-doc-handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const getMemberValuePath = require('react-docgen/dist/utils/getMemberValuePath').default;
const resolveToValue = require('react-docgen/dist/utils/resolveToValue').default;
const getPropertyName = require('react-docgen/dist/utils/getPropertyName').default;
const parseJsDoc = require('react-docgen/dist/utils/parseJsDoc').default;
const recast = require('recast');

const { types: { namedTypes: types } } = recast;
// component-prop-types-js-doc-handler
function componentPropTypesJsDocHandler(documentation, path) {
let propTypesPath = getMemberValuePath(path, 'propTypes');

if (!propTypesPath) {
return;
}
propTypesPath = resolveToValue(propTypesPath);
if (!propTypesPath || !types.ObjectExpression.check(propTypesPath.node)) {
return;
}

propTypesPath.get('properties').each((propertyPath) => {
// we only support documentation of actual properties, not spread
if (types.Property.check(propertyPath.node)) {
const propName = getPropertyName(propertyPath);
const propDescriptor = documentation.getPropDescriptor(propName);
if (!propDescriptor.description || !propDescriptor.type) {
return;
}
const jsDoc = parseJsDoc(propDescriptor.description);
propDescriptor.description = jsDoc.description || propDescriptor.description;
propDescriptor.type.params = jsDoc.params || [];
propDescriptor.type.returns = jsDoc.returns;
}
});
}

module.exports = componentPropTypesJsDocHandler;
9 changes: 6 additions & 3 deletions react-doc/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
const path = require('path');
const reactDocGen = require('react-docgen');
const { createDisplayNameHandler } = require('react-docgen-displayname-handler');
const getSourceFileContent = require('./get-source-file-content');
const createResolver = require('./create-resolver');
const createDisplayNameHandler = require('react-docgen-displayname-handler').createDisplayNameHandler;
const componentPropTypesJsDocHandler = require('./component-prop-types-js-doc-handler');

const documentation = {};
const defaultHandlers = [
Expand All @@ -15,7 +16,8 @@ const defaultHandlers = [
reactDocGen.handlers.componentDocblockHandler,
reactDocGen.handlers.displayNameHandler,
reactDocGen.handlers.componentMethodsHandler,
reactDocGen.handlers.componentMethodsJsDocHandler
reactDocGen.handlers.componentMethodsJsDocHandler,
componentPropTypesJsDocHandler
];

function getReactComponentInfo(filePath, parentPath) {
Expand All @@ -24,7 +26,7 @@ function getReactComponentInfo(filePath, parentPath) {
}

const src = getSourceFileContent(filePath, parentPath);
const content = src.content;
const { content } = src;
filePath = src.filePath;
const info = reactDocGen.parse(
content,
Expand All @@ -45,4 +47,5 @@ function getReactComponentInfo(filePath, parentPath) {
return info;
}


module.exports = getReactComponentInfo;
122 changes: 122 additions & 0 deletions typings/__examples__/a.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/* eslint-disable */
import React from 'react';
import PropTypes from 'prop-types';

/**
* Component description.
*/
export default class A extends React.Component {
static propTypes = {
optionalArray: PropTypes.array,
requiredArray: PropTypes.array.isRequired,
/**
* Prop documentation
*/
optionalBool: PropTypes.bool,
requiredBool: PropTypes.bool.isRequired,
optionalFunc: PropTypes.func,
requiredFunc: PropTypes.func.isRequired,
optionalNumber: PropTypes.number,
requiredNumber: PropTypes.number.isRequired,
optionalObject: PropTypes.object,
requiredObject: PropTypes.object.isRequired,
optionalString: PropTypes.string,
requiredString: PropTypes.string.isRequired,
optionalSymbol: PropTypes.symbol,
requiredSymbol: PropTypes.symbol.isRequired,
optionalNode: PropTypes.node,
requiredNode: PropTypes.node.isRequired,
optionalElement: PropTypes.element,
requiredElement: PropTypes.element.isRequired,
optionalMessage: PropTypes.instanceOf(Message),
requiredMessage: PropTypes.instanceOf(Message).isRequired,
optionalEnum: PropTypes.oneOf(['News', 'Photos']),
requiredEnum: PropTypes.oneOf(['News', 'Photos']).isRequired,
optionalUnion: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
]),
requiredUnion: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
]).isRequired,
optionalArrayOf: PropTypes.arrayOf(PropTypes.number),
requiredArrayOf: PropTypes.arrayOf(PropTypes.number).isRequired,
optionalObjectOf: PropTypes.objectOf(PropTypes.number),
requiredObjectOf: PropTypes.objectOf(PropTypes.number).isRequired,
optionalAny: PropTypes.any,
requiredAny: PropTypes.any.isRequired,
optionalObjectWithShape: PropTypes.shape({
color: PropTypes.string,
/**
* Sub prop documentation
*/
fontSize: PropTypes.number.isRequired,
/**
* @param {string} value
*/
onChange: PropTypes.func,
subShape: PropTypes.shape({
/**
* Even deeper documentation
*/
name: PropTypes.string,
size: PropTypes.number
})
}),
/**
* Callback with documentation
*
* @param {String} stringParam
* @param {number} count
* @param {React.MouseEvent} event
* @param {React.KeyboardEvent} anotherEvent
* @param {HTMLDivElement} element some html element
*
* @returns {string|number}
*/
onClick: PropTypes.func,
onChange: PropTypes.func
};

render() {
return null;
}

privateMethod(name) {
}

/**
* Some description.
*
* @public
*/
publicMethod1() {

}

/**
* Maybe we just forgot to add params?
*
* @public
*/
publicMethodWithouParams() {

}

/**
* Some description.
*
* @public
* @param {string} str1 Some description.
* @param {String} str2 Some description.
* @param {number} num1 Some description.
* @param {Number} num2 Some description.
* @param {Boolean} bool1 Some description.
* @param {bool} bool2 Some description.
* @param {boolean} bool3 Some description.
* @param {string|number} union Some description.
*/
publicWithParams(str1, str2, num1, num2, bool1, bool2, bool3, union) {
}
}
120 changes: 120 additions & 0 deletions typings/__tests__/__snapshots__/index.tests.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`simple component with all prop types 1`] = `
"
import { Component, ReactNode } from 'react';
import * as Type from 'prop-types';

export type AOptionalEnumFieldType = 'News' | 'Photos';
export type ARequiredEnumFieldType = 'News' | 'Photos';
export type AOptionalUnionFieldType = string | number;
export type ARequiredUnionFieldType = string | number;
export type AOptionalObjectOfFieldType = {
readonly [key: string]: number;
};
export type ARequiredObjectOfFieldType = {
readonly [key: string]: number;
};
export type AOptionalObjectWithShapeSubShapeFieldType = {

/**
* Even deeper documentation
*/
readonly name?: string;
readonly size?: number
};
export type AOptionalObjectWithShapeFieldType = {
readonly color?: string;

/**
* Sub prop documentation
*/
readonly fontSize: number;

/**
* @param {string} value
*/
readonly onChange?: Function;
readonly subShape?: AOptionalObjectWithShapeSubShapeFieldType
};
export type AOnClickReturnFieldType = string | number;
export type APublicWithParamsUnionParamFieldType = string | number;


export interface AProps {
readonly optionalArray?: ReadonlyArray<any>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Heymdall
Если все поля readonly и поднята версия typescript - то не грех и generic утилитой Readonly воспользоваться чтоб читаемым было

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Или даже может стоить рискнуть и написать что-нибудь типа
type DeepReadonly = Readonly<{ [P in keyof T]: T[P] extends Array ? ReadonlyArray<DeepReadonly<T[P][0]>> : DeepReadonly<T[P]> }>;
Правда я думаю, что это еще не совсем корректно работает

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Было бы ништяк еслиб это работало
type DeepReadonly = T extends Array ? ReadonlyArray<DeepReadonly<T[0]>> : Readonly<{ [P in keyof T]: DeepReadonly<T[P]> }>

Copy link
Contributor

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Heymdall, я тут подумал, что сам интерфейс компонента лучше не делать readonly. Потому что ты можешь ведь динамически набивать проемы, перед тем как передать их компоненту ( не всегда ты можешь хорошо стипизировать литерал). Поэтому важно чтобы только this.props было deepreadonly.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Фишка в том, что там, где ожидается ридонли можно кинуть обычный, а наоборот - нет.
Ридонли же по сути сабсет обычных типов.
Начал приделывать это изначально именно из-за того что пришлось кастовать ридонли типы из редакс стейта до обычных, когда кидал их в наши компоненты.
Поэтому и захотелось сделать интерфейс компонента ридонли

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А про DeepReadonly согласен, обновил

readonly requiredArray: ReadonlyArray<any>;

/**
* Prop documentation
*/
readonly optionalBool?: boolean;
readonly requiredBool: boolean;
readonly optionalFunc?: Function;
readonly requiredFunc: Function;
readonly optionalNumber?: number;
readonly requiredNumber: number;
readonly optionalObject?: object;
readonly requiredObject: object;
readonly optionalString?: string;
readonly requiredString: string;
readonly optionalSymbol?: Symbol;
readonly requiredSymbol: Symbol;
readonly optionalNode?: ReactNode;
readonly requiredNode: ReactNode;
readonly optionalElement?: ReactNode;
readonly requiredElement: ReactNode;
readonly optionalMessage?: any/* Не нашёлся встроенный тип для типа {\\"name\\":\\"instanceOf\\",\\"value\\":\\"Message\\"}
* https://github.com/alfa-laboratory/library-utils/issues/new
*/;
readonly requiredMessage: any/* Не нашёлся встроенный тип для типа {\\"name\\":\\"instanceOf\\",\\"value\\":\\"Message\\"}
* https://github.com/alfa-laboratory/library-utils/issues/new
*/;
readonly optionalEnum?: AOptionalEnumFieldType;
readonly requiredEnum: ARequiredEnumFieldType;
readonly optionalUnion?: AOptionalUnionFieldType;
readonly requiredUnion: ARequiredUnionFieldType;
readonly optionalArrayOf?: ReadonlyArray<number>;
readonly requiredArrayOf: ReadonlyArray<number>;
readonly optionalObjectOf?: AOptionalObjectOfFieldType;
readonly requiredObjectOf: ARequiredObjectOfFieldType;
readonly optionalAny?: any;
readonly requiredAny: any;
readonly optionalObjectWithShape?: AOptionalObjectWithShapeFieldType;

/**
* Callback with documentation
*/
readonly onClick?: (stringParam?: string, count?: number, event?: React.MouseEvent<any>, anotherEvent?: React.KeyboardEvent<any>, element?: HTMLDivElement) => AOnClickReturnFieldType;
readonly onChange?: Function;

}


export type APropTypes = Record<keyof AProps, Type.Validator<AProps>>;


/**
* Component description.
*/

export default class A extends Component<AProps> {
static propTypes: APropTypes;

/**
* Some description.
*/
publicMethod1(...args: any[]): any;

/**
* Maybe we just forgot to add params?
*/
publicMethodWithouParams(...args: any[]): any;

/**
* Some description.
*/
publicWithParams(str1?: string, str2?: string, num1?: number, num2?: number, bool1?: boolean, bool2?: boolean, bool3?: boolean, union?: APublicWithParamsUnionParamFieldType): void;
}
"
`;
Loading