diff --git a/.babelrc.js b/.babelrc.js index 1ab407db7c2..2f82f74905c 100644 --- a/.babelrc.js +++ b/.babelrc.js @@ -11,10 +11,12 @@ module.exports = { "useBuiltIns": "usage", "modules": process.env.BABEL_MODULES ? process.env.BABEL_MODULES : "commonjs" // babel's default is commonjs }], + "@babel/typescript", "@babel/react" ], "plugins": [ "pegjs-inline-precompile", + "./scripts/babel/proptypes-from-ts-props", "add-module-exports", [ "react-docgen", diff --git a/.gitignore b/.gitignore index 92b2f9a8c6c..76d61771ae6 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,6 @@ tmp/ dist/ lib/ es/ -types/ .idea .vscode/ .DS_Store @@ -19,3 +18,7 @@ types/ npm-debug.log yarn-error.log eui.d.ts + +# typescript output +types/ +eui.d.ts diff --git a/.npmignore b/.npmignore index 5a9c731ccbc..fa38bf66477 100644 --- a/.npmignore +++ b/.npmignore @@ -5,11 +5,13 @@ tmp/ wiki/ generator-eui/ test/ -types/ src-docs/ src-framer/ .nvmrc +# typescript output +types/ + # ignore everything in `scripts` except postinstall.js scripts/!(postinstall.js) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d8bf59ebc4..357a4b679e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ## [`master`](https://github.com/elastic/eui/tree/master) -No public interface changes since `5.2.0`. +- Introduced TypeScript support, converted `EuiSpacer` and `EuiHorizontalRule` ([#1317](https://github.com/elastic/eui/pull/1317)) ## [`5.2.0`](https://github.com/elastic/eui/tree/v5.2.0) diff --git a/generator-eui/component/index.js b/generator-eui/component/index.js index 2fd0afe4ca5..fc9c253a7ff 100644 --- a/generator-eui/component/index.js +++ b/generator-eui/component/index.js @@ -49,11 +49,11 @@ module.exports = class extends Generator { const vars = config.vars = { componentName, cssClassName, - fileName, + fileName: fileName.replace('.ts', ''), }; - const componentPath = config.componentPath = `${path}/${fileName}.js`; - const testPath = config.testPath = `${path}/${fileName}.test.js`; + const componentPath = config.componentPath = `${path}/${fileName}.tsx`; + const testPath = config.testPath = `${path}/${fileName}.test.tsx`; const stylesPath = config.stylesPath = `${path}/_${fileName}.scss`; config.stylesImportPath = `./_${fileName}.scss`; @@ -66,8 +66,8 @@ module.exports = class extends Generator { ); this.fs.copyTpl( - this.templatePath('index.js'), - this.destinationPath(`${path}/index.js`), + this.templatePath('index.ts'), + this.destinationPath(`${path}/index.ts`), vars ); } @@ -75,15 +75,15 @@ module.exports = class extends Generator { // Create component file. this.fs.copyTpl( isStatelessFunction ? - this.templatePath('stateless_function.js') : - this.templatePath('component.js'), + this.templatePath('stateless_function.tsx') : + this.templatePath('component.tsx'), this.destinationPath(componentPath), vars ); // Create component test file. this.fs.copyTpl( - this.templatePath('test.js'), + this.templatePath('test.tsx'), this.destinationPath(testPath), vars ); @@ -110,9 +110,8 @@ module.exports = class extends Generator { end() { const showImportComponentSnippet = () => { const componentName = this.config.vars.componentName; - const componentPath = this.config.componentPath; - this.log(chalk.white(`\n// Export component (e.. from component's index.js).`)); + this.log(chalk.white(`\n// Export component (e.. from component's index.ts).`)); this.log( `${chalk.magenta('export')} {\n` + ` ${componentName},\n` + diff --git a/generator-eui/component/templates/component.js b/generator-eui/component/templates/component.tsx similarity index 57% rename from generator-eui/component/templates/component.js rename to generator-eui/component/templates/component.tsx index 3ea81b7dabc..a3319d0ae26 100644 --- a/generator-eui/component/templates/component.js +++ b/generator-eui/component/templates/component.tsx @@ -1,16 +1,16 @@ import React, { Component, + HTMLAttributes, } from 'react'; -import PropTypes from 'prop-types'; import classNames from 'classnames'; +import { CommonProps } from '../common'; -export class <%= componentName %> extends Component { - static propTypes = { - children: PropTypes.node, - className: PropTypes.string, - } +export type <%= componentName %>Props = HTMLAttributes & CommonProps & { + +}; - constructor(props) { +export class <%= componentName %> extends Component<<%= componentName %>Props> { + constructor(props: <%= componentName %>Props) { super(props); } @@ -18,7 +18,7 @@ export class <%= componentName %> extends Component { const { children, className, - ...rest, + ...rest } = this.props; const classes = classNames( diff --git a/generator-eui/component/templates/index.js b/generator-eui/component/templates/index.ts similarity index 100% rename from generator-eui/component/templates/index.js rename to generator-eui/component/templates/index.ts diff --git a/generator-eui/component/templates/stateless_function.js b/generator-eui/component/templates/stateless_function.js deleted file mode 100644 index 9f5c28cbd53..00000000000 --- a/generator-eui/component/templates/stateless_function.js +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -export const <%= componentName %> = ({ - children, - className, - ...rest, -}) => { - const classes = classNames('<%= cssClassName %>', className); - - return ( -
- {children} -
- ); -}; - -<%= componentName %>.propTypes = { - children: PropTypes.node, - className: PropTypes.string, -}; diff --git a/generator-eui/component/templates/stateless_function.tsx b/generator-eui/component/templates/stateless_function.tsx new file mode 100644 index 00000000000..17a009510b7 --- /dev/null +++ b/generator-eui/component/templates/stateless_function.tsx @@ -0,0 +1,24 @@ +import React, { HTMLAttributes, SFC } from 'react'; +import { CommonProps } from '../common'; +import classNames from 'classnames'; + +export type <%= componentName %>Props = HTMLAttributes & CommonProps & { + +}; + +export const <%= componentName %>: React.SFC<<%= componentName %>Props> = ({ + children, + className, + ...rest +}) => { + const classes = classNames('<%= cssClassName %>', className); + + return ( +
+ {children} +
+ ); +}; diff --git a/generator-eui/component/templates/test.js b/generator-eui/component/templates/test.tsx similarity index 85% rename from generator-eui/component/templates/test.js rename to generator-eui/component/templates/test.tsx index ab9c6b7a939..4f384d6c2d3 100644 --- a/generator-eui/component/templates/test.js +++ b/generator-eui/component/templates/test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { render } from 'enzyme'; -import { requiredProps } from '../../test'; +import { requiredProps } from '../../test/required_props'; import { <%= componentName %> } from './<%= fileName %>'; diff --git a/generator-eui/documentation/index.js b/generator-eui/documentation/index.js index c035b0b83be..e812de48857 100644 --- a/generator-eui/documentation/index.js +++ b/generator-eui/documentation/index.js @@ -86,10 +86,10 @@ module.exports = class extends Generator { const documentationPageDemoPath = config.documentationPageDemoPath - = `${path}/${folderName}/${fileName}.js`; + = `${path}/${folderName}/${fileName}.tsx`; this.fs.copyTpl( - this.templatePath('documentation_page_demo.js'), + this.templatePath('documentation_page_demo.tsx'), this.destinationPath(documentationPageDemoPath), vars ); diff --git a/generator-eui/documentation/templates/documentation_page_demo.js b/generator-eui/documentation/templates/documentation_page_demo.tsx similarity index 72% rename from generator-eui/documentation/templates/documentation_page_demo.js rename to generator-eui/documentation/templates/documentation_page_demo.tsx index 01ed6a405ae..47f4baedc9d 100644 --- a/generator-eui/documentation/templates/documentation_page_demo.js +++ b/generator-eui/documentation/templates/documentation_page_demo.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { <%= componentName %>, -} from '../../../../src/components'; +} from '../../../../src/components/<%= fileName %>'; export default () => ( <<%= componentName %>> diff --git a/package.json b/package.json index c92ccb3aa60..6ce71d64fad 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "lint-es-fix": "eslint --fix --cache --ignore-pattern \"**/*.snap.js\" \"src/**/*.js\" \"src-docs/**/*.js\"", "lint-sass": "sass-lint -v --max-warnings 0", "lint-sass-fix": "sass-lint-auto-fix -c ./.sass-lint-fix.yml", - "lint-ts": "tslint -c ./tslint.yaml -p ./tsconfig.json && tsc -p ./tsconfig.json", + "lint-ts": "tslint -c ./tslint.yaml -p ./tsconfig.json && tsc -p ./tsconfig.json --noEmit", "lint-ts-fix": "tslint -c ./tslint.yaml -p ./tsconfig.json --fix", "lint-framer": "tslint -c ./tslint.yaml -p ./src-framer/tsconfig.json", "lint-framer-fix": "tslint -c ./tslint.yaml -p ./src-framer/tsconfig.json --fix", @@ -68,8 +68,12 @@ "@babel/polyfill": "^7.0.0", "@babel/preset-env": "^7.1.0", "@babel/preset-react": "^7.0.0", + "@babel/preset-typescript": "^7.1.0", "@elastic/eslint-config-kibana": "^0.15.0", + "@types/classnames": "^2.2.6", "@types/enzyme": "^3.1.13", + "@types/jest": "^23.3.9", + "@types/lodash": "^4.14.116", "@types/react": "^16.0.31", "@types/react-virtualized": "^9.18.6", "autoprefixer": "^7.1.5", @@ -81,6 +85,7 @@ "babel-plugin-inline-react-svg": "^1.0.1", "babel-plugin-pegjs-inline-precompile": "^0.1.0", "babel-plugin-react-docgen": "^2.0.0", + "babel-template": "^6.26.0", "chai": "^4.1.2", "chai-webdriverio": "^0.4.3", "chalk": "^2.4.1", @@ -105,6 +110,7 @@ "eslint-plugin-prettier": "^2.6.0", "eslint-plugin-react": "^7.4.0", "file-loader": "^1.1.11", + "fork-ts-checker-webpack-plugin": "^0.4.4", "geckodriver": "^1.11.0", "glob": "^7.1.2", "html-webpack-plugin": "^3.2.0", diff --git a/scripts/babel/proptypes-from-ts-props/index.js b/scripts/babel/proptypes-from-ts-props/index.js new file mode 100644 index 00000000000..5e6b1e1d32b --- /dev/null +++ b/scripts/babel/proptypes-from-ts-props/index.js @@ -0,0 +1,928 @@ +/* eslint-disable new-cap */ + +const fs = require('fs'); +const path = require('path'); +const babelTemplate = require('babel-template'); +const babelCore = require('@babel/core'); + +/** + * Converts an Array type to PropTypes.arrayOf(X) + * @param node + * @param state + * @returns AST node representing matching proptypes + */ +function resolveArrayToPropTypes(node, state) { + const types = state.get('types'); + + const { typeParameters } = node; + + if (typeParameters == null) { + // Array without any type information + // PropTypes.array + return buildPropTypePrimitiveExpression(types, 'array'); + } else { + // Array with typed elements + // PropTypes.arrayOf() + // Array type only has one type argument + const { params: [arrayType] } = typeParameters; + return types.callExpression( + types.memberExpression( + types.identifier('PropTypes'), + types.identifier('arrayOf') + ), + [ + getPropTypesForNode(arrayType, false, state) + ] + ); + } +} + +/** + * Converts an X[] type to PropTypes.arrayOf(X) + * @param node + * @param state + * @returns AST node representing matching proptypes + */ +function resolveArrayTypeToPropTypes(node, state) { + const types = state.get('types'); + + const { elementType } = node; + + if (elementType == null) { + // Array without any type information + // PropTypes.array + return buildPropTypePrimitiveExpression(types, 'array'); + } else { + // Array with typed elements + // PropTypes.arrayOf() + // Array type only has one type argument + return types.callExpression( + types.memberExpression( + types.identifier('PropTypes'), + types.identifier('arrayOf') + ), + [ + getPropTypesForNode(elementType, false, state) + ] + ); + } +} + +/** + * Resolves the node's identifier to its proptypes. + * Responsible for resolving + * - React.* (SFC, ReactNode, etc) + * - Arrays + * - defined types/interfaces (found during initial program body parsing) + * Returns `null` for unresolvable types + * @param node + * @param state + * @returns AST | null + */ +function resolveIdentifierToPropTypes(node, state) { + const typeDefinitions = state.get('typeDefinitions'); + const types = state.get('types'); + + let identifier; + switch (node.type) { + case 'TSTypeReference': + identifier = node.typeName; + break; + + case 'Identifier': + identifier = node; + break; + } + + // resolve React.* identifiers + if (identifier.type === 'TSQualifiedName' && identifier.left.name === 'React') { + return resolveIdentifierToPropTypes(identifier.right, state); + } + + // React Component + switch (identifier.name) { + // PropTypes.element + case 'Component': + case 'ReactElement': + case 'ComponentClass': + case 'SFC': + case 'StatelessComponent': + return types.memberExpression( + types.identifier('PropTypes'), + types.identifier('element') + ); + + // PropTypes.node + case 'ReactNode': + return types.memberExpression( + types.identifier('PropTypes'), + types.identifier('node') + ); + } + + if (identifier.name === 'Array') return resolveArrayToPropTypes(node, state); + + // Lookup this identifier from types/interfaces defined in code + const identifierDefinition = typeDefinitions[identifier.name]; + + if (identifierDefinition) { + return getPropTypesForNode(identifierDefinition, true, state); + } else { + return null; + } +} + +/** + * Small DRY abstraction to return the AST of PropTypes.${typeName} + * @param types + * @param typeName + * @returns AST + */ +function buildPropTypePrimitiveExpression(types, typeName) { + return types.memberExpression( + types.identifier('PropTypes'), + types.identifier(typeName) + ); +} + +/** + * Heavy lifter to generate the proptype AST for a node. Initially called by `processComponentDeclaration`, + * its return value is set as the component's `propTypes` value. This function calls itself recursively to translate + * the whole type/interface AST into prop types. + * @param node + * @param optional + * @param state + * @returns AST | null + */ +function getPropTypesForNode(node, optional, state) { + const types = state.get('types'); + + if (node.isAlreadyResolved === true) return node; + + let propType; + switch(node.type) { + // a type value by identifier + case 'TSTypeReference': + propType = resolveIdentifierToPropTypes(node, state); + break; + + // a type annotation on a node + // Array + // ^^^ Foo + case 'TSTypeAnnotation': + propType = getPropTypesForNode(node.typeAnnotation, true, state); + break; + + // translates intersections (Foo & Bar & Baz) to a shape with the types' members (Foo, Bar, Baz) merged together + case 'TSIntersectionType': + // merge the resolved proptypes for each intersection member into one object, mergedProperties + const mergedProperties = node.types.reduce( + (mergedProperties, node) => { + const nodePropTypes = getPropTypesForNode(node, true, state); + + // if this propType is PropTypes.any there is nothing to do here + if ( + types.isMemberExpression(nodePropTypes) && + nodePropTypes.object.name === 'PropTypes' && + nodePropTypes.property.name === 'any' + ) { + return mergedProperties; + } + + // validate that this resulted in a shape, otherwise we don't know how to extract/merge the values + if ( + !types.isCallExpression(nodePropTypes) || + !types.isMemberExpression(nodePropTypes.callee) || + nodePropTypes.callee.object.name !== 'PropTypes' || + nodePropTypes.callee.property.name !== 'shape' + ) { + throw new Error('Cannot process an encountered type intersection'); + } + + // iterate over this type's members, adding them (and their comments) to `mergedProperties` + const typeProperties = nodePropTypes.arguments[0].properties; // properties on the ObjectExpression passed to PropTypes.shape() + for (let i = 0; i < typeProperties.length; i++) { + const typeProperty = typeProperties[i]; + // this member may be duplicated between two shapes, e.g. Foo = { buzz: string } & Bar = { buzz: string } + // either or both may have leading comments and we want to forward all comments to the generated prop type + const leadingComments = [ + ...(typeProperty.leadingComments || []), + ...((mergedProperties[typeProperty.key.name] ? mergedProperties[typeProperty.key.name].leadingComments : null) || []), + ]; + mergedProperties[typeProperty.key.name] = typeProperty.value; + mergedProperties[typeProperty.key.name].leadingComments = leadingComments; + } + + return mergedProperties; + }, + {} + ); + + const propertyKeys = Object.keys(mergedProperties); + // if there is one or more members on `mergedProperties` then use PropTypes.shape, + // otherwise none of the types were resolvable and fallback to PropTypes.any + if (propertyKeys.length > 0) { + // At least one type/interface was resolved to proptypes + propType = types.callExpression( + types.memberExpression( + types.identifier('PropTypes'), + types.identifier('shape') + ), + [ + types.objectExpression(propertyKeys.map( + propKey => { + const objectProperty = types.objectProperty( + types.identifier(propKey), + mergedProperties[propKey] + ); + + objectProperty.leadingComments = mergedProperties[propKey].leadingComments; + mergedProperties[propKey].leadingComments = null; + + return objectProperty; + } + )) + ] + ); + } else { + // None of the types were resolveable, return with PropTypes.any + propType = types.memberExpression( + types.identifier('PropTypes'), + types.identifier('any') + ); + } + break; + + // translate an interface definition into a PropTypes.shape + case 'TSInterfaceBody': + propType = types.callExpression( + types.memberExpression( + types.identifier('PropTypes'), + types.identifier('shape') + ), + [ + types.objectExpression( + node.body.map(property => { + const objectProperty = types.objectProperty( + types.identifier(property.key.name || `"${property.key.value}"`), + getPropTypesForNode(property.typeAnnotation, property.optional, state) + ); + if (property.leadingComments != null) { + objectProperty.leadingComments = property.leadingComments.map(({ type, value }) => ({ type, value })); + } + return objectProperty; + }) + ) + ] + ); + break; + + // resolve a type operator (keyword) that operates on a value + // currently only supporting `keyof typeof [object variable]` + case 'TSTypeOperator': + if (node.operator === 'keyof' && node.typeAnnotation.type === 'TSTypeQuery') { + const typeDefinitions = state.get('typeDefinitions'); + const typeDefinition = typeDefinitions[node.typeAnnotation.exprName.name]; + if (typeDefinition != null) { + propType = getPropTypesForNode(typeDefinition, true, state); + } + } + break; + + // invoked only by `keyof typeof` TSTypeOperator, safe to form PropTypes.oneOf(Object.keys(variable)) + case 'ObjectExpression': + propType = types.callExpression( + types.memberExpression( + types.identifier('PropTypes'), + types.identifier('oneOf') + ), + [ + types.arrayExpression( + node.properties.map(property => types.stringLiteral(property.key.name || property.key.name || property.key.value)) + ) + ] + ); + break; + + // translate a type definition into a PropTypes.shape + case 'TSTypeLiteral': + propType = types.callExpression( + types.memberExpression( + types.identifier('PropTypes'), + types.identifier('shape') + ), + [ + types.objectExpression( + node.members.map(property => { + const objectProperty = types.objectProperty( + types.identifier(property.key.name || `"${property.key.value}"`), + getPropTypesForNode(property.typeAnnotation, property.optional, state) + ); + if (property.leadingComments != null) { + objectProperty.leadingComments = property.leadingComments.map(({ type, value }) => ({ type, value })); + } + return objectProperty; + }) + ) + ] + ); + break; + + // translate a union (Foo | Bar | Baz) into PropTypes.oneOf or PropTypes.oneOfType, depending on + // the kind of members in the union (no literal values, all literals, or mixed) + // literal values are extracted into a `oneOf`, if all members are literals this oneOf is used + // otherwise `oneOfType` is used - if there are any literal values it contains the literals' `oneOf` + case 'TSUnionType': + const tsUnionTypes = node.types.map(node => getPropTypesForNode(node, false, state)); + + // `tsUnionTypes` could be: + // 1. all non-literal values (string | number) + // 2. all literal values ("foo" | "bar") + // 3. a mix of value types ("foo" | number) + // this reduce finds any literal values and groups them into a oneOf node + + const reducedUnionTypes = tsUnionTypes.reduce( + (foundTypes, tsUnionType) => { + if (types.isLiteral(tsUnionType)) { + if (foundTypes.oneOfPropType == null) { + foundTypes.oneOfPropType = types.arrayExpression([]); + foundTypes.unionTypes.push( + types.callExpression( + types.memberExpression( + types.identifier('PropTypes'), + types.identifier('oneOf') + ), + [foundTypes.oneOfPropType] + ) + ); + } + + // this is a literal value, move to the oneOfPropType argument + foundTypes.oneOfPropType.elements.push(tsUnionType); + } else { + // this is a non-literal type + foundTypes.unionTypes.push(tsUnionType); + } + + return foundTypes; + }, + { + unionTypes: [], + oneOfPropType: null, + } + ); + + // if there is only one member on the reduced union types, _and_a oneOf proptype was created, + // then that oneOf proptype is the one member on union types, and can be be extracted out + // e.g. + // PropTypes.oneOf([PropTypes.oneOf(['red', 'blue'])]) + // -> + // PropTypes.oneOf(['red', 'blue']) + if (reducedUnionTypes.unionTypes.length === 1 && reducedUnionTypes.oneOfPropType != null) { + // the only proptype is a `oneOf`, use only that + propType = reducedUnionTypes.unionTypes[0]; + } else { + propType = types.callExpression( + types.memberExpression( + types.identifier('PropTypes'), + types.identifier('oneOfType'), + ), + [ + types.arrayExpression( + reducedUnionTypes.unionTypes + ) + ] + ); + } + break; + + // translate enum to PropTypes.oneOf + case 'TSEnumDeclaration': + const memberTypes = node.members.map(member => getPropTypesForNode(member, true, state)); + propType = types.callExpression( + types.memberExpression( + types.identifier('PropTypes'), + types.identifier('oneOf'), + ), + [ + types.arrayExpression( + memberTypes + ) + ] + ); + break; + + // an enum member is a simple wrapper around a type definition + case 'TSEnumMember': + propType = getPropTypesForNode(node.initializer, optional, state); + break; + + // translate `string` to `PropTypes.string` + case 'TSStringKeyword': + propType = buildPropTypePrimitiveExpression(types, 'string'); + break; + + // translate `number` to `PropTypes.number` + case 'TSNumberKeyword': + propType = buildPropTypePrimitiveExpression(types, 'number'); + break; + + // translate `boolean` to `PropTypes.bool` + case 'TSBooleanKeyword': + propType = buildPropTypePrimitiveExpression(types, 'bool'); + break; + + // translate any function type to `PropTypes.func` + case 'TSFunctionType': + propType = buildPropTypePrimitiveExpression(types, 'func'); + break; + + // translate an array type, e.g. Foo[] + case 'TSArrayType': + propType = resolveArrayTypeToPropTypes(node, state); + break; + + // parenthesized type is a small wrapper around another type definition + // e.g. (() => void)[] + // ^^^^^^^^^^^^ wrapping the TSFunctionType `() => void` + case 'TSParenthesizedType': + propType = getPropTypesForNode(node.typeAnnotation, optional, state); + optional = true; // handling `optional` has been delegated to the above call + break; + + // literal type wraps a literal value + case 'TSLiteralType': + propType = getPropTypesForNode(node.literal, true, state); + optional = true; // cannot call `.isRequired` on a literal + break; + + case 'StringLiteral': + propType = types.stringLiteral(node.value); + optional = true; // cannot call `.isRequired` on a string literal + break; + + case 'NumericLiteral': + propType = types.numericLiteral(node.value); + optional = true; // cannot call `.isRequired` on a number literal + break; + + case 'BooleanLiteral': + propType = types.booleanLiteral(node.value); + optional = true; // cannot call `.isRequired` on a boolean literal + break; + + // very helpful debugging code + // default: + // debugger; + // throw new Error(`Could not generate prop types for node type ${node.type}`); + } + + // if the node was unable to be translated to a prop type, fallback to PropTypes.any + if (propType == null) { + propType = types.memberExpression( + types.identifier('PropTypes'), + types.identifier('any') + ); + } + + if (optional) { + return propType; + } else { + return types.memberExpression( + propType, + types.identifier('isRequired') + ); + } +} + +// typeDefinitionExtractors is a mapping of [ast_node_type: func] which is used to find type definitions +// these definitions come from four sources: +// - import statements +// - interface declaration +// - type declaration +// - enum declaration +const typeDefinitionExtractors = { + /** + * Looks at the named imports from _relative files_ (nothing from node_modues) + * The imported source is then loaded & parsed, in the same way recursively resolving that files' typescript definitions + * After parsing, the imported definitions are extracted, pre-resolved, and marked with `isAlreadyResolved = true` + * @param node + * @param extractionOptions + * @returns Array + */ + ImportDeclaration: (node, extractionOptions) => { + const { fs, sourceFilename, parse, state } = extractionOptions; + const importPath = node.source.value; + const isPathRelative = /^\.{1,2}\//.test(importPath); + + // only process relative imports for typescript definitions (avoid node_modules) + if (isPathRelative) { + + // find the variable names being imported + const importedTypeNames = node.specifiers.map(specifier => { + switch (specifier.type) { + case 'ImportSpecifier': + return specifier.imported.name; + + // default: + // throw new Error(`Unable to process import specifier type ${specifier.type}`); + } + }); + + // find the file pointed to by `importPath` + let resolvedPath = path.resolve(path.dirname(sourceFilename), importPath); + if (fs.existsSync(resolvedPath)) { + // imported path exists, it might be a directory or a file + const isDirectory = fs.statSync(resolvedPath).isDirectory(); + if (isDirectory) { + // imported path is a directory, try resolving to the /index.ts + resolvedPath = `${resolvedPath}/index.ts`; + if (!fs.existsSync(resolvedPath)) { + // no index file to resolve to, return with no found type definitions + return []; + } + } + } else if (fs.existsSync(`${resolvedPath}.ts`)) { + resolvedPath += '.ts'; + } else if (fs.existsSync(`${resolvedPath}.tsx`)) { + resolvedPath += '.tsx'; + } else if (!fs.existsSync(resolvedPath)) { + // could not resolve this file, skip out + return []; + } + + // load & parse the imported file + const ast = parse(fs.readFileSync(resolvedPath).toString()); + + // extract any type definitions in the imported file + const definitions = []; + const subExtractionOptions = { + ...extractionOptions, + sourceFilename: resolvedPath, + }; + for (let i = 0; i < ast.program.body.length; i++) { + const bodyNode = ast.program.body[i]; + Array.prototype.push.apply(definitions, extractTypeDefinition(bodyNode, subExtractionOptions) || []); + } + + // temporarily override typeDefinitions so variable scope doesn't bleed between files + const _typeDefinitions = state.get('typeDefinitions'); + state.set( + 'typeDefinitions', + definitions.reduce( + (typeDefinitions, definition) => { + if (definition) { + typeDefinitions[definition.name] = definition.definition; + } + + return typeDefinitions; + }, + {} + ) + ); + + // for each importedTypeName, fully resolve the type information + const importedDefinitions = definitions.reduce( + (importedDefinitions, { name, definition }) => { + if (importedTypeNames.includes(name)) { + // this type declaration is imported by the parent script + const propTypes = getPropTypesForNode(definition, true, state); + propTypes.isAlreadyResolved = true; // when getPropTypesForNode is called on this node later, tell it to skip processing + importedDefinitions.push({ name, definition: propTypes }); + } + + return importedDefinitions; + }, + [] + ); + + // reset typeDefinitions and continue processing the original file + state.set('typeDefinitions', _typeDefinitions); + + return importedDefinitions; + } + + return []; + }, + + /** + * Associates this interfaces's identifier name with its definition + * @param node + * @returns Array + */ + TSInterfaceDeclaration: node => { + const { id, body } = node; + + if (id.type !== 'Identifier') { + throw new Error(`TSInterfaceDeclaration typeDefinitionExtract could not understand id type ${id.type}`); + } + + return [{ name: id.name, definition: body }]; + }, + + /** + * Associates this type's identifier name with its definition + * @param node + * @returns Array + */ + TSTypeAliasDeclaration: node => { + const { id, typeAnnotation } = node; + + if (id.type !== 'Identifier') { + throw new Error(`TSTypeAliasDeclaraction typeDefinitionExtract could not understand id type ${id.type}`); + } + + return [{ name: id.name, definition: typeAnnotation }]; + }, + + /** + * Associates this enum's identifier name with its definition + * @param node + * @returns Array + */ + TSEnumDeclaration: node => { + const { id } = node; + + if (id.type !== 'Identifier') { + throw new Error(`TSEnumDeclaration typeDefinitionExtract could not understand id type ${id.type}`); + } + + return [{ name: id.name, definition: node }]; + }, + + /** + * Tracks variable declarations as object definitions are used by `keyof typeof [object variable] + * @param node + * @returns Array + */ + VariableDeclaration: node => { + return node.declarations.reduce( + (declarations, declaration) => { + if (declaration.init.type === 'ObjectExpression') { + declarations.push({ name: declaration.id.name, definition: declaration.init }); + } + return declarations; + }, + [] + ); + }, + + ExportNamedDeclaration: node => extractTypeDefinition(node.declaration), +}; +function extractTypeDefinition(node, opts) { + if (node == null) { + return null; + } + return typeDefinitionExtractors.hasOwnProperty(node.type) ? typeDefinitionExtractors[node.type](node, opts) : null; +} + +/** + * given the node path, walks up the path's scope looking for the variable binding `variableName` + * @param path + * @param variableName + * @returns nodePath | null + */ +function getVariableBinding(path, variableName) { + while (path) { + if (path.scope.bindings[variableName]) return path.scope.bindings[variableName]; + path = path.parentPath; + } + return null; +} + +/** + * Takes an AST of PropTypes.* and walks down until an ObjectExpression is found. + * Required as a component's `propTypes` definition itself is an object (ObjectExpression) + * and the AST node passed here is an actual PropType call itself; without this method the result would be + * FooComponent.propTypes = PropTypes.shape({ ... }); + * which getPropTypesNodeFromAST converts to + * FooComponent.propTypes = { ... }; + * @param node + * @param types + * @returns {*} + */ +function getPropTypesNodeFromAST(node, types) { + switch (node.type) { + case 'MemberExpression': + return getPropTypesNodeFromAST(node.object, types); + + case 'CallExpression': + return getPropTypesNodeFromAST(node.arguments[0], types); + } + return node; +} + +// Used to generate AST for assigning component's propTypes +const buildPropTypes = babelTemplate('COMPONENT_NAME.propTypes = PROP_TYPES'); + +/** + * Called with a type definition and a React component node path, `processComponentDeclaration` translates that definiton + * to an AST of PropType.* calls and attaches those prop types to the component. + * @param typeDefinition + * @param path + * @param state + */ +function processComponentDeclaration(typeDefinition, path, state) { + const types = state.get('types'); + + const propTypesAST = getPropTypesForNode(typeDefinition, false, state); + + // if the resulting proptype is PropTypes.any don't bother setting the proptypes + if (types.isMemberExpression(propTypesAST.object) && propTypesAST.object.property.name === 'any') return; + + const propTypes = getPropTypesNodeFromAST( + // `getPropTypesForNode` returns a PropTypes.shape representing the top-level object, we need to + // reach into the shape call expression and use the object literal directly + propTypesAST, + types + ); + + const ancestry = path.getAncestry(); + + // find the ancestor who lives in the nearest block + let blockChildAncestor = ancestry[0]; + for (let i = 1; i < ancestry.length; i++) { + const ancestor = ancestry[i]; + if (ancestor.isBlockParent()) { + // stop here, we want to insert the propTypes assignment into this block, + // immediately after the already found `blockChildAncestor` + break; + } + blockChildAncestor = ancestor; + } + + blockChildAncestor.insertAfter([ + buildPropTypes({ + COMPONENT_NAME: types.identifier(path.node.id.name), + PROP_TYPES: propTypes, + }) + ]); + + // import PropTypes library if it isn't already + const proptypesBinding = getVariableBinding(path, 'PropTypes'); + if (proptypesBinding == null) { + const reactBinding = getVariableBinding(path, 'React'); + if (reactBinding == null) { + throw new Error('Cannot import PropTypes module, no React namespace import found'); + } + const reactImportDeclaration = reactBinding.path.getAncestry()[1]; + reactImportDeclaration.insertAfter( + types.importDeclaration( + [types.importDefaultSpecifier(types.identifier('PropTypes'))], + types.stringLiteral('prop-types') + ) + ); + } +} + +module.exports = function propTypesFromTypeScript({ types }) { + return { + visitor: { + /** + * Visit the program path to setup the processing initial state. + * @param programPath + * @param state + */ + Program: function visitProgram(programPath, state) { + // only process typescript files + if (path.extname(state.file.opts.filename) !== '.ts' && path.extname(state.file.opts.filename) !== '.tsx') return; + + // Extract any of the imported variables from 'react' (SFC, ReactNode, etc) + // we do this here instead of resolving when the imported values are used + // as the babel typescript preset strips type-only imports before babel visits their usages + const importsFromReact = new Set(); + programPath.traverse( + { + ImportDeclaration: ({ node }) => { + if (node.source.value === 'react') { + node.specifiers.forEach(specifier => { + if (specifier.type === 'ImportSpecifier') { + importsFromReact.add(specifier.local.name); + } + }); + } + } + }, + state + ); + state.set('importsFromReact', importsFromReact); + + const { opts = {} } = state; + const typeDefinitions = {}; + state.set('typeDefinitions', typeDefinitions); + state.set('types', types); + + // extraction options are used to further resolve types imported from other files + const extractionOptions = { + state, + sourceFilename: path.resolve(process.cwd(), this.file.opts.filename), + fs: opts.fs || fs, + parse: code => babelCore.parse(code, state.file.opts), + }; + + // collect named TS type definitions for later reference + for (let i = 0; i < programPath.node.body.length; i++) { + const bodyNode = programPath.node.body[i]; + + const extractedDefinitions = extractTypeDefinition(bodyNode, extractionOptions) || []; + + for (let i = 0; i < extractedDefinitions.length; i++) { + const typeDefinition = extractedDefinitions[i]; + if (typeDefinition) { + typeDefinitions[typeDefinition.name] = typeDefinition.definition; + } + } + } + }, + + /** + * Visit class declarations and check to see if it extends React.Component + * If so process the definition and add generate the component's propTypes. + * @param nodePath + * @param state + */ + ClassDeclaration: function visitClassDeclaration(nodePath, state) { + // only process typescript files + if (path.extname(state.file.opts.filename) !== '.ts' && path.extname(state.file.opts.filename) !== '.tsx') return; + + const types = state.get('types'); + + if (nodePath.node.superClass != null) { + let isReactComponent = false; + + if (types.isMemberExpression(nodePath.node.superClass)) { + const objectName = nodePath.node.superClass.object.name; + const propertyName = nodePath.node.superClass.property.name; + if (objectName === 'React' && (propertyName === 'Component' || propertyName === 'PureComponent')) { + isReactComponent = true; + } + } else if (types.isIdentifier(nodePath.node.superClass)) { + const identifierName = nodePath.node.superClass.name; + if (identifierName === 'Component' || identifierName === 'PureComponent') { + if (state.get('importsFromReact').has(identifierName)) { + isReactComponent = true; + } + } + } + + if (isReactComponent && nodePath.node.superTypeParameters != null) { + processComponentDeclaration(nodePath.node.superTypeParameters.params[0], nodePath, state); + + // babel-plugin-react-docgen passes `this.file.code` to react-docgen + // instead of using the modified AST; to expose our changes to react-docgen + // they need to be rendered to a string + this.file.code = babelCore.transformFromAst(this.file.ast).code; + } + } + }, + + /** + * Visit class declarations and check to see if it they are annotated as an SFC + * If so process the definition and add generate the component's propTypes. + * @param nodePath + * @param state + */ + VariableDeclarator: function visitVariableDeclarator(nodePath, state) { + // only process typescript files + if (path.extname(state.file.opts.filename) !== '.ts' && path.extname(state.file.opts.filename) !== '.tsx') return; + + const variableDeclarator = nodePath.node; + const { id } = variableDeclarator; + const idTypeAnnotation = id.typeAnnotation; + + if (idTypeAnnotation) { + let fileCodeNeedsUpdating = false; + + if (idTypeAnnotation.typeAnnotation.typeName.type === 'TSQualifiedName') { + const { left, right } = idTypeAnnotation.typeAnnotation.typeName; + + if (left.name === 'React') { + if (right.name === 'SFC') { + processComponentDeclaration(idTypeAnnotation.typeAnnotation.typeParameters.params[0], nodePath, state); + fileCodeNeedsUpdating = true; + } else { + throw new Error(`Cannot process annotation id React.${right.name}`); + } + } + } else if (idTypeAnnotation.typeAnnotation.typeName.type === 'Identifier') { + if (idTypeAnnotation.typeAnnotation.typeName.name === 'SFC') { + if (state.get('importsFromReact').has('SFC')) { + processComponentDeclaration(idTypeAnnotation.typeAnnotation.typeParameters.params[0], nodePath, state); + fileCodeNeedsUpdating = true; + } + } + } else { + throw new Error('Cannot process annotation type of', idTypeAnnotation.typeAnnotation.id.type); + } + + if (fileCodeNeedsUpdating) { + // babel-plugin-react-docgen passes `this.file.code` to react-docgen + // instead of using the modified AST; to expose our changes to react-docgen + // they need to be rendered to a string + this.file.code = babelCore.transformFromAst(this.file.ast).code; + } + } + }, + }, + }; +}; diff --git a/scripts/babel/proptypes-from-ts-props/index.test.js b/scripts/babel/proptypes-from-ts-props/index.test.js new file mode 100644 index 00000000000..294f61e16d2 --- /dev/null +++ b/scripts/babel/proptypes-from-ts-props/index.test.js @@ -0,0 +1,2075 @@ +const path = require('path'); +const { transform } = require('@babel/core'); +const babelOptions = { + babelrc: false, + presets: [ + '@babel/typescript', + ], + plugins: [ + './scripts/babel/proptypes-from-ts-props', + ], + filename: 'somefile.tsx', +}; + +describe('proptypes-from-ts-props', () => { + + describe('proptype generation', () => { + + describe('basic generation', () => { + + it('imports PropTypes and creates an empty propTypes object on the component', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps {} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = {};`); + }); + + it('creates the propTypes assignment at the nearest block', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps {} +(function() { + if (true) { + const FooComponent: React.SFC = () => { + return (
Hello World
); + } + } +})();`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +(function () { + if (true) { + const FooComponent = () => { + return
Hello World
; + }; + + FooComponent.propTypes = {}; + } +})();`); + }); + + }); + + describe('primitive propTypes', () => { + + it('understands string props', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps {bar: string} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + bar: PropTypes.string.isRequired +};`); + }); + + it('understands number props', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps {bar: number} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + bar: PropTypes.number.isRequired +};`); + }); + + it('understands boolean props', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps {bar: boolean} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + bar: PropTypes.bool.isRequired +};`); + }); + + it('understands function props', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps { + bar: () => void +} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + bar: PropTypes.func.isRequired +};`); + }); + + it('understands optional props', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps {bar?: number} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + bar: PropTypes.number +};`); + }); + + it('understands mixed props', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps { + bar1: string, + bar2?: number, + bar3: (x: number, y: number) => string, + bar4?: () => void, + bar5: boolean +} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + bar1: PropTypes.string.isRequired, + bar2: PropTypes.number, + bar3: PropTypes.func.isRequired, + bar4: PropTypes.func, + bar5: PropTypes.bool.isRequired +};`); + }); + + }); + + describe('enum / oneOf propTypes', () => { + + describe('union type', () => { + + it('understands a union of strings', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps {flower: 'daisy' | 'daffodil' | 'dandelion'} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + flower: PropTypes.oneOf(["daisy", "daffodil", "dandelion"]).isRequired +};`); + }); + + it('understands a union of numbers', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps {prime: 2 | 3 | 5 | 7 | 11 | 13} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + prime: PropTypes.oneOf([2, 3, 5, 7, 11, 13]).isRequired +};`); + }); + + it('understands a union of booleans', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps {visible: true | false} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + visible: PropTypes.oneOf([true, false]).isRequired +};`); + }); + + it('understands a mix of primitives', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps {bool: true | false | 'FileNotFound'} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + bool: PropTypes.oneOf([true, false, "FileNotFound"]).isRequired +};`); + }); + + it('understands optional unions', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps {bar?: 'hello' | 'world'} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + bar: PropTypes.oneOf(["hello", "world"]) +};`); + }); + + }); + + describe('enum', () => { + + it('understands enum of strings', () => { + const result = transform( + ` +import React from 'react'; +enum Foo { + bar = 'BAR', + baz = 'BAZ', +}; +interface IFooProps {foo: Foo} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; +var Foo; + +(function (Foo) { + Foo["bar"] = "BAR"; + Foo["baz"] = "BAZ"; +})(Foo || (Foo = {})); + +; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + foo: PropTypes.oneOf(["BAR", "BAZ"]).isRequired +};`); + }); + + it('understands enum of numbers', () => { + const result = transform( + ` +import React from 'react'; +enum Foo { + bar = 3, + baz = 54, +}; +interface IFooProps {foo: Foo} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; +var Foo; + +(function (Foo) { + Foo[Foo["bar"] = 3] = "bar"; + Foo[Foo["baz"] = 54] = "baz"; +})(Foo || (Foo = {})); + +; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + foo: PropTypes.oneOf([3, 54]).isRequired +};`); + }); + + it('understands a mix of primitives', () => { + const result = transform( + ` +import React from 'react'; +enum Foo { + bar = 'BAR', + baz = 5, + buzz = false, +}; +interface IFooProps {foo: Foo} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; +var Foo; + +(function (Foo) { + Foo["bar"] = "BAR"; + Foo[Foo["baz"] = 5] = "baz"; + Foo[Foo["buzz"] = false] = "buzz"; +})(Foo || (Foo = {})); + +; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + foo: PropTypes.oneOf(["BAR", 5, false]).isRequired +};`); + }); + + it('understands optional enums', () => { + const result = transform( + ` +import React from 'react'; +enum Foo { + bar = 'BAR', + baz = 'BAZ', +}; +interface IFooProps {foo?: Foo} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; +var Foo; + +(function (Foo) { + Foo["bar"] = "BAR"; + Foo["baz"] = "BAZ"; +})(Foo || (Foo = {})); + +; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + foo: PropTypes.oneOf(["BAR", "BAZ"]) +};`); + }); + + }); + + describe('keyof typeof', () => { + + it('understands keyof typeof', () => { + const result = transform( + ` +import React from 'react'; +const FooMap = { + foo: 'bar', + fizz: 'buzz', +}; +interface IFooProps {foo: keyof typeof FooMap} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; +const FooMap = { + foo: 'bar', + fizz: 'buzz' +}; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + foo: PropTypes.oneOf(["foo", "fizz"]).isRequired +};`); + }); + + }); + + }); + + describe('object / shape propTypes', () => { + + it('understands an object of primitive values', () => { + const result = transform( + ` +import React from 'react'; +interface iFoo {name: string, age: number} +interface IFooProps {person: iFoo} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + person: PropTypes.shape({ + name: PropTypes.string.isRequired, + age: PropTypes.number.isRequired + }).isRequired +};`); + }); + + it('understands an object of object values', () => { + const result = transform( + ` +import React from 'react'; +interface iBar {name: string} +interface iFoo {name: string, age: number} +interface iFizz {bar: iBar, foo?: iFoo} +interface IFooProps {fizz: iFizz} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + fizz: PropTypes.shape({ + bar: PropTypes.shape({ + name: PropTypes.string.isRequired + }).isRequired, + foo: PropTypes.shape({ + name: PropTypes.string.isRequired, + age: PropTypes.number.isRequired + }) + }).isRequired +};`); + }); + + }); + + describe('React component & element propTypes', () => { + + describe('element propType', () => { + + it('understands React.Component', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps {foo: React.Component} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + foo: PropTypes.element.isRequired +};`); + }); + + it('understands Component', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps {foo: Component} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + foo: PropTypes.element.isRequired +};`); + }); + + it('understands React.ReactElement', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps {foo: React.ReactElement} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + foo: PropTypes.element.isRequired +};`); + }); + + it('understands ReactElement', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps {foo: ReactElement} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + foo: PropTypes.element.isRequired +};`); + }); + + it('understands React.ComponentClass', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps {foo: React.ComponentClass} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + foo: PropTypes.element.isRequired +};`); + }); + + it('understands ComponentClass', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps {foo: ComponentClass} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + foo: PropTypes.element.isRequired +};`); + }); + + it('understands React.SFC', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps {foo: React.SFC} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + foo: PropTypes.element.isRequired +};`); + }); + + it('understands SFC', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps {foo: SFC} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + foo: PropTypes.element.isRequired +};`); + }); + + }); + + describe('node propType', () => { + + it('understands React.ReactNode', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps {foo: React.ReactNode} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + foo: PropTypes.node.isRequired +};`); + }); + + it('understands ReactNode', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps {foo: ReactNode} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + foo: PropTypes.node.isRequired +};`); + }); + + }); + + }); + + describe('intersection types', () => { + + it('intersects multiple types together', () => { + const result = transform( + ` +import React from 'react'; +interface iBar {name: string} +interface iFoo {age: number} +interface IFooProps {fizz: iBar & iFoo} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + fizz: PropTypes.shape({ + name: PropTypes.string.isRequired, + age: PropTypes.number.isRequired + }).isRequired +};`); + }); + + it('intersects overlapping types together', () => { + const result = transform( + ` +import React from 'react'; +interface iBar {name: string} +interface iFoo {name: string, age: number} +interface IFooProps {fizz: iBar & iFoo} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + fizz: PropTypes.shape({ + name: PropTypes.string.isRequired, + age: PropTypes.number.isRequired + }).isRequired +};`); + }); + + }); + + describe('union types', () => { + + it('unions primitive types and values', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps {bar: string | 5 | 6} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + bar: PropTypes.oneOfType([PropTypes.string.isRequired, PropTypes.oneOf([5, 6])]).isRequired +};`); + }); + + it('unions custom types', () => { + const result = transform( + ` +import React from 'react'; +interface iFoo {foo: string, bar?: number} +type Bar = {name: string, isActive: true | false} +interface IFooProps {buzz: iFoo | Bar} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + buzz: PropTypes.oneOfType([PropTypes.shape({ + foo: PropTypes.string.isRequired, + bar: PropTypes.number + }).isRequired, PropTypes.shape({ + name: PropTypes.string.isRequired, + isActive: PropTypes.oneOf([true, false]).isRequired + }).isRequired]).isRequired +};`); + }); + + }); + + describe('array / arrayOf propTypes', () => { + + describe('Array', () => { + it('understands an Array of strings', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps {bar: Array} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + bar: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired +};`); + }); + + it('understands an Array of numbers', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps {bar: Array} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + bar: PropTypes.arrayOf(PropTypes.number.isRequired).isRequired +};`); + }); + + it('understands an Array of booleans', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps {bar: Array} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + bar: PropTypes.arrayOf(PropTypes.bool.isRequired).isRequired +};`); + }); + + it('understands an Array of functions', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps {bar: Array<() => void>} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + bar: PropTypes.arrayOf(PropTypes.func.isRequired).isRequired +};`); + }); + + it('understands an Array of literal values', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps {bar: Array<'foo' | 'bar'>} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + bar: PropTypes.arrayOf(PropTypes.oneOf(["foo", "bar"]).isRequired).isRequired +};`); + }); + + it('understands an Array of mixed literal and non-literal types', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps {bar: Array} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + bar: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string.isRequired, PropTypes.oneOf([5, 6])]).isRequired).isRequired +};`); + }); + + it('understands an optional Array of strings and numbers', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps {bar?: Array} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + bar: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string.isRequired, PropTypes.number.isRequired]).isRequired) +};`); + }); + + it('understands an Array of a custom type', () => { + const result = transform( + ` +import React from 'react'; +interface FooBar {foo: string, bar?: boolean} +interface IFooProps {bar: Array} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + bar: PropTypes.arrayOf(PropTypes.shape({ + foo: PropTypes.string.isRequired, + bar: PropTypes.bool + }).isRequired).isRequired +};`); + }); + }); + + describe('T[]', () => { + it('understands an Array of strings', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps {bar: string[]} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + bar: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired +};`); + }); + + it('understands an Array of numbers', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps {bar: number[]} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + bar: PropTypes.arrayOf(PropTypes.number.isRequired).isRequired +};`); + }); + + it('understands an Array of booleans', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps {bar: boolean[]} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + bar: PropTypes.arrayOf(PropTypes.bool.isRequired).isRequired +};`); + }); + + it('understands an Array of functions', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps {bar: (() => void)[]} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + bar: PropTypes.arrayOf(PropTypes.func.isRequired).isRequired +};`); + }); + + it('understands an Array of literal values', () => { + const result = transform( + ` +import React from 'react'; +type BarType = 'foo' | 'bar' +interface IFooProps {bar: BarType[]} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + bar: PropTypes.arrayOf(PropTypes.oneOf(["foo", "bar"]).isRequired).isRequired +};`); + }); + + it('understands an Array of mixed literal and non-literal types', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps {bar: (string | 5 | 6)[]} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + bar: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string.isRequired, PropTypes.oneOf([5, 6])]).isRequired).isRequired +};`); + }); + + it('understands an optional Array of strings and numbers', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps {bar?: (string | number)[]} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + bar: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string.isRequired, PropTypes.number.isRequired]).isRequired) +};`); + }); + + it('understands an Array of a custom type', () => { + const result = transform( + ` +import React from 'react'; +interface FooBar {foo: string, bar?: boolean} +interface IFooProps {bar: FooBar[]} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + bar: PropTypes.arrayOf(PropTypes.shape({ + foo: PropTypes.string.isRequired, + bar: PropTypes.bool + }).isRequired).isRequired +};`); + }); + }); + + }); + + describe('type and interface resolving', () => { + + it('understands inline definitions', () => { + const result = transform( + ` +import React from 'react'; +const FooComponent: React.SFC<{bar: string}> = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + bar: PropTypes.string.isRequired +};`); + }); + + it('understands one level of indirection', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps {bar: string} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + bar: PropTypes.string.isRequired +};`); + }); + + it('understands two levels of indirection', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps {bar: string} +type FooProps = IFooProps +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + bar: PropTypes.string.isRequired +};`); + }); + + describe('external references', () => { + + describe('non-resolvable', () => { + + it(`doesn't set propTypes if the whole type is un-resolvable`, () => { + const result = transform( + ` +import React from 'react'; +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; + +const FooComponent = () => { + return
Hello World
; +};`); + }); + + it('marks un-resolvable types as PropTypes.any', () => { + const result = transform( + ` +import React from 'react'; +const FooComponent: React.SFC<{foo: Foo, bar?: Bar}> = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + foo: PropTypes.any.isRequired, + bar: PropTypes.any +};`); + }); + + it('ignores types from node modules', () => { + const result = transform( + ` +import React, { HTMLAttributes } from 'react'; +const FooComponent: React.SFC> = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; + +const FooComponent = () => { + return
Hello World
; +};`); + }); + + it('intersection with all unknown types resolves to PropTypes.any', () => { + const result = transform( + ` +import React from 'react'; +interface IFooProps {fizz: iBar & iFoo} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + fizz: PropTypes.any.isRequired +};`); + }); + + it('intersection with some unknown types resolves to known types', () => { + const result = transform( + ` +import React from 'react'; +interface iBar { name: string, age?: number } +interface IFooProps {fizz: iBar & iFoo} +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + fizz: PropTypes.shape({ + name: PropTypes.string.isRequired, + age: PropTypes.number + }).isRequired +};`); + }); + + }); + + describe('local references', () => { + + it('resolves types from relative imports', () => { + const result = transform( + ` +import React from 'react'; +import { CommonProps } from '../common'; +const FooComponent: React.SFC<{foo: Foo, bar?: Bar} & CommonProps> = () => { + return (
Hello World
); +}`, + { + ...babelOptions, + plugins: [ + [ + './scripts/babel/proptypes-from-ts-props', + { + fs: { + existsSync: () => true, + statSync: () => ({ isDirectory: () => false }), + readFileSync: () => Buffer.from(` + export interface CommonProps { + className?: string; + 'aria-label'?: string; + 'data-test-subj'?: string; + } + `) + } + } + ], + ] + } + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + foo: PropTypes.any.isRequired, + bar: PropTypes.any, + className: PropTypes.string, + "aria-label": PropTypes.string, + "data-test-subj": PropTypes.string +};`); + }); + + it('resolves to directory index files', () => { + const result = transform( + ` +import React from 'react'; +import { CommonProps } from './common'; +const FooComponent: React.SFC<{foo: Foo, bar?: Bar} & CommonProps> = () => { + return (
Hello World
); +}`, + { + ...babelOptions, + filename: 'foo.tsx', + plugins: [ + [ + './scripts/babel/proptypes-from-ts-props', + { + fs: { + existsSync: () => true, + statSync: () => ({ isDirectory: () => true }), + readFileSync: filepath => { + if (filepath !== path.resolve(process.cwd(), 'common/index.ts')) { + throw new Error('Test case should only try to read file unknown/common/index.ts'); + } + + return Buffer.from(` + export interface CommonProps { + className?: string; + 'aria-label'?: string; + 'data-test-subj'?: string; + } + `); + } + } + } + ], + ] + } + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + foo: PropTypes.any.isRequired, + bar: PropTypes.any, + className: PropTypes.string, + "aria-label": PropTypes.string, + "data-test-subj": PropTypes.string +};`); + }); + + it('loads only exported types', () => { + const result = transform( + ` +import React from 'react'; +import { CommonProps } from '../common'; +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + { + ...babelOptions, + plugins: [ + [ + './scripts/babel/proptypes-from-ts-props', + { + fs: { + existsSync: () => true, + statSync: () => ({ isDirectory: () => false }), + readFileSync: () => Buffer.from(` + interface FooProps { + foo: string + } + export interface CommonProps { + className?: string; + 'aria-label'?: string; + 'data-test-subj'?: string; + } + `) + } + } + ], + ] + } + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + className: PropTypes.string, + "aria-label": PropTypes.string, + "data-test-subj": PropTypes.string +};`); + }); + + it('imported types can also import types', () => { + const result = transform( + ` +import React from 'react'; +import { CommonProps } from './common.ts'; +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + { + ...babelOptions, + plugins: [ + [ + './scripts/babel/proptypes-from-ts-props', + { + fs: { + existsSync: () => true, + statSync: () => ({ isDirectory: () => false }), + readFileSync: filepath => { + if (filepath === path.resolve(process.cwd(), 'common.ts')) { + return Buffer.from(` + import { FooType } from './types.ts'; + export interface CommonProps { + className?: string; + 'aria-label'?: string; + 'data-test-subj'?: string; + foo: FooType; + } + `); + } else if (filepath === path.resolve(process.cwd(), 'types.ts')) { + return Buffer.from(` + export type FooType = "Foo" | "Bar" | "Fizz"; + `); + } + } + } + } + ], + ] + } + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + className: PropTypes.string, + "aria-label": PropTypes.string, + "data-test-subj": PropTypes.string, + foo: PropTypes.oneOf(["Foo", "Bar", "Fizz"]).isRequired +};`); + }); + + it('imported types can import types from other locations', () => { + const result = transform( + ` +import React from 'react'; +import { Foo } from './types/foo.ts'; +const FooComponent: React.SFC = () => { + return (
Hello World
); +}`, + { + ...babelOptions, + plugins: [ + [ + './scripts/babel/proptypes-from-ts-props', + { + fs: { + existsSync: () => true, + statSync: () => ({ isDirectory: () => false }), + readFileSync: filepath => { + if (filepath === path.resolve(process.cwd(), 'types', 'foo.ts')) { + return Buffer.from(` + import { IFoo } from '../interfaces/foo.ts'; + export type Foo = IFoo; + `); + } else if (filepath === path.resolve(process.cwd(), 'interfaces', 'foo.ts')) { + return Buffer.from(` + export interface IFoo { bar: string } + `); + } + } + } + } + ], + ] + } + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + bar: PropTypes.string.isRequired +};`); + }); + + it('resolves object keys used in keyof typeof', () => { + const result = transform( + ` +import React from 'react'; +import { commonKeys, commonKeyTypes } from '../common'; +const FooComponent: React.SFC<{foo: keyof typeof commonKeys, bar?: commonKeyTypes}> = () => { + return (
Hello World
); +}`, + { + ...babelOptions, + plugins: [ + [ + './scripts/babel/proptypes-from-ts-props', + { + fs: { + existsSync: () => true, + statSync: () => ({ isDirectory: () => false }), + readFileSync: () => Buffer.from(` + export const commonKeys = { + s: 'small', + 'l': 'large', + }; + + export type commonKeyTypes = keyof typeof commonKeys; + `) + } + } + ], + ] + } + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + foo: PropTypes.oneOf(["s", "l"]).isRequired, + bar: PropTypes.oneOf(["s", "l"]) +};`); + }); + + }); + + }); + + }); + + describe('supported component declarations', () => { + + it('annotates React.SFC components', () => { + const result = transform( + ` +import React from 'react'; +const FooComponent: React.SFC<{foo: string, bar?: number}> = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + foo: PropTypes.string.isRequired, + bar: PropTypes.number +};`); + }); + + it('annotates SFC components', () => { + const result = transform( + ` +import React, { SFC } from 'react'; +const FooComponent: SFC<{foo: string, bar?: number}> = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + foo: PropTypes.string.isRequired, + bar: PropTypes.number +};`); + }); + + it('annotates React.Component components', () => { + const result = transform( + ` +import React from 'react'; +class FooComponent extends React.Component<{foo: string, bar?: number}> { + render() { + return (
Hello World
); + } +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +class FooComponent extends React.Component { + render() { + return
Hello World
; + } + +} + +FooComponent.propTypes = { + foo: PropTypes.string.isRequired, + bar: PropTypes.number +};`); + }); + + it('annotates React.PureComponent components', () => { + const result = transform( + ` +import React from 'react'; +class FooComponent extends React.PureComponent<{foo: string, bar?: number}> { + render() { + return (
Hello World
); + } +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +class FooComponent extends React.PureComponent { + render() { + return
Hello World
; + } + +} + +FooComponent.propTypes = { + foo: PropTypes.string.isRequired, + bar: PropTypes.number +};`); + }); + + it('annotates Component components', () => { + const result = transform( + ` +import React, { Component } from 'react'; +class FooComponent extends Component<{foo: string, bar?: number}> { + render() { + return (
Hello World
); + } +}`, + babelOptions + ); + + expect(result.code).toBe(`import React, { Component } from 'react'; +import PropTypes from "prop-types"; + +class FooComponent extends Component { + render() { + return
Hello World
; + } + +} + +FooComponent.propTypes = { + foo: PropTypes.string.isRequired, + bar: PropTypes.number +};`); + }); + + it('annotates PureComponent components', () => { + const result = transform( + ` +import React, { PureComponent } from 'react'; +class FooComponent extends PureComponent<{foo: string, bar?: number}> { + render() { + return (
Hello World
); + } +}`, + babelOptions + ); + + expect(result.code).toBe(`import React, { PureComponent } from 'react'; +import PropTypes from "prop-types"; + +class FooComponent extends PureComponent { + render() { + return
Hello World
; + } + +} + +FooComponent.propTypes = { + foo: PropTypes.string.isRequired, + bar: PropTypes.number +};`); + }); + + }); + + describe('comments', () => { + + it('copies comments from types to proptypes', () => { + const result = transform( + ` +import React, { SFC } from 'react'; +interface FooProps { + // this is the foo prop + foo: string, + /** + * this is the optional bar prop + */ + bar?: number +} +const FooComponent: SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + // this is the foo prop + foo: PropTypes.string.isRequired, + + /** + * this is the optional bar prop + */ + bar: PropTypes.number +};`); + }); + + it('copies comments from intersected types', () => { + const result = transform( + ` +import React, { SFC } from 'react'; +interface iFoo { + // this is the foo prop + foo: string +} +interface iBar { + /* bar's foo */ + foo: string, + /** + * this is the optional bar prop + */ + bar?: number +} +const FooComponent: SFC = () => { + return (
Hello World
); +}`, + babelOptions + ); + + expect(result.code).toBe(`import React from 'react'; +import PropTypes from "prop-types"; + +const FooComponent = () => { + return
Hello World
; +}; + +FooComponent.propTypes = { + /* bar's foo */ + // this is the foo prop + foo: PropTypes.string.isRequired, + + /** + * this is the optional bar prop + */ + bar: PropTypes.number +};`); + }); + + }); + + }); + +}); diff --git a/scripts/compile-eui.js b/scripts/compile-eui.js index cc1173f25eb..64e72ef463f 100755 --- a/scripts/compile-eui.js +++ b/scripts/compile-eui.js @@ -1,23 +1,27 @@ const { execSync } = require('child_process'); const chalk = require('chalk'); const shell = require('shelljs'); -const glob = require('glob'); const path = require('path'); +const glob = require('glob'); function compileLib() { shell.mkdir('-p', 'lib/components/icon/assets/tokens', 'lib/services', 'lib/test'); console.log('Compiling src/ to es/ and lib/'); + // Run all code (com|trans)pilation through babel (ESNext JS & TypeScript) execSync( - 'babel --quiet --out-dir=es --ignore "**/webpack.config.js,**/*.test.js" src', + 'babel --quiet --out-dir=es --extensions .js,.ts,.tsx --ignore "**/webpack.config.js,**/*.test.js,**/*.d.ts" src', { env: { ...this.process.env, BABEL_MODULES: false } } ); - execSync('babel --quiet --out-dir=lib --ignore "**/webpack.config.js,**/*.test.js" src'); + execSync('babel --quiet --out-dir=lib --extensions .js,.ts,.tsx --ignore "**/webpack.config.js,**/*.test.js,**/*.d.ts" src'); console.log(chalk.green('✔ Finished compiling src/')); + // Use `tsc` to emit typescript declaration files for .ts files + console.log('Generating typescript definitions file'); execSync(`node ${path.resolve(__dirname, 'dtsgenerator.js')}`); + console.log(chalk.green('✔ Finished generating definitions')); // Also copy over SVGs. Babel has a --copy-files option but that brings over // all kinds of things we don't want into the lib folder. diff --git a/scripts/jest/config.json b/scripts/jest/config.json index c1cf159a266..521a59f5116 100644 --- a/scripts/jest/config.json +++ b/scripts/jest/config.json @@ -1,8 +1,8 @@ { "rootDir": "../../", "roots": [ - "/src-docs/src", - "/src/" + "/src/", + "/scripts/babel" ], "collectCoverageFrom": [ "src/components/**/*.js", @@ -26,20 +26,19 @@ "html" ], "moduleFileExtensions": [ + "ts", + "tsx", "js", "json" ], "testMatch": [ - "**/*.test.js" - ], - "testPathIgnorePatterns": [ - "/scripts/", - "/docs/", - "/node_modules/" - ], - "transformIgnorePatterns": [ - "[/\\\\]node_modules[/\\\\].+\\.js$" + "**/*.test.js", + "**/*.test.ts", + "**/*.test.tsx" ], + "transform": { + "^.+\\.(js|tsx?)$": "babel-jest" + }, "snapshotSerializers": [ "/node_modules/enzyme-to-json/serializer" ] diff --git a/src-docs/src/components/guide_section/guide_section.js b/src-docs/src/components/guide_section/guide_section.js index 94a12a0358d..35e86ed5441 100644 --- a/src-docs/src/components/guide_section/guide_section.js +++ b/src-docs/src/components/guide_section/guide_section.js @@ -318,7 +318,8 @@ export class GuideSection extends Component { const npmImports = code .replace(/(from )'(..\/)+src\/components(\/?';)/, `from '@elastic/eui';`) .replace(/(from )'(..\/)+src\/services(\/?';)/, `from '@elastic/eui/lib/services';`) - .replace(/(from )'(..\/)+src\/experimental(\/?';)/, `from '@elastic/eui/lib/experimental';`); + .replace(/(from )'(..\/)+src\/experimental(\/?';)/, `from '@elastic/eui/lib/experimental';`) + .replace(/(from )'(..\/)+src\/components\/.*?';/, `from '@elastic/eui';`); return (
diff --git a/src-docs/src/views/horizontal_rule/horizontal_rule.js b/src-docs/src/views/horizontal_rule/horizontal_rule.tsx similarity index 79% rename from src-docs/src/views/horizontal_rule/horizontal_rule.js rename to src-docs/src/views/horizontal_rule/horizontal_rule.tsx index da02122e22c..7abca3742ac 100644 --- a/src-docs/src/views/horizontal_rule/horizontal_rule.js +++ b/src-docs/src/views/horizontal_rule/horizontal_rule.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { EuiHorizontalRule, -} from '../../../../src/components'; +} from '../../../../src/components/horizontal_rule'; export default () => (
diff --git a/src-docs/src/views/horizontal_rule/horizontal_rule_margin.js b/src-docs/src/views/horizontal_rule/horizontal_rule_margin.tsx similarity index 88% rename from src-docs/src/views/horizontal_rule/horizontal_rule_margin.js rename to src-docs/src/views/horizontal_rule/horizontal_rule_margin.tsx index bf0c750fbe1..0e2c10fe0cf 100644 --- a/src-docs/src/views/horizontal_rule/horizontal_rule_margin.js +++ b/src-docs/src/views/horizontal_rule/horizontal_rule_margin.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { EuiHorizontalRule, -} from '../../../../src/components'; +} from '../../../../src/components/horizontal_rule'; export default () => (
diff --git a/src-docs/src/views/spacer/spacer.js b/src-docs/src/views/spacer/spacer.tsx similarity index 92% rename from src-docs/src/views/spacer/spacer.js rename to src-docs/src/views/spacer/spacer.tsx index 392490d04d6..68aa3ebea34 100644 --- a/src-docs/src/views/spacer/spacer.js +++ b/src-docs/src/views/spacer/spacer.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { EuiSpacer, -} from '../../../../src/components'; +} from '../../../../src/components/spacer'; export default () => (
diff --git a/src-docs/webpack.config.js b/src-docs/webpack.config.js index e5dc03b41eb..58f74afa948 100644 --- a/src-docs/webpack.config.js +++ b/src-docs/webpack.config.js @@ -1,6 +1,7 @@ const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CircularDependencyPlugin = require('circular-dependency-plugin'); +const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); module.exports = { mode: 'development', @@ -18,9 +19,13 @@ module.exports = { filename: 'bundle.js' }, + resolve: { + extensions: ['.ts', '.tsx', '.js', '.json'], + }, + module: { rules: [{ - test: /\.js$/, + test: /\.(js|tsx?)$/, loader: 'babel-loader', exclude: /node_modules/ }, { @@ -56,6 +61,12 @@ module.exports = { exclude: /node_modules/, failOnError: true, }), + // run TypeScript and tslint during webpack build + new ForkTsCheckerWebpackPlugin({ + tsconfig: path.resolve(__dirname, '..', 'tsconfig.json'), + tslint: path.resolve(__dirname, '..', 'tslint.yaml'), + async: false, // makes errors more visible, but potentially less performant + }), ], devServer: { diff --git a/src-framer/code/_framer_helpers/frame_size.tsx b/src-framer/code/_framer_helpers/frame_size.tsx index 9b0d607201d..e086367c7b1 100644 --- a/src-framer/code/_framer_helpers/frame_size.tsx +++ b/src-framer/code/_framer_helpers/frame_size.tsx @@ -9,19 +9,19 @@ interface Props { export class FrameSize extends React.Component { // Set default properties - public static defaultProps = { + static defaultProps = { frame: true, }; // Items shown in property panel - public static propertyControls: PropertyControls = { + static propertyControls: PropertyControls = { frame: { type: ControlType.boolean, title: '🖍 Fit to frame', }, }; - public render() { + render() { let optionallyFramedComponent; if (this.props.frame) { optionallyFramedComponent = ( diff --git a/src-framer/code/_framer_helpers/theme.tsx b/src-framer/code/_framer_helpers/theme.tsx index 4aaddb9affb..d0081edc5fc 100644 --- a/src-framer/code/_framer_helpers/theme.tsx +++ b/src-framer/code/_framer_helpers/theme.tsx @@ -11,12 +11,12 @@ interface Props { export class Theme extends React.Component { // Set default properties - public static defaultProps = { + static defaultProps = { theme: 'light', }; // Items shown in property panel - public static propertyControls: PropertyControls = { + static propertyControls: PropertyControls = { theme: { type: ControlType.SegmentedEnum, options: ['light', 'dark'], @@ -24,7 +24,7 @@ export class Theme extends React.Component { }, }; - public render() { + render() { const lightBgColor = '#FFF'; const darkBgColor = '#222'; diff --git a/src-framer/code/avatar/avatar.tsx b/src-framer/code/avatar/avatar.tsx index a2b66a151b8..fb2bd3891c2 100644 --- a/src-framer/code/avatar/avatar.tsx +++ b/src-framer/code/avatar/avatar.tsx @@ -17,14 +17,14 @@ interface Props { export class Avatar extends React.Component { // Set default properties - public static defaultProps = { + static defaultProps = { name: 'Han Solo', height: 32, // To give a decent start with sizing width: 32, // To give a decent start with sizing }; // Items shown in property panel - public static propertyControls: PropertyControls = { + static propertyControls: PropertyControls = { name: { type: ControlType.String, title: 'name', @@ -54,7 +54,7 @@ export class Avatar extends React.Component { }, }; - public render() { + render() { return ( { // Set default properties - public static defaultProps = { + static defaultProps = { childText: 'Badge text', color: 'primary', iconType: null, @@ -27,7 +27,7 @@ export class Badge extends React.Component { }; // Items shown in property panel - public static propertyControls: PropertyControls = { + static propertyControls: PropertyControls = { childText: { type: ControlType.String, title: '🧙 childText', @@ -59,7 +59,7 @@ export class Badge extends React.Component { }, }; - public render() { + render() { return ( { // Set default properties - public static defaultProps = { + static defaultProps = { childText: 'Button text', color: 'primary', iconType: null, @@ -31,7 +31,7 @@ export class Button extends React.Component { }; // Items shown in property panel - public static propertyControls: PropertyControls = { + static propertyControls: PropertyControls = { childText: { type: ControlType.String, title: '🧙 childText', @@ -76,7 +76,7 @@ export class Button extends React.Component { }, }; - public render() { + render() { return ( { // Set default properties - public static defaultProps = { + static defaultProps = { title: 'Title', color: 'primary', iconType: null, @@ -25,7 +25,7 @@ export class CallOut extends React.Component { }; // Items shown in property panel - public static propertyControls: PropertyControls = { + static propertyControls: PropertyControls = { title: { type: ControlType.String, title: 'title', @@ -54,7 +54,7 @@ export class CallOut extends React.Component { }, }; - public render() { + render() { return ( { // Set default properties - public static defaultProps = { + static defaultProps = { title: 'Hey there', showIconProps: false, iconSize: 'xl', @@ -32,7 +32,7 @@ export class Card extends React.Component { }; // Items shown in property panel - public static propertyControls: PropertyControls = { + static propertyControls: PropertyControls = { title: { type: ControlType.String, title: 'title', @@ -96,7 +96,7 @@ export class Card extends React.Component { }, }; - public render() { + render() { return ( { // Set default properties - public static defaultProps = { + static defaultProps = { name: 'Han Solo', height: 300, // To give a decent start with sizing width: 600, // To give a decent start with sizing }; // Items shown in property panel - public static propertyControls: PropertyControls = { + static propertyControls: PropertyControls = { fontSize: { type: ControlType.SegmentedEnum, options: FONT_SIZES, @@ -52,7 +52,7 @@ export class CodeBlock extends React.Component { }, }; - public render() { + render() { return ( { // Set default properties - public static defaultProps = { + static defaultProps = { align: 'left', compressed: false, textStyle: 'normal', @@ -39,7 +39,7 @@ export class DescriptionList extends React.Component { }; // Items shown in property panel - public static propertyControls: PropertyControls = { + static propertyControls: PropertyControls = { titleText: { type: ControlType.String, title: '🧙 titleText', @@ -69,7 +69,7 @@ export class DescriptionList extends React.Component { }, }; - public render() { + render() { return ( { // Set default properties - public static defaultProps = { + static defaultProps = { childText: 'Facet button', quantity: 19, height: 40, // To give a decent start with sizing @@ -27,7 +27,7 @@ export class FacetButton extends React.Component { }; // Items shown in property panel - public static propertyControls: PropertyControls = { + static propertyControls: PropertyControls = { childText: { type: ControlType.String, title: '🧙 childText', @@ -78,7 +78,7 @@ export class FacetButton extends React.Component { }, }; - public render() { + render() { return ( { // Set default properties - public static defaultProps = { + static defaultProps = { hasFormRow: false, fullWidth: true, formRowLabel: 'Label name', }; // Items shown in property panel - public static propertyControls: PropertyControls = { + static propertyControls: PropertyControls = { placeholder: { type: ControlType.String, title: 'placeholder' }, value: { type: ControlType.String, title: 'value' }, isInvalid: { type: ControlType.Boolean, title: 'isInvalid' }, @@ -50,7 +50,7 @@ export class FieldPassword extends React.Component { }, }; - public render() { + render() { const fieldPassword = ( { // Set default properties - public static defaultProps = { + static defaultProps = { hasFormRow: false, formRowLabel: 'Label name', }; // Items shown in property panel - public static propertyControls: PropertyControls = { + static propertyControls: PropertyControls = { placeholder: { type: ControlType.String, title: 'placeholder' }, value: { type: ControlType.String, title: 'value' }, isInvalid: { type: ControlType.Boolean, title: 'isInvalid' }, @@ -49,7 +49,7 @@ export class FieldSearch extends React.Component { }, }; - public render() { + render() { const fieldSearch = ( { // Set default properties - public static defaultProps = { + static defaultProps = { hasFormRow: false, fullWidth: true, formRowLabel: 'Label name', }; // Items shown in property panel - public static propertyControls: PropertyControls = { + static propertyControls: PropertyControls = { placeholder: { type: ControlType.String, title: 'placeholder' }, value: { type: ControlType.String, title: 'value' }, isInvalid: { type: ControlType.Boolean, title: 'isInvalid' }, @@ -57,7 +57,7 @@ export class FieldText extends React.Component { }, }; - public render() { + render() { const fieldText = ( { // Set default properties - public static defaultProps = { + static defaultProps = { size: 'xl', type: 'logoElasticsearch', // Initial size at 32 for ease of use @@ -23,7 +23,7 @@ export class Icon extends React.Component { }; // Items shown in property panel - public static propertyControls: PropertyControls = { + static propertyControls: PropertyControls = { type: { type: ControlType.Enum, options: TYPES, @@ -41,7 +41,7 @@ export class Icon extends React.Component { }, }; - public render() { + render() { return ( { // Set default properties - public static defaultProps = { + static defaultProps = { paddingSize: 'm', hasShadow: false, fitToFrame: true, }; // Items shown in property panel - public static propertyControls: PropertyControls = { + static propertyControls: PropertyControls = { hasShadow: { type: ControlType.Boolean, title: 'hasShadow' }, betaBadgeLabel: { type: ControlType.String, title: 'betaBadgeLabel' }, paddingSize: { @@ -32,7 +32,7 @@ export class Panel extends React.Component { fitToFrame: { type: ControlType.Boolean, title: 'fitToFrame' }, }; - public render() { + render() { return ( { // Set default properties - public static defaultProps = { + static defaultProps = { childText: 'Add your text in the overide', }; // Items shown in property panel - public static propertyControls: PropertyControls = { + static propertyControls: PropertyControls = { childText: { type: ControlType.String, title: '🧙 childText', @@ -48,7 +48,7 @@ export class Text extends React.Component { }, }; - public render() { + render() { return ( { // Set default properties - public static defaultProps = { + static defaultProps = { childText: 'Title text goes here', size: 'l', }; // Items shown in property panel - public static propertyControls: PropertyControls = { + static propertyControls: PropertyControls = { childText: { type: ControlType.String, title: '🧙 childText', @@ -36,7 +36,7 @@ export class Title extends React.Component { }, }; - public render() { + render() { return ( +import { CommonProps } from '../common'; import { HTMLAttributes, SFC } from 'react'; diff --git a/src/components/badge/index.d.ts b/src/components/badge/index.d.ts index 5d401789d82..7c34681fd93 100644 --- a/src/components/badge/index.d.ts +++ b/src/components/badge/index.d.ts @@ -2,6 +2,7 @@ /// import { HTMLAttributes, MouseEventHandler, SFC, ReactNode } from 'react'; +import { CommonProps } from '../common'; declare module '@elastic/eui' { @@ -15,7 +16,7 @@ declare module '@elastic/eui' { onClick?: MouseEventHandler; onClickAriaLabel?: string; color?: string; - closeButtonProps?: Object; + closeButtonProps?: object; } export const EuiBadge: SFC< diff --git a/src/components/button/index.d.ts b/src/components/button/index.d.ts index fd3096cee25..d17bf8f0454 100644 --- a/src/components/button/index.d.ts +++ b/src/components/button/index.d.ts @@ -1,4 +1,4 @@ -/// +import { CommonProps } from '../common'; /// import { SFC, ButtonHTMLAttributes, AnchorHTMLAttributes, MouseEventHandler, HTMLAttributes } from 'react'; diff --git a/src/components/call_out/index.d.ts b/src/components/call_out/index.d.ts index 74b343ac68e..c9b51b64998 100644 --- a/src/components/call_out/index.d.ts +++ b/src/components/call_out/index.d.ts @@ -1,4 +1,4 @@ -/// +import { CommonProps, Omit } from '../common'; /// import { SFC, ReactNode, HTMLAttributes } from 'react'; diff --git a/src/components/code/index.d.ts b/src/components/code/index.d.ts index a1e3d134747..73b26b7ef83 100644 --- a/src/components/code/index.d.ts +++ b/src/components/code/index.d.ts @@ -1,4 +1,4 @@ -/// +import { CommonProps } from '../common'; import { SFC, HTMLAttributes } from 'react'; diff --git a/src/components/combo_box/index.d.ts b/src/components/combo_box/index.d.ts index 3fb61c90a57..c10f514ad33 100644 --- a/src/components/combo_box/index.d.ts +++ b/src/components/combo_box/index.d.ts @@ -6,6 +6,7 @@ import { EuiComboBoxOptionsListPosition, EuiComboBoxOptionsListProps, } from '@elastic/eui'; +import { RefCallback } from '../common'; declare module '@elastic/eui' { export type EuiComboBoxOptionProps = ButtonHTMLAttributes & { diff --git a/src/components/common.d.ts b/src/components/common.d.ts deleted file mode 100644 index 20491a3b09f..00000000000 --- a/src/components/common.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -declare module '@elastic/eui' { - export interface CommonProps { - className?: string; - 'aria-label'?: string; - 'data-test-subj'?: string; - } - - export type NoArgCallback = () => T; - - export type RefCallback = ( - element: Element - ) => void; - - // utility types: - - type Omit = Pick>; -} diff --git a/src/components/common.ts b/src/components/common.ts new file mode 100644 index 00000000000..c552fa0d27f --- /dev/null +++ b/src/components/common.ts @@ -0,0 +1,15 @@ +export interface CommonProps { + className?: string; + 'aria-label'?: string; + 'data-test-subj'?: string; +} + +export type NoArgCallback = () => T; + +export type RefCallback = ( + element: Element +) => void; + +// utility types: + +export type Omit = Pick>; diff --git a/src/components/context_menu/index.d.ts b/src/components/context_menu/index.d.ts index c43c74566f5..e386544ee7c 100644 --- a/src/components/context_menu/index.d.ts +++ b/src/components/context_menu/index.d.ts @@ -1,4 +1,4 @@ -/// +import { CommonProps, RefCallback, NoArgCallback, Omit } from '../common'; import { SFC, diff --git a/src/components/description_list/index.d.ts b/src/components/description_list/index.d.ts index cd1d7db6cef..c4290953721 100644 --- a/src/components/description_list/index.d.ts +++ b/src/components/description_list/index.d.ts @@ -1,4 +1,5 @@ import { HTMLAttributes, Component, ReactNode } from 'react'; +import { CommonProps } from '../common'; declare module '@elastic/eui' { export type EuiDescriptionListType = 'row' | 'column' | 'inline'; diff --git a/src/components/empty_prompt/index.d.ts b/src/components/empty_prompt/index.d.ts index 920ae94c94d..34e05663894 100644 --- a/src/components/empty_prompt/index.d.ts +++ b/src/components/empty_prompt/index.d.ts @@ -1,4 +1,4 @@ -/// +import { CommonProps, Omit } from '../common'; /// /// diff --git a/src/components/error_boundary/index.d.ts b/src/components/error_boundary/index.d.ts index 813200d82c7..f3ac93950dc 100644 --- a/src/components/error_boundary/index.d.ts +++ b/src/components/error_boundary/index.d.ts @@ -1,6 +1,5 @@ -/// - import { HTMLAttributes, Component } from 'react'; +import { CommonProps } from '../common'; declare module '@elastic/eui' { export class EuiErrorBoundary extends Component< diff --git a/src/components/flex/index.d.ts b/src/components/flex/index.d.ts index d2ae1beefa0..38f89dac7c4 100644 --- a/src/components/flex/index.d.ts +++ b/src/components/flex/index.d.ts @@ -1,4 +1,4 @@ -/// +import { CommonProps } from '../common'; import { SFC, HTMLAttributes } from 'react'; diff --git a/src/components/flyout/index.d.ts b/src/components/flyout/index.d.ts index 918a526f2e2..866661dc3ce 100644 --- a/src/components/flyout/index.d.ts +++ b/src/components/flyout/index.d.ts @@ -1,3 +1,5 @@ +import { CommonProps } from '../common'; + declare module '@elastic/eui' { export interface EuiFlyoutProps { onClose: () => void; diff --git a/src/components/form/checkbox/index.d.ts b/src/components/form/checkbox/index.d.ts index 70d43e8c58d..91ec6d14b15 100644 --- a/src/components/form/checkbox/index.d.ts +++ b/src/components/form/checkbox/index.d.ts @@ -1,4 +1,4 @@ -/// +import { CommonProps } from '../../common'; import { SFC, ReactNode, HTMLAttributes, ChangeEventHandler, InputHTMLAttributes } from 'react'; diff --git a/src/components/form/field_number/index.d.ts b/src/components/form/field_number/index.d.ts index 3e767d1c3e1..4300b76c292 100644 --- a/src/components/form/field_number/index.d.ts +++ b/src/components/form/field_number/index.d.ts @@ -1,4 +1,4 @@ -/// +import { CommonProps } from '../../common'; /// import { ReactNode, SFC, InputHTMLAttributes } from 'react'; diff --git a/src/components/form/field_password/index.d.ts b/src/components/form/field_password/index.d.ts index e8113b8d2a9..f344ffc8230 100644 --- a/src/components/form/field_password/index.d.ts +++ b/src/components/form/field_password/index.d.ts @@ -1,4 +1,4 @@ -/// +import { CommonProps } from '../../common'; import { SFC, InputHTMLAttributes, Ref } from 'react'; diff --git a/src/components/form/field_search/index.d.ts b/src/components/form/field_search/index.d.ts index 389ab1d54ad..2d67b8144cc 100644 --- a/src/components/form/field_search/index.d.ts +++ b/src/components/form/field_search/index.d.ts @@ -1,4 +1,4 @@ -/// +import { CommonProps } from '../../common'; import { SFC, InputHTMLAttributes } from 'react'; diff --git a/src/components/form/field_text/index.d.ts b/src/components/form/field_text/index.d.ts index c01f312fab9..0e969d040ec 100644 --- a/src/components/form/field_text/index.d.ts +++ b/src/components/form/field_text/index.d.ts @@ -1,4 +1,4 @@ -/// +import { CommonProps } from '../../common'; import { SFC, InputHTMLAttributes } from 'react'; diff --git a/src/components/form/form_help_text/index.d.ts b/src/components/form/form_help_text/index.d.ts index 0c3c51e7065..f8564267b1a 100644 --- a/src/components/form/form_help_text/index.d.ts +++ b/src/components/form/form_help_text/index.d.ts @@ -1,4 +1,4 @@ -/// +import { CommonProps } from '../../common'; /// import { ReactNode, SFC, HTMLAttributes } from 'react'; diff --git a/src/components/form/form_label/index.d.ts b/src/components/form/form_label/index.d.ts index 90f95f3a5b8..d441bc3976d 100644 --- a/src/components/form/form_label/index.d.ts +++ b/src/components/form/form_label/index.d.ts @@ -1,4 +1,4 @@ -/// +import { CommonProps } from '../../common'; import { SFC, ReactNode, LabelHTMLAttributes } from 'react'; diff --git a/src/components/form/form_row/index.d.ts b/src/components/form/form_row/index.d.ts index e957c909870..f926211644d 100644 --- a/src/components/form/form_row/index.d.ts +++ b/src/components/form/form_row/index.d.ts @@ -1,4 +1,4 @@ -/// +import { CommonProps } from '../../common'; import { SFC, ReactNode, HTMLAttributes } from 'react'; diff --git a/src/components/form/index.d.ts b/src/components/form/index.d.ts index fe57e400f94..f04a27614e5 100644 --- a/src/components/form/index.d.ts +++ b/src/components/form/index.d.ts @@ -1,4 +1,4 @@ -/// +import { CommonProps } from '../common'; /// /// /// diff --git a/src/components/form/radio/index.d.ts b/src/components/form/radio/index.d.ts index 915da2ae31b..4673106cd91 100644 --- a/src/components/form/radio/index.d.ts +++ b/src/components/form/radio/index.d.ts @@ -1,4 +1,4 @@ -/// +import { CommonProps, Omit } from '../../common'; import { SFC, ChangeEventHandler, HTMLAttributes, ReactNode } from 'react'; diff --git a/src/components/form/range/index.d.ts b/src/components/form/range/index.d.ts index c1d95ba4c5d..6b15cbf3bd8 100644 --- a/src/components/form/range/index.d.ts +++ b/src/components/form/range/index.d.ts @@ -1,4 +1,4 @@ -/// +import { CommonProps } from '../../common'; import { SFC, ReactNode, HTMLAttributes, ChangeEventHandler, InputHTMLAttributes } from 'react'; diff --git a/src/components/form/select/index.d.ts b/src/components/form/select/index.d.ts index e7d7d83abc7..051d3e85fee 100644 --- a/src/components/form/select/index.d.ts +++ b/src/components/form/select/index.d.ts @@ -1,4 +1,4 @@ -/// +import { CommonProps } from '../../common'; import {SFC, ReactNode, Ref, OptionHTMLAttributes, SelectHTMLAttributes} from 'react'; diff --git a/src/components/form/switch/index.d.ts b/src/components/form/switch/index.d.ts index f3c67f8c25e..06f1fd58dd5 100644 --- a/src/components/form/switch/index.d.ts +++ b/src/components/form/switch/index.d.ts @@ -1,4 +1,4 @@ -/// +import { CommonProps } from '../../common'; import { SFC, InputHTMLAttributes, ReactNode } from 'react'; diff --git a/src/components/form/text_area/index.d.ts b/src/components/form/text_area/index.d.ts index 15387a74f1d..3cffacdfb7a 100644 --- a/src/components/form/text_area/index.d.ts +++ b/src/components/form/text_area/index.d.ts @@ -1,4 +1,4 @@ -/// +import { CommonProps } from '../../common'; import { SFC, TextareaHTMLAttributes } from 'react'; diff --git a/src/components/health/index.d.ts b/src/components/health/index.d.ts index 06133d3c867..0b078eb82be 100644 --- a/src/components/health/index.d.ts +++ b/src/components/health/index.d.ts @@ -1,6 +1,7 @@ /// import { SFC, HTMLAttributes } from 'react'; +import { CommonProps } from '../common'; declare module '@elastic/eui' { /** diff --git a/src/components/horizontal_rule/__snapshots__/horizontal_rule.test.js.snap b/src/components/horizontal_rule/__snapshots__/horizontal_rule.test.tsx.snap similarity index 100% rename from src/components/horizontal_rule/__snapshots__/horizontal_rule.test.js.snap rename to src/components/horizontal_rule/__snapshots__/horizontal_rule.test.tsx.snap diff --git a/src/components/horizontal_rule/horizontal_rule.test.js b/src/components/horizontal_rule/horizontal_rule.test.tsx similarity index 100% rename from src/components/horizontal_rule/horizontal_rule.test.js rename to src/components/horizontal_rule/horizontal_rule.test.tsx diff --git a/src/components/horizontal_rule/horizontal_rule.js b/src/components/horizontal_rule/horizontal_rule.tsx similarity index 58% rename from src/components/horizontal_rule/horizontal_rule.js rename to src/components/horizontal_rule/horizontal_rule.tsx index 9ce7cc6a128..c30291ae2ce 100644 --- a/src/components/horizontal_rule/horizontal_rule.js +++ b/src/components/horizontal_rule/horizontal_rule.tsx @@ -1,7 +1,19 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { SFC, HTMLAttributes } from 'react'; import classNames from 'classnames'; +import { CommonProps } from '../common'; + +export type EuiHorizontalRuleSize = keyof typeof sizeToClassNameMap; +export type EuiHorizontalRuleMargin = keyof typeof marginToClassNameMap; + +export interface EuiHorizontalRuleProps { + /** + * Defines the width of the HR. + */ + size?: EuiHorizontalRuleSize; + margin?: EuiHorizontalRuleMargin; +} + const sizeToClassNameMap = { full: 'euiHorizontalRule--full', half: 'euiHorizontalRule--half', @@ -22,7 +34,9 @@ const marginToClassNameMap = { export const MARGINS = Object.keys(marginToClassNameMap); -export const EuiHorizontalRule = ({ +export const EuiHorizontalRule: SFC< +CommonProps & HTMLAttributes & EuiHorizontalRuleProps +> = ({ className, size, margin, @@ -30,8 +44,8 @@ export const EuiHorizontalRule = ({ }) => { const classes = classNames( 'euiHorizontalRule', - sizeToClassNameMap[size], - marginToClassNameMap[margin], + size ? sizeToClassNameMap[size] : undefined, + margin ? marginToClassNameMap[margin] : undefined, className ); @@ -43,13 +57,6 @@ export const EuiHorizontalRule = ({ ); }; -EuiHorizontalRule.propTypes = { - children: PropTypes.node, - className: PropTypes.string, - size: PropTypes.oneOf(SIZES), - margin: PropTypes.oneOf(MARGINS), -}; - EuiHorizontalRule.defaultProps = { size: 'full', margin: 'l', diff --git a/src/components/horizontal_rule/index.d.ts b/src/components/horizontal_rule/index.d.ts deleted file mode 100644 index f5fa16ded08..00000000000 --- a/src/components/horizontal_rule/index.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -/// - -import { SFC, HTMLAttributes } from 'react'; - -declare module '@elastic/eui' { - - /** - * EuiHorizontalRule type defs - * - * @see './horizontal_rule.js' - */ - - export type EuiHorizontalRuleSize = 'full' | 'half' | 'quarter'; - - export type EuiHorizontalRuleMargin = 'none' | 'xs' | 's' | 'm' | 'l' | 'xl' | 'xxl'; - - export interface EuiHorizontalRuleProps { - size?: EuiHorizontalRuleSize; - margin?: EuiHorizontalRuleMargin; - } - - export const EuiHorizontalRule: SFC< - CommonProps & HTMLAttributes & EuiHorizontalRuleProps - >; - -} diff --git a/src/components/horizontal_rule/index.js b/src/components/horizontal_rule/index.ts similarity index 100% rename from src/components/horizontal_rule/index.js rename to src/components/horizontal_rule/index.ts diff --git a/src/components/icon/index.d.ts b/src/components/icon/index.d.ts index aa4e0bcca28..2601ec22d0c 100644 --- a/src/components/icon/index.d.ts +++ b/src/components/icon/index.d.ts @@ -1,4 +1,4 @@ -/// +import { CommonProps } from '../common'; import { SFC, SVGAttributes } from 'react'; diff --git a/src/components/index.d.ts b/src/components/index.d.ts index 1f52035a509..05fa53360e0 100644 --- a/src/components/index.d.ts +++ b/src/components/index.d.ts @@ -6,7 +6,6 @@ /// /// /// -/// /// /// /// @@ -15,7 +14,6 @@ /// /// /// -/// /// /// /// @@ -28,7 +26,6 @@ /// /// /// -/// /// /// /// @@ -36,3 +33,8 @@ /// /// /// + +declare module '@elastic/eui' { + // @ts-ignore + export * from '@elastic/eui/components/common'; +} diff --git a/src/components/key_pad_menu/index.d.ts b/src/components/key_pad_menu/index.d.ts index 2b53cb923d9..f5985a37d89 100644 --- a/src/components/key_pad_menu/index.d.ts +++ b/src/components/key_pad_menu/index.d.ts @@ -1,4 +1,4 @@ -/// +import { CommonProps } from '../common'; import { AnchorHTMLAttributes, ButtonHTMLAttributes, HTMLAttributes, MouseEventHandler, ReactNode, SFC } from 'react'; diff --git a/src/components/link/index.d.ts b/src/components/link/index.d.ts index 2d78142a9cd..fd2b5b5f64b 100644 --- a/src/components/link/index.d.ts +++ b/src/components/link/index.d.ts @@ -1,4 +1,4 @@ -/// +import { CommonProps, Omit } from '../common'; import { SFC, diff --git a/src/components/loading/index.d.ts b/src/components/loading/index.d.ts index 432ea3b7426..985b5f2c9c8 100644 --- a/src/components/loading/index.d.ts +++ b/src/components/loading/index.d.ts @@ -1,4 +1,4 @@ -/// +import { CommonProps } from '../common'; import { SFC, HTMLAttributes } from 'react'; diff --git a/src/components/modal/index.d.ts b/src/components/modal/index.d.ts index 290350a98a5..076c848cd5d 100644 --- a/src/components/modal/index.d.ts +++ b/src/components/modal/index.d.ts @@ -1,4 +1,4 @@ -/// +import { CommonProps, Omit } from '../common'; /// import { ReactNode, SFC, HTMLAttributes } from 'react'; diff --git a/src/components/overlay_mask/index.d.ts b/src/components/overlay_mask/index.d.ts index 174e99d8f7d..1da3016e4f4 100644 --- a/src/components/overlay_mask/index.d.ts +++ b/src/components/overlay_mask/index.d.ts @@ -1,4 +1,4 @@ -/// +import { CommonProps } from '../common'; import { SFC, HTMLAttributes } from 'react'; diff --git a/src/components/page/index.d.ts b/src/components/page/index.d.ts index f8b538df508..1f2f8a1b96c 100644 --- a/src/components/page/index.d.ts +++ b/src/components/page/index.d.ts @@ -1,4 +1,4 @@ -/// +import { CommonProps } from '../common'; /// import { SFC, HTMLAttributes } from 'react'; diff --git a/src/components/pagination/index.d.ts b/src/components/pagination/index.d.ts index 3287446549a..54ef393a7cb 100644 --- a/src/components/pagination/index.d.ts +++ b/src/components/pagination/index.d.ts @@ -1,4 +1,4 @@ -/// +import { CommonProps, Omit } from '../common'; /// import { HTMLAttributes, SFC } from 'react'; diff --git a/src/components/panel/index.d.ts b/src/components/panel/index.d.ts index eb0e4234aae..2ceee2effee 100644 --- a/src/components/panel/index.d.ts +++ b/src/components/panel/index.d.ts @@ -1,4 +1,4 @@ -/// +import { CommonProps, RefCallback } from '../common'; import { HTMLAttributes, SFC } from 'react'; diff --git a/src/components/popover/index.d.ts b/src/components/popover/index.d.ts index 2e58d8f837d..235dfce8c98 100644 --- a/src/components/popover/index.d.ts +++ b/src/components/popover/index.d.ts @@ -1,4 +1,4 @@ -/// +import { CommonProps, NoArgCallback } from '../common'; /// import { SFC, ReactNode, HTMLAttributes } from 'react'; diff --git a/src/components/progress/index.d.ts b/src/components/progress/index.d.ts index 53037e1b3f7..b913ea775a3 100644 --- a/src/components/progress/index.d.ts +++ b/src/components/progress/index.d.ts @@ -1,4 +1,4 @@ -/// +import { CommonProps } from '../common'; import { SFC, ProgressHTMLAttributes } from 'react'; diff --git a/src/components/spacer/__snapshots__/spacer.test.js.snap b/src/components/spacer/__snapshots__/spacer.test.tsx.snap similarity index 100% rename from src/components/spacer/__snapshots__/spacer.test.js.snap rename to src/components/spacer/__snapshots__/spacer.test.tsx.snap diff --git a/src/components/spacer/index.d.ts b/src/components/spacer/index.d.ts deleted file mode 100644 index 87c4a8480ff..00000000000 --- a/src/components/spacer/index.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -/// - -declare module '@elastic/eui' { - import { SFC, HTMLAttributes } from 'react'; - - /** - * spacer type defs - * - * @see './spacer.js' - */ - - export type SpacerSize = 'xs' | 's' | 'm' | 'l' | 'xl' | 'xxl'; - - export interface EuiSpacerProps { - size?: SpacerSize; - } - - export const EuiSpacer: SFC< - CommonProps & HTMLAttributes & EuiSpacerProps - >; -} diff --git a/src/components/spacer/index.js b/src/components/spacer/index.ts similarity index 100% rename from src/components/spacer/index.js rename to src/components/spacer/index.ts diff --git a/src/components/spacer/spacer.test.js b/src/components/spacer/spacer.test.tsx similarity index 100% rename from src/components/spacer/spacer.test.js rename to src/components/spacer/spacer.test.tsx diff --git a/src/components/spacer/spacer.js b/src/components/spacer/spacer.tsx similarity index 60% rename from src/components/spacer/spacer.js rename to src/components/spacer/spacer.tsx index 88ad1fe9782..fd37d095dbc 100644 --- a/src/components/spacer/spacer.js +++ b/src/components/spacer/spacer.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import PropTypes from 'prop-types'; import classNames from 'classnames'; +import { HTMLAttributes } from 'react'; +import { CommonProps } from '../common'; const sizeToClassNameMap = { xs: 'euiSpacer--xs', @@ -13,14 +14,20 @@ const sizeToClassNameMap = { export const SIZES = Object.keys(sizeToClassNameMap); -export const EuiSpacer = ({ +export type SpacerSize = keyof typeof sizeToClassNameMap; + +export type EuiSpacerProps = HTMLAttributes & CommonProps & { + size?: SpacerSize, +}; + +export const EuiSpacer: React.SFC = ({ className, size, ...rest }) => { const classes = classNames( 'euiSpacer', - sizeToClassNameMap[size], + size ? sizeToClassNameMap[size] : undefined, className ); @@ -32,12 +39,6 @@ export const EuiSpacer = ({ ); }; -EuiSpacer.propTypes = { - children: PropTypes.node, - className: PropTypes.string, - size: PropTypes.oneOf(SIZES), -}; - EuiSpacer.defaultProps = { size: 'l', }; diff --git a/src/components/steps/index.d.ts b/src/components/steps/index.d.ts index 74457e8b82e..209fca4ea5f 100644 --- a/src/components/steps/index.d.ts +++ b/src/components/steps/index.d.ts @@ -1,8 +1,7 @@ -/// +import { SFC, ReactNode, HTMLAttributes, MouseEventHandler } from 'react'; +import { CommonProps, Omit } from '../common'; declare module '@elastic/eui' { - import { SFC, ReactNode, HTMLAttributes, MouseEventHandler } from 'react'; - export type EuiStepStatus = 'complete' | 'incomplete' | 'warning' | 'danger' | 'disabled' /** diff --git a/src/components/table/index.d.ts b/src/components/table/index.d.ts index 2397846aa6e..05aa35f0a8a 100644 --- a/src/components/table/index.d.ts +++ b/src/components/table/index.d.ts @@ -1,5 +1,5 @@ /// -/// +import { CommonProps, NoArgCallback } from '../common'; /// /// diff --git a/src/components/table/table_pagination/index.d.ts b/src/components/table/table_pagination/index.d.ts index dbf6a3662a4..2aea332d569 100644 --- a/src/components/table/table_pagination/index.d.ts +++ b/src/components/table/table_pagination/index.d.ts @@ -1,4 +1,4 @@ -/// +import { CommonProps } from '../../common'; import { SFC } from 'react'; diff --git a/src/components/tabs/index.d.ts b/src/components/tabs/index.d.ts index f259d2b8c96..8ac6d675d3b 100644 --- a/src/components/tabs/index.d.ts +++ b/src/components/tabs/index.d.ts @@ -1,8 +1,7 @@ -/// +import { MouseEventHandler, ReactNode, SFC, HTMLAttributes } from 'react'; +import { CommonProps } from '../common'; declare module '@elastic/eui' { - import { MouseEventHandler, ReactNode, SFC, HTMLAttributes } from 'react'; - type TAB_SIZES = 's' | 'm'; type EuiTabProps = { diff --git a/src/components/text/index.d.ts b/src/components/text/index.d.ts index 79056140eb6..b7951d87dd9 100644 --- a/src/components/text/index.d.ts +++ b/src/components/text/index.d.ts @@ -1,8 +1,7 @@ -/// +import { CommonProps } from '../common'; +import { SFC, HTMLAttributes } from 'react'; declare module '@elastic/eui' { - import { SFC, HTMLAttributes } from 'react'; - /** * text type defs * diff --git a/src/components/title/index.d.ts b/src/components/title/index.d.ts index bef381ec733..442bc2549fd 100644 --- a/src/components/title/index.d.ts +++ b/src/components/title/index.d.ts @@ -1,8 +1,7 @@ -/// +import { CommonProps } from '../common'; +import { SFC } from 'react'; declare module '@elastic/eui' { - import { SFC } from 'react'; - /** * title type defs * diff --git a/src/components/toast/index.d.ts b/src/components/toast/index.d.ts index ba0b0f9e8e3..2dc7d6511a3 100644 --- a/src/components/toast/index.d.ts +++ b/src/components/toast/index.d.ts @@ -1,4 +1,4 @@ -/// +import { CommonProps } from '../common'; /// import { Component, SFC, HTMLAttributes, ReactChild } from 'react'; diff --git a/src/components/token/index.d.ts b/src/components/token/index.d.ts index dbfdc7ad0c9..c2a8d6f98cf 100644 --- a/src/components/token/index.d.ts +++ b/src/components/token/index.d.ts @@ -1,19 +1,19 @@ -/// /// import { SFC, HTMLAttributes } from 'react'; +import { CommonProps } from '../common'; declare module '@elastic/eui' { /** * token type defs - * + * * @see './token.js' */ - + export type TokenSize = 's' | 'm' | 'l'; - export type TokenColor = + export type TokenColor = | 'tokenTint01' | 'tokenTint02' | 'tokenTint03' diff --git a/src/services/format/format_boolean.js b/src/services/format/format_boolean.ts similarity index 54% rename from src/services/format/format_boolean.js rename to src/services/format/format_boolean.ts index 05ada398b48..82c8847d400 100644 --- a/src/services/format/format_boolean.js +++ b/src/services/format/format_boolean.ts @@ -1,6 +1,6 @@ import { isNil } from '../predicate'; -export const formatBoolean = (value, { yes = 'Yes', no = 'No', nil = '' } = {}) => { +export const formatBoolean = (value: boolean, { yes = 'Yes', no = 'No', nil = '' } = {}) => { if (isNil(value)) { return nil; } diff --git a/src/services/predicate/common_predicates.js b/src/services/predicate/common_predicates.ts similarity index 56% rename from src/services/predicate/common_predicates.js rename to src/services/predicate/common_predicates.ts index 22196fdc010..9efc5f2366a 100644 --- a/src/services/predicate/common_predicates.js +++ b/src/services/predicate/common_predicates.ts @@ -4,26 +4,26 @@ export const always = () => true; export const never = () => false; -export const isUndefined = (value) => { +export const isUndefined = (value: any) => { return value === undefined; }; -export const isNull = (value) => { +export const isNull = (value: any) => { return value === null; }; -export const isNil = (value) => { +export const isNil = (value: any) => { return isUndefined(value) || isNull(value); }; -export const isMoment = (value) => { +export const isMoment = (value: any) => { return moment.isMoment(value); }; -export const isDate = (value) => { +export const isDate = (value: any) => { return moment.isDate(value); }; -export const isDateLike = (value) => { +export const isDateLike = (value: any) => { return isMoment(value) || isDate(value); }; diff --git a/src/services/predicate/index.js b/src/services/predicate/index.ts similarity index 100% rename from src/services/predicate/index.js rename to src/services/predicate/index.ts diff --git a/src/services/predicate/lodash_predicates.js b/src/services/predicate/lodash_predicates.ts similarity index 88% rename from src/services/predicate/lodash_predicates.js rename to src/services/predicate/lodash_predicates.ts index b3925795e4b..544cf9a7b2c 100644 --- a/src/services/predicate/lodash_predicates.js +++ b/src/services/predicate/lodash_predicates.ts @@ -5,5 +5,4 @@ export { isBoolean, isNumber, isNaN, - isPromise } from 'lodash'; diff --git a/src/test/required_props.js b/src/test/required_props.ts similarity index 83% rename from src/test/required_props.js rename to src/test/required_props.ts index 1d991b3d81e..16d0c6cba1c 100644 --- a/src/test/required_props.js +++ b/src/test/required_props.ts @@ -3,5 +3,5 @@ export const requiredProps = { 'aria-label': 'aria-label', 'className': 'testClass1 testClass2', - 'data-test-subj': 'test subject string' + 'data-test-subj': 'test subject string', }; diff --git a/src/webpack.config.js b/src/webpack.config.js index 66a556b4150..da5e8248d0e 100644 --- a/src/webpack.config.js +++ b/src/webpack.config.js @@ -2,6 +2,7 @@ const path = require('path'); const webpack = require('webpack'); const CircularDependencyPlugin = require('circular-dependency-plugin'); const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); +const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); const isProduction = process.env.NODE_ENV === 'production'; @@ -11,6 +12,12 @@ const plugins = [ exclude: /node_modules/, failOnError: true, }), + // run TypeScript and tslint during webpack build + new ForkTsCheckerWebpackPlugin({ + tsconfig: path.resolve(__dirname, '..', 'tsconfig.json'), + tslint: path.resolve(__dirname, '..', 'tslint.yaml'), + async: false, // makes errors more visible, but potentially less performant + }), ]; module.exports = { @@ -29,6 +36,10 @@ module.exports = { filename: `eui${isProduction ? '.min' : ''}.js` }, + resolve: { + extensions: ['.ts', '.tsx', '.js', '.json'], + }, + // Specify where these libraries should be found externals: { 'moment': 'window.moment', @@ -39,7 +50,7 @@ module.exports = { module: { rules: [{ - test: /\.js$/, + test: /\.(js|tsx?)$/, loader: 'babel-loader', exclude: /node_modules/ }, { diff --git a/tsconfig.json b/tsconfig.json index e93c7646d2a..ad523526e0c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,9 +1,7 @@ { "compilerOptions": { "baseUrl": ".", - - // necessary to output declaration files relative to the correct path - "rootDir": "./src", + "rootDir": ".", // emit files to the `types` dir, these are ignored by everything but TS needs _somewhere_ to emit "outDir": "types", diff --git a/tslint.yaml b/tslint.yaml index 7e7110bf835..ebd7dbf8c18 100644 --- a/tslint.yaml +++ b/tslint.yaml @@ -17,6 +17,8 @@ rules: } ] object-literal-sort-keys: false + object-literal-key-quotes: false interface-name: false - no-default-export: true quotemark: [true, "single", "jsx-double", "avoid-template", "avoid-escape"] + no-reference: false + member-access: [true, "no-public"] diff --git a/wiki/creating-components-manually.md b/wiki/creating-components-manually.md index bf0d9bc968d..fe4a799c5f5 100644 --- a/wiki/creating-components-manually.md +++ b/wiki/creating-components-manually.md @@ -12,8 +12,8 @@ This makes your styles available to your project and to the [EUI documentation][ ## Create the React component -1. Create the React component(s) in the same directory as the related SCSS file(s). -2. Export these components from an `index.js` file. +1. Create the React component(s) (preferably as TypeScript) in the same directory as the related SCSS file(s). +2. Export these components from an `index.ts` file. 3. Re-export these components from `src/components/index.js`. This makes your React component available for import into your project. diff --git a/yarn.lock b/yarn.lock index 45d598a1e5a..c8f3844f268 100644 --- a/yarn.lock +++ b/yarn.lock @@ -378,6 +378,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-syntax-typescript@^7.0.0": + version "7.1.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.1.5.tgz#956a1f43dec8a9d6b36221f5c865335555fdcb98" + integrity sha512-VqK5DFcS6/T8mT5CcJv1BwZLYFxkHiGZmP7Hs87F53lSToE/qfL7TpPrqFSaKyZi9w7Z/b/tmOGZZDupcJjFvw== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-arrow-functions@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.0.0.tgz#a6c14875848c68a3b4b3163a486535ef25c7e749" @@ -616,6 +623,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-transform-typescript@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.1.0.tgz#81e7b4be90e7317cbd04bf1163ebf06b2adee60b" + integrity sha512-TOTtVeT+fekAesiCHnPz+PSkYSdOSLyLn42DI45nxg6iCdlQY6LIj/tYqpMB0y+YicoTUiYiXqF8rG6SKfhw6Q== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-typescript" "^7.0.0" + "@babel/plugin-transform-unicode-regex@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.0.0.tgz#c6780e5b1863a76fe792d90eded9fcd5b51d68fc" @@ -691,6 +706,14 @@ "@babel/plugin-transform-react-jsx-self" "^7.0.0" "@babel/plugin-transform-react-jsx-source" "^7.0.0" +"@babel/preset-typescript@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.1.0.tgz#49ad6e2084ff0bfb5f1f7fb3b5e76c434d442c7f" + integrity sha512-LYveByuF9AOM8WrsNne5+N79k1YxjNB6gmpCQsnuSBAcV8QUeB+ZUxQzL7Rz7HksPbahymKkq2qBR+o36ggFZA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-typescript" "^7.1.0" + "@babel/template@7.0.0-beta.36": version "7.0.0-beta.36" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.36.tgz#02e903de5d68bd7899bce3c5b5447e59529abb00" @@ -826,6 +849,11 @@ resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.8.tgz#5702f74f78b73e13f1eb1bd435c2c9de61a250d4" integrity sha512-LzF540VOFabhS2TR2yYFz2Mu/fTfkA+5AwYddtJbOJGwnYrr2e7fHadT7/Z3jNGJJdCRlO3ySxmW26NgRdwhNA== +"@types/classnames@^2.2.6": + version "2.2.6" + resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.6.tgz#dbe8a666156d556ed018e15a4c65f08937c3f628" + integrity sha512-XHcYvVdbtAxVstjKxuULYqYaWIzHR15yr1pZj4fnGChuBVJlIAp9StJna0ZJNSgxPh4Nac2FL4JM3M11Tm6fqQ== + "@types/enzyme@^3.1.13": version "3.1.13" resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.1.13.tgz#4bbc5c81fa40c9fc7efee25c4a23cb37119a33ea" @@ -834,6 +862,16 @@ "@types/cheerio" "*" "@types/react" "*" +"@types/jest@^23.3.9": + version "23.3.9" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.3.9.tgz#c16b55186ee73ae65e001fbee69d392c51337ad1" + integrity sha512-wNMwXSUcwyYajtbayfPp55tSayuDVU6PfY5gzvRSj80UvxdXEJOVPnUVajaOp7NgXLm+1e2ZDLULmpsU9vDvQw== + +"@types/lodash@^4.14.116": + version "4.14.118" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.118.tgz#247bab39bfcc6d910d4927c6e06cbc70ec376f27" + integrity sha512-iiJbKLZbhSa6FYRip/9ZDX6HXhayXLDGY2Fqws9cOkEQ6XeKfaxB0sC541mowZJueYyMnVUmmG+al5/4fCDrgw== + "@types/node@*": version "9.3.0" resolved "https://registry.yarnpkg.com/@types/node/-/node-9.3.0.tgz#3a129cda7c4e5df2409702626892cb4b96546dd5" @@ -5305,6 +5343,20 @@ forever-agent@~0.6.1: resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= +fork-ts-checker-webpack-plugin@^0.4.4: + version "0.4.15" + resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-0.4.15.tgz#7cd9f94f3dd58cd1fe8f953f876e72090eda3f6d" + integrity sha512-qNYuygh2GxXehBvQZ5rI5YlQFn+7ZV6kmkyD9Sgs33dWl73NZdUOB5aCp8v0EXJn176AhPrZP8YCMT3h01fs+g== + dependencies: + babel-code-frame "^6.22.0" + chalk "^2.4.1" + chokidar "^2.0.4" + lodash "^4.17.11" + micromatch "^3.1.10" + minimatch "^3.0.4" + resolve "^1.5.0" + tapable "^1.0.0" + form-data@~2.1.1: version "2.1.4" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" @@ -8491,6 +8543,25 @@ micromatch@^2.1.5, micromatch@^2.3.11: parse-glob "^3.0.4" regex-cache "^0.4.2" +micromatch@^3.1.10, micromatch@^3.1.8, micromatch@^3.1.9: + version "3.1.10" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" + integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + braces "^2.3.1" + define-property "^2.0.2" + extend-shallow "^3.0.2" + extglob "^2.0.4" + fragment-cache "^0.2.1" + kind-of "^6.0.2" + nanomatch "^1.2.9" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.2" + micromatch@^3.1.4: version "3.1.5" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.5.tgz#d05e168c206472dfbca985bfef4f57797b4cd4ba" @@ -8510,25 +8581,6 @@ micromatch@^3.1.4: snapdragon "^0.8.1" to-regex "^3.0.1" -micromatch@^3.1.8, micromatch@^3.1.9: - version "3.1.10" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" - integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - braces "^2.3.1" - define-property "^2.0.2" - extend-shallow "^3.0.2" - extglob "^2.0.4" - fragment-cache "^0.2.1" - kind-of "^6.0.2" - nanomatch "^1.2.9" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.2" - miller-rabin@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"