├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.js ├── lib ├── parse.js └── toMarkdown.js ├── package.json └── test ├── fixtures ├── parse │ ├── default │ │ ├── Actual.js │ │ └── expected.json │ ├── description │ │ ├── Actual.js │ │ └── expected.json │ ├── example │ │ ├── Actual.js │ │ └── expected.json │ ├── optional │ │ ├── Actual.js │ │ └── expected.json │ ├── prop-description │ │ ├── Actual.js │ │ └── expected.json │ ├── require │ │ ├── Actual.js │ │ ├── User.js │ │ └── expected.json │ └── required │ │ ├── Actual.js │ │ └── expected.json └── toMarkdown │ ├── default │ └── expected.md │ ├── description │ └── expected.md │ ├── example │ └── expected.md │ ├── optional │ └── expected.md │ ├── prop-description │ └── expected.md │ ├── require │ └── expected.md │ └── required │ └── expected.md └── test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | stage: 0, 3 | loose: true, 4 | optional: ["runtime"] 5 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "env": { 4 | "browser": true, 5 | "node": true 6 | }, 7 | "ecmaFeatures": { 8 | "modules": true 9 | }, 10 | "rules": { 11 | "semi": 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | node_modules 3 | dev 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.log 3 | node_modules 4 | test 5 | dev 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | before_script: 5 | - export DISPLAY=:99.0 6 | - sh -e /etc/init.d/xvfb start -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | > **Tags:** 4 | > - [New Feature] 5 | > - [Bug Fix] 6 | > - [Breaking Change] 7 | > - [Documentation] 8 | > - [Internal] 9 | > - [Polish] 10 | > - [Experimental] 11 | 12 | **Note**: Gaps between patch versions are faulty/broken releases. 13 | 14 | # v0.9.3 15 | 16 | - **New Feature** 17 | - support all tcomb types, fix #40 (@gcanti) 18 | 19 | # v0.9.2 20 | 21 | - **Experimental** 22 | - add support for [babel-plugin-tcomb](https://github.com/gcanti/babel-plugin-tcomb), fix #32 (@gcanti) 23 | - the following exports and augmentations are deprecated: 24 | - `t` 25 | - `t.ReactElement` 26 | - `t.ReactNode` 27 | - `t.ReactChild` 28 | - `t.ReactChildren` 29 | 30 | 31 | # v0.9.1 32 | 33 | - **New Feature** 34 | - account for `interface`s (tcomb ^3.1.0) 35 | 36 | # v0.9.0 37 | 38 | **Warning**. If you don't rely in your codebase on the property `maybe(MyType)(undefined) === null` this **is not a breaking change** for you. 39 | 40 | - **Breaking Change** 41 | - upgrade to `tcomb-doc` v0.5.0 42 | - upgrade to `tcomb-validation` v3.0.0 43 | 44 | # v0.8.13 45 | 46 | - **Bug Fix** 47 | - `t.ReactChildren` rejects a correctly renderable `children`, fix #29 (thanks @FrancescoCioria) 48 | 49 | # v0.8.12 50 | 51 | - **New Feature** 52 | - Documentation tool: `parse` module, fix #24 53 | - Documentation tool: `toMarkdown` module, fix #25 54 | 55 | # v0.8.11 56 | 57 | - **New Feature** 58 | - attach the original predicate to `propTypes.__subtype__.predicate` so other components can read it 59 | - **Internal** 60 | - upgrade to latest `tcomb-validation` (2.2.0) 61 | - add tests for production env 62 | 63 | # v0.8.10 64 | 65 | - **New Feature** 66 | - `ReactChild` and `ReactChildren` pre-defined types, fix #19 (thanks @jedmao) 67 | 68 | # v0.8.9 69 | 70 | - **Internal** 71 | - upgrade to react v0.14.0-rc1 72 | 73 | # v0.8.8 74 | 75 | - **New Feature** 76 | + additional argument `options` to `propType()` to allow a finer configuration 77 | 78 | # v0.8.7 79 | 80 | - **New Feature** 81 | + Added support for childContextTypes and contextTypes (thanks [@gavacho](https://github.com/gavacho)) 82 | 83 | # v0.8.6 84 | 85 | - **New Feature** 86 | + added pre-defined types (#14): 87 | * `t.ReactElement` 88 | * `t.ReactNode` 89 | + `ReactElement` and `ReactNode` at top level are deprecated 90 | 91 | # v0.8.5 92 | 93 | - **New Feature** 94 | + added pre-defined types (#14): 95 | * `ReactElement` 96 | * `ReactNode` 97 | 98 | # v0.8.4 99 | 100 | - **Internal** 101 | + upgrade to latest version of tcomb-validation (2.0.0) 102 | 103 | # v0.8.3 104 | 105 | - **New Feature** 106 | + re-export tcomb 107 | 108 | # v0.8.2 109 | 110 | - **New Feature** 111 | + `propTypes` can also accept an object fix #12 (thanks @tehnomaag) 112 | 113 | # v0.8.1 114 | 115 | - **New Feature** (thanks @deoxxa) 116 | + show all the errors from tcomb-validation in warnings 117 | + report all additional properties, not just the first 118 | + retain (limited) proptypes in production 119 | 120 | # v0.8.0 121 | 122 | - **Breaking Change** 123 | + upgrade to tcomb-validation v2.0 124 | 125 | # v0.5.0 126 | 127 | - **New Feature** 128 | + If you try to pass additional props it will fail 129 | 130 | # v0.4.0 131 | 132 | - **New Feature** 133 | + Add @props ES7 decorator 134 | - **Breaking Change** 135 | + Remove `react` and `t` namespace 136 | + Remove `ReactElement` and `ReactNode` type 137 | + Remove `React` dependency 138 | 139 | # v0.3.0 140 | 141 | - **Internal** 142 | + Upgrade to latest tcomb-validation 143 | + Remove tcomb-form and react-tools dependencies #7 144 | - **Polish** 145 | + Remove Playground [BREAKING] 146 | 147 | # v0.2.4 148 | 149 | - **New Feature** 150 | + make propTypes introspectable by adding a tcomb property #6 151 | 152 | # v0.2.3 153 | 154 | - **Bug Fix** 155 | + fix a bug when propTypes is a subtype 156 | 157 | # v0.2.2 158 | 159 | - **Polish** 160 | + move tcomb-validation and tcomb-form to peerDependencies, fix #5 161 | + update to tcomb-form v0.4.5 162 | 163 | # v0.2.1 164 | 165 | - **Polish** 166 | + update to tcomb-form v0.4.2 167 | 168 | # v0.2.0 169 | 170 | - **Internal** 171 | + complete refactoring 172 | 173 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Giulio Canti 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![build status](https://img.shields.io/travis/gcanti/tcomb-react/master.svg?style=flat-square)](https://travis-ci.org/gcanti/tcomb-react) 2 | [![dependency status](https://img.shields.io/david/gcanti/tcomb-react.svg?style=flat-square)](https://david-dm.org/gcanti/tcomb-react) 3 | ![npm downloads](https://img.shields.io/npm/dm/tcomb-react.svg) 4 | 5 | # Features 6 | 7 | - **by default props are required**, a saner default since it's quite easy to forget `.isRequired` 8 | - **checks for unwanted additional props** 9 | - **documentation** (types and comments) can be automatically extracted 10 | - additional fine grained type checks, nestable at arbitrary level 11 | - builds on [tcomb](https://github.com/gcanti/tcomb), [tcomb-validation](https://github.com/gcanti/tcomb-validation), [tcomb-doc](https://github.com/gcanti/tcomb-doc) libraries 12 | 13 | # Compatibility 14 | `tcomb-react` has been tested and found working on the following targets. 15 | The list is not exhaustive and `tcomb-react` will probably work on other versions that haven't been listed. 16 | 17 | React: `^0.13.0`, `^0.14.0`, `^15.0.0` 18 | 19 | # Prop types 20 | 21 | ## The `@props` decorator (ES7) 22 | 23 | For an equivalent implementation in ES5, or for Stateless Components, see the `propTypes` function below. 24 | 25 | **Signature** 26 | 27 | ```js 28 | type Props = {[key: string]: TcombType}; 29 | 30 | type PropsType = TcombStruct | TcombInterface; 31 | 32 | type Type = Props | PropsType | Refinement; 33 | 34 | type Options = { 35 | strict?: boolean // default true 36 | }; 37 | 38 | @props(type: Type, options?: Options) 39 | ``` 40 | 41 | where 42 | 43 | - `type` can be a map `string -> TcombType`, a `tcomb` struct, a `tcomb` interface, a refinement of a `tcomb` struct / interface, a refinement of a `tcomb` interface 44 | - `options`: 45 | - `strict: boolean` (default `true`) if `true` checks for unwanted additional props 46 | 47 | **Example** (ES7) 48 | 49 | ```js 50 | import t from 'tcomb' 51 | import { props } from 'tcomb-react' 52 | 53 | const Gender = t.enums.of(['Male', 'Female'], 'Gender') 54 | const URL = t.refinement(t.String, (s) => s.startsWith('http'), 'URL') 55 | 56 | @props({ 57 | name: t.String, // a required string 58 | surname: t.maybe(t.String), // an optional string 59 | age: t.Number, // a required number 60 | gender: Gender, // an enum 61 | avatar: URL // a refinement 62 | }) 63 | class Card extends React.Component { 64 | 65 | render() { 66 | return ( 67 |
68 |

{this.props.name}

69 | ... 70 |
71 | ) 72 | } 73 | 74 | } 75 | ``` 76 | 77 | **Unwanted additional props** 78 | 79 | By default `tcomb-react` checks for unwanted additional props: 80 | 81 | ```js 82 | @props({ 83 | name: t.String 84 | }) 85 | class Person extends React.Component { 86 | 87 | render() { 88 | return ( 89 |
90 |

{this.props.name}

91 |
92 | ) 93 | } 94 | 95 | } 96 | 97 | ... 98 | 99 | 100 | ``` 101 | 102 | **Output** 103 | 104 | ``` 105 | Warning: Failed propType: [tcomb] Invalid additional prop(s): 106 | 107 | [ 108 | "surname" 109 | ] 110 | 111 | supplied to Person. 112 | ``` 113 | 114 | **Note**. You can **opt-out** passing the `option` argument `{ strict: false }`. 115 | 116 | ## The `propTypes` function 117 | 118 | **Signature** 119 | 120 | Same as `@props`. 121 | 122 | **Stateless Component Example** 123 | 124 | ```js 125 | import { propTypes } from 'tcomb-react' 126 | 127 | const MyComponentProps = t.interface({ 128 | name: t.String, 129 | }); 130 | 131 | const MyComponent = (props) => ( 132 |
133 | ); 134 | MyComponent.propTypes = propTypes(MyComponentProps); 135 | ``` 136 | 137 | **ES5 `React.createClass` Example** 138 | 139 | ```js 140 | var t = require('tcomb'); 141 | var propTypes = require('tcomb-react').propTypes; 142 | 143 | var Gender = t.enums.of(['Male', 'Female'], 'Gender'); 144 | var URL = t.refinement(t.String, function (s) { return s.startsWith('http'); }, 'URL'); 145 | 146 | var Card = React.createClass({ 147 | 148 | propTypes: propTypes({ 149 | name: t.String, // a required string 150 | surname: t.maybe(t.String), // an optional string 151 | age: t.Number, // a required number 152 | gender: Gender, // an enum 153 | avatar: URL // a refinement 154 | }), 155 | 156 | render: function () { 157 | return ( 158 |
159 |

{this.props.name}

160 | ... 161 |
162 | ); 163 | } 164 | 165 | }); 166 | ``` 167 | 168 | ## How it works 169 | 170 | The `@props` decorator sets `propTypes` on the target component to use a [custom validator function](https://facebook.github.io/react/docs/reusable-components.html#prop-validation) built around tcomb types for each specified prop. 171 | 172 | For example, the following: 173 | 174 | ```js 175 | const URL = t.refinement(t.String, (s) => s.startsWith('http'), 'URL'); 176 | 177 | @props({ 178 | name: t.String, 179 | url: URL, 180 | }) 181 | class MyComponent extends React.Component { 182 | // ... 183 | } 184 | ``` 185 | 186 | is roughly equivalent to: 187 | 188 | ```js 189 | const URL = t.refinement(t.String, (s) => s.startsWith('http'), 'URL'); 190 | 191 | class MyComponent extends React.Component { 192 | // ... 193 | } 194 | MyComponent.propTypes = { 195 | name: function(props, propName, componentName) { 196 | if (!t.validate(props[propName], t.String).isValid()) { 197 | return new Error('...'); 198 | } 199 | }, 200 | url: function(props, propName, componentName) { 201 | if (!t.validate(props[propName], URL).isValid()) { 202 | return new Error('...'); 203 | } 204 | }, 205 | } 206 | ``` 207 | 208 | ## The babel plugin 209 | 210 | Using [babel-plugin-tcomb](https://github.com/gcanti/babel-plugin-tcomb) you can express `propTypes` as Flow type annotations: 211 | 212 | ```js 213 | import React from 'react' 214 | import ReactDOM from 'react-dom' 215 | import type { $Refinement } from 'tcomb' 216 | import { props } from 'tcomb-react' 217 | 218 | type Gender = 'Male' | 'Female'; 219 | 220 | const isUrl = (s) => s.startsWith('http') 221 | type URL = string & $Refinement; 222 | 223 | type Props = { 224 | name: string, 225 | surname: ?string, 226 | age: number, 227 | gender: Gender, 228 | avatar: URL 229 | }; 230 | 231 | @props(Props) 232 | class Card extends React.Component { 233 | 234 | render() { 235 | return ( 236 |
237 |

{this.props.name}

238 | ... 239 |
240 | ) 241 | } 242 | 243 | } 244 | ``` 245 | 246 | # Extract documentation from your components 247 | 248 | ## The `parse` function 249 | 250 | Given a path to a component file returns a JSON / JavaScript blob containing **props types, default values and comments**. 251 | 252 | **Signature** 253 | 254 | ```js 255 | (path: string | Array) => Object 256 | ``` 257 | 258 | **Example** 259 | 260 | Source 261 | 262 | ```js 263 | import t from 'tcomb' 264 | import { props } from 'tcomb-react' 265 | 266 | /** 267 | * Component description here 268 | * @param name - name description here 269 | * @param surname - surname description here 270 | */ 271 | 272 | @props({ 273 | name: t.String, // a required string 274 | surname: t.maybe(t.String) // an optional string 275 | }) 276 | export default class Card extends React.Component { 277 | 278 | static defaultProps = { 279 | surname: 'Canti' // default value for surname prop 280 | } 281 | 282 | render() { 283 | return ( 284 |
285 |

{this.props.name}

286 |

{this.props.surname}

287 |
288 | ) 289 | } 290 | } 291 | ``` 292 | 293 | Usage 294 | 295 | ```js 296 | import parse from 'tcomb-react/lib/parse' 297 | const json = parse('./components/Card.js') 298 | console.log(JSON.stringify(json, null, 2)) 299 | ``` 300 | 301 | Output 302 | 303 | ```json 304 | { 305 | "name": "Card", 306 | "description": "Component description here", 307 | "props": { 308 | "name": { 309 | "kind": "irreducible", 310 | "name": "String", 311 | "required": true, 312 | "description": "name description here" 313 | }, 314 | "surname": { 315 | "kind": "irreducible", 316 | "name": "String", 317 | "required": false, 318 | "defaultValue": "Canti", 319 | "description": "surname description here" 320 | } 321 | } 322 | } 323 | ``` 324 | 325 | **Note**. Since `parse` uses runtime type introspection, your components should be `require`able from your script (you may be required to shim the browser environment). 326 | 327 | **Parsing multiple components** 328 | 329 | ```js 330 | import parse from 'tcomb-react/lib/parse' 331 | import path from 'path' 332 | import glob from 'glob' 333 | 334 | function getPath(file) { 335 | return path.resolve(process.cwd(), file); 336 | } 337 | 338 | parse(glob.sync('./components/*.js').map(getPath)); 339 | ``` 340 | 341 | ## The `toMarkdown` function 342 | 343 | Given a JSON / JavaScript blob returned by `parse` returns a markdown containing the components documentation. 344 | 345 | **Signature** 346 | 347 | ```js 348 | (json: Object) => string 349 | ``` 350 | 351 | **Example** 352 | 353 | Usage 354 | 355 | ```js 356 | import parse from 'tcomb-react/lib/parse' 357 | import toMarkdown from 'tcomb-react/lib/toMarkdown' 358 | const json = parse('./components/Card.js') 359 | console.log(toMarkdown(json)); 360 | ``` 361 | 362 | Output 363 | 364 | ```markdown 365 | ## Card 366 | 367 | Component description here 368 | 369 | **Props** 370 | 371 | - `name: String` name description here 372 | - `surname: String` (optional, default: `"Canti"`) surname description here 373 | 374 | ``` 375 | 376 | # Augmented pre-defined types 377 | 378 | `tcomb-react` exports some useful pre-defined types: 379 | 380 | - `ReactElement` 381 | - `ReactNode` 382 | - `ReactChild` 383 | - `ReactChildren` 384 | 385 | **Example** 386 | 387 | ```js 388 | import { props, ReactChild } from 'tcomb-react'; 389 | 390 | @props({ 391 | children: ReactChild // only one child is allowed 392 | }) 393 | class MyComponent extends React.Component { 394 | 395 | render() { 396 | return ( 397 |
398 | {this.props.children} 399 |
400 | ); 401 | } 402 | 403 | } 404 | ``` 405 | 406 | # Support for babel-plugin-tcomb 407 | 408 | The following types for Flow are exported: 409 | 410 | - `ReactElementT` 411 | - `ReactNodeT` 412 | - `ReactChildT` 413 | - `ReactChildrenT` 414 | 415 | # Comparison table 416 | 417 | | Type | React | tcomb-react | 418 | |------|-------|-------------| 419 | | array | array | Array | 420 | | boolean | bool | Boolean | 421 | | functions | func | Function | 422 | | numbers | number | Number | 423 | | objects | object | Object | 424 | | strings | string | String | 425 | | all | any | Any | 426 | | required prop | T.isRequired | T | 427 | | optional prop | T | maybe(T) | 428 | | custom types | ✘ | ✓ | 429 | | tuples | ✘ | tuple([T, U, ...]) | 430 | | lists | arrayOf(T) | list(T) | 431 | | instance | instanceOf(A) | T | 432 | | dictionaries | objectOf(T) | dict(T, U) (keys are checked) | 433 | | enums | oneOf(['a', 'b']) | enums.of('a b') | 434 | | unions | oneOfType([T, U]) | union([T, U]) | 435 | | duck typing | shape | interface | 436 | | react element | element | ReactElement | 437 | | react node | node | ReactNode | 438 | | react child | ✘ | ReactChild | 439 | | react children | ✘ | ReactChildren | 440 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var t = require('tcomb-validation'); 3 | 4 | function getMessage(errors, what, displayName, type) { 5 | return [ 6 | 'Invalid ' + what + ' supplied to ' + displayName + ', should be a ' + t.getTypeName(type) + '.\n', 7 | 'Detected errors (' + errors.length + '):\n' 8 | ].concat(errors.map(function (e, i) { 9 | return ' ' + (i + 1) + '. ' + e.message; 10 | })).join('\n') + '\n\n'; 11 | } 12 | 13 | // 14 | // main function 15 | // 16 | 17 | function getPropTypes(type, options) { 18 | 19 | // getPropTypes can also accept a dictionary prop -> type 20 | if (t.Object.is(type)) { 21 | type = t.struct(type); 22 | } 23 | 24 | var isSubtype = ( type.meta.kind === 'subtype' ); 25 | 26 | // here type should be a struct or a subtype of a struct 27 | if (process.env.NODE_ENV !== 'production') { 28 | t.assert( 29 | t.isType(type), 30 | '[tcomb-react] Invalid argument type supplied to propTypes()' 31 | ); 32 | } 33 | 34 | var propTypes = {}; 35 | var innerType = isSubtype ? type.meta.type : type; 36 | 37 | if (innerType.meta.kind === 'struct' || innerType.meta.kind === 'interface') { 38 | var props = innerType.meta.props; 39 | 40 | Object.keys(props).forEach(function (k) { 41 | 42 | var propType = props[k]; 43 | var checkPropType; 44 | 45 | if (process.env.NODE_ENV !== 'production') { 46 | 47 | // React custom prop validators 48 | // see http://facebook.github.io/react/docs/reusable-components.html 49 | checkPropType = function (values, prop, displayName) { 50 | 51 | var value = values[prop]; 52 | var validationResult = t.validate(value, propType); 53 | 54 | if (!validationResult.isValid()) { 55 | 56 | var message = getMessage(validationResult.errors, 'prop ' + t.stringify(prop), displayName, propType); 57 | 58 | // add a readable entry in the call stack 59 | // when "Pause on exceptions" and "Pause on Caught Exceptions" 60 | // are enabled in Chrome DevTools 61 | checkPropType.displayName = message; 62 | 63 | t.fail(message); 64 | } 65 | }; 66 | } else { 67 | checkPropType = function () {}; 68 | } 69 | 70 | // attach the original tcomb definition, so other components can read it 71 | // via `propTypes.whatever.tcomb` 72 | checkPropType.tcomb = propType; 73 | 74 | propTypes[k] = checkPropType; 75 | }); 76 | 77 | if (process.env.NODE_ENV !== 'production') { 78 | options = options || {}; 79 | // allows to opt-out additional props check 80 | if (options.strict !== false) { 81 | propTypes.__strict__ = function (values, prop, displayName) { 82 | var extra = []; 83 | for (var k in values) { 84 | // __strict__ and __subtype__ keys are excluded in order to support the React context feature 85 | if (k !== '__strict__' && k !== '__subtype__' && values.hasOwnProperty(k) && !props.hasOwnProperty(k)) { 86 | extra.push(k); 87 | } 88 | } 89 | if (extra.length > 0) { 90 | t.fail('Invalid additional prop(s):\n\n' + t.stringify(extra) + '\n\nsupplied to ' + displayName + '.'); 91 | } 92 | }; 93 | } 94 | } 95 | 96 | } 97 | else { 98 | if (process.env.NODE_ENV !== 'production') { 99 | propTypes.__generictype__ = function (values, prop, displayName) { 100 | var validationResult = t.validate(values, innerType); 101 | if (!validationResult.isValid()) { 102 | t.fail(getMessage(validationResult.errors, 'props', displayName, innerType)); 103 | } 104 | }; 105 | } 106 | } 107 | 108 | if (isSubtype) { 109 | if (process.env.NODE_ENV !== 'production') { 110 | propTypes.__subtype__ = function (values, prop, displayName) { 111 | if (!type.meta.predicate(values)) { 112 | t.fail('Invalid props:\n\n' + t.stringify(values) + '\n\nsupplied to ' + displayName + ', should be a ' + t.getTypeName(type) + ' subtype.'); 113 | } 114 | }; 115 | } else { 116 | propTypes.__subtype__ = function () {}; 117 | } 118 | 119 | // attach the original predicate, so other components can read it 120 | // via `propTypes.__subtype__.predicate` 121 | propTypes.__subtype__.predicate = type.meta.predicate; 122 | } 123 | 124 | return propTypes; 125 | } 126 | 127 | // 128 | // ES7 decorator 129 | // 130 | 131 | function es7PropsDecorator(type, options) { 132 | return function (Component) { 133 | Component.propTypes = getPropTypes(type, options); 134 | }; 135 | } 136 | 137 | // 138 | // Built-in types 139 | // 140 | 141 | var ReactElement = t.irreducible('ReactElement', React.isValidElement); 142 | var ReactNode = t.irreducible('ReactNode', function (x) { 143 | return t.Str.is(x) || t.Num.is(x) || ReactElement.is(x) || t.list(ReactNode).is(x); 144 | }); 145 | var ReactChild = t.irreducible('ReactChild', function(x) { 146 | return ReactNode.is(x) || t.Bool.is(x) || t.Nil.is(x); 147 | }); 148 | var ReactChildren = t.irreducible('ReactChildren', function(x) { 149 | return ReactChild.is(x) || t.list(ReactChildren).is(x); 150 | }); 151 | 152 | t.ReactElement = ReactElement; // deprecated 153 | t.ReactNode = ReactNode; // deprecated 154 | t.ReactChild = ReactChild; // deprecated 155 | t.ReactChildren = ReactChildren; // deprecated 156 | 157 | module.exports = { 158 | t: t, // deprecated 159 | propTypes: getPropTypes, 160 | props: es7PropsDecorator, 161 | ReactElement: ReactElement, 162 | ReactNode: ReactNode, 163 | ReactChild: ReactChild, 164 | ReactChildren: ReactChildren, 165 | ReactElementT: ReactElement, 166 | ReactNodeT: ReactNode, 167 | ReactChildT: ReactChild, 168 | ReactChildrenT: ReactChildren 169 | }; 170 | -------------------------------------------------------------------------------- /lib/parse.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var t = require('../').t; 3 | var toObject = require('tcomb-doc').toObject; 4 | var parseComments = require('get-comments'); 5 | var parseJSDocs = require('doctrine').parse; 6 | 7 | // (path: t.String) => { description: t.String, tags: Array } 8 | function getComments(path) { 9 | var source = fs.readFileSync(path, 'utf8'); 10 | var comments = parseComments(source, true); 11 | var values = comments.map(function (comment) { 12 | return comment.value; 13 | }); 14 | return parseJSDocs(values.join('\n'), { unwrap: true }); 15 | } 16 | 17 | // (comments: t.Object) => { component, props: {} } 18 | function getDescriptions(comments) { 19 | var ret = { 20 | component: comments.description || null, 21 | props: {} 22 | }; 23 | comments.tags.forEach(function (tag) { 24 | if (tag.name) { 25 | ret.props[tag.name] = tag.description || null; 26 | } 27 | }); 28 | return ret; 29 | } 30 | 31 | // (exports: t.Object) => ReactComponent 32 | function getComponent(defaultExport) { 33 | if (defaultExport['default']) { // eslint-disable-line dot-notation 34 | defaultExport = defaultExport['default']; // eslint-disable-line dot-notation 35 | } 36 | return defaultExport.propTypes ? defaultExport : null; 37 | } 38 | 39 | // (component: ReactComponent) => t.String 40 | function getComponentName(component) { 41 | return component.name; 42 | } 43 | 44 | // (component: ReactComponent) => TcombType 45 | function getPropsType(component) { 46 | var propTypes = component.propTypes; 47 | var props = {}; 48 | Object.keys(propTypes).forEach(function (k) { 49 | if (k !== '__strict__' && k !== '__subtype__') { 50 | props[k] = propTypes[k].tcomb; 51 | } 52 | }); 53 | if (propTypes.hasOwnProperty('__subtype__')) { 54 | return t.refinement(t.struct(props), propTypes.__subtype__.predicate); 55 | } 56 | return t.struct(props); 57 | } 58 | 59 | // (component: ReactComponent) => t.Object 60 | function getDefaultProps(component) { 61 | return component.defaultProps || {}; 62 | } 63 | 64 | // (path: t.String) => { name, description, props } 65 | function parse(path) { 66 | if (t.Array.is(path)) { 67 | return path.map(parse).filter(Boolean); 68 | } 69 | var component = getComponent(require(path)); 70 | if (component) { 71 | var comments = getComments(path); 72 | var descriptions = getDescriptions(comments); 73 | var type = toObject(getPropsType(component)); 74 | var props = type.kind === 'refinement' ? type.type.props : type.props; 75 | var defaultProps = getDefaultProps(component); 76 | var name = getComponentName(component); 77 | for (var prop in props) { 78 | if (props.hasOwnProperty(prop)) { 79 | if (defaultProps.hasOwnProperty(prop)) { 80 | props[prop].defaultValue = defaultProps[prop]; 81 | } 82 | if (descriptions.props.hasOwnProperty(prop)) { 83 | props[prop].description = descriptions.props[prop]; 84 | } 85 | } 86 | } 87 | return { 88 | name: name, 89 | description: descriptions.component, 90 | props: props 91 | }; 92 | } 93 | } 94 | 95 | module.exports = parse; 96 | -------------------------------------------------------------------------------- /lib/toMarkdown.js: -------------------------------------------------------------------------------- 1 | var t = require('../').t; 2 | 3 | function getProps(props) { 4 | return Object.keys(props).map(function (k) { 5 | var prop = props[k]; 6 | return '- `' + k + ': ' + prop.name + '` ' + (prop.required ? '' : '(optional' + (t.Nil.is(prop.defaultValue) ? '' : ', default: `' + JSON.stringify(prop.defaultValue) + '`') + ') ') + ( prop.description || ''); 7 | }).join('\n'); 8 | } 9 | 10 | function toMarkdown(json) { 11 | if (t.Array.is(json)) { 12 | return json.map(toMarkdown).join('\n'); 13 | } 14 | return '## ' + json.name + '\n' + (json.description ? '\n' + json.description + '\n' : '') + '\n**Props**\n\n' + getProps(json.props) + '\n'; 15 | } 16 | 17 | module.exports = toMarkdown; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tcomb-react", 3 | "version": "0.9.3", 4 | "description": "Type checking for React components", 5 | "main": "index.js", 6 | "files": [ 7 | "index.js", 8 | "lib" 9 | ], 10 | "scripts": { 11 | "lint": "eslint index.js lib", 12 | "test": "mocha --compilers js:babel/register" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/gcanti/tcomb-react.git" 17 | }, 18 | "author": "Giulio Canti ", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/gcanti/tcomb-react/issues" 22 | }, 23 | "homepage": "https://github.com/gcanti/tcomb-react", 24 | "dependencies": { 25 | "doctrine": "0.7.2", 26 | "get-comments": "1.0.1", 27 | "tcomb-doc": "^0.5.0", 28 | "tcomb-validation": "^3.0.0", 29 | "react": ">=0.13.0" 30 | }, 31 | "devDependencies": { 32 | "babel": "5.8.34", 33 | "babel-core": "5.8.34", 34 | "babel-eslint": "3.1.30", 35 | "babel-runtime": "5.8.34", 36 | "eslint": "^3.4.0", 37 | "mocha": "2.3.2" 38 | }, 39 | "tags": [ 40 | "tcomb", 41 | "react", 42 | "react-component" 43 | ], 44 | "keywords": [ 45 | "tcomb", 46 | "react", 47 | "react-component" 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /test/fixtures/parse/default/Actual.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { props, t } from '../../../../.'; 3 | 4 | @props({ 5 | name: t.maybe(t.String) 6 | }) 7 | export default class Component extends React.Component { 8 | 9 | static defaultProps = { 10 | name: 'Giulio' 11 | } 12 | 13 | render() { 14 | return ( 15 |
16 |

{this.props.name}

17 |
18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/fixtures/parse/default/expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Component", 3 | "description": null, 4 | "props": { 5 | "name": { 6 | "kind": "irreducible", 7 | "name": "String", 8 | "required": false, 9 | "defaultValue": "Giulio" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /test/fixtures/parse/description/Actual.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { props, t } from '../../../../.'; 3 | 4 | /** 5 | * component description 6 | */ 7 | 8 | @props({ 9 | name: t.String 10 | }) 11 | export default class Component extends React.Component { 12 | 13 | render() { 14 | return ( 15 |
16 |

{this.props.name}

17 |
18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/fixtures/parse/description/expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Component", 3 | "description": "component description", 4 | "props": { 5 | "name": { 6 | "kind": "irreducible", 7 | "name": "String", 8 | "required": true 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /test/fixtures/parse/example/Actual.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { props, t } from '../../../../.'; 3 | 4 | /** 5 | * Component description here 6 | * @param name - name description here 7 | * @param surname - surname description here 8 | */ 9 | 10 | @props({ 11 | name: t.String, // a required string 12 | surname: t.maybe(t.String) // an optional string 13 | }) 14 | export default class Card extends React.Component { 15 | 16 | static defaultProps = { 17 | surname: 'Canti' 18 | } 19 | 20 | render() { 21 | return ( 22 |
23 |

{this.props.name}

24 |

{this.props.surname}

25 |
26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/fixtures/parse/example/expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Card", 3 | "description": "Component description here", 4 | "props": { 5 | "name": { 6 | "kind": "irreducible", 7 | "name": "String", 8 | "required": true, 9 | "description": "name description here" 10 | }, 11 | "surname": { 12 | "kind": "irreducible", 13 | "name": "String", 14 | "required": false, 15 | "defaultValue": "Canti", 16 | "description": "surname description here" 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /test/fixtures/parse/optional/Actual.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { props, t } from '../../../../.'; 3 | 4 | @props({ 5 | name: t.maybe(t.String) 6 | }) 7 | export default class Component extends React.Component { 8 | 9 | render() { 10 | return ( 11 |
12 |

{this.props.name}

13 |
14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/fixtures/parse/optional/expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Component", 3 | "description": null, 4 | "props": { 5 | "name": { 6 | "kind": "irreducible", 7 | "name": "String", 8 | "required": false 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /test/fixtures/parse/prop-description/Actual.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { props, t } from '../../../../.'; 3 | 4 | /** 5 | * @param name - prop description 6 | */ 7 | 8 | @props({ 9 | name: t.String 10 | }) 11 | export default class Component extends React.Component { 12 | 13 | render() { 14 | return ( 15 |
16 |

{this.props.name}

17 |
18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/fixtures/parse/prop-description/expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Component", 3 | "description": null, 4 | "props": { 5 | "name": { 6 | "kind": "irreducible", 7 | "name": "String", 8 | "required": true, 9 | "description": "prop description" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /test/fixtures/parse/require/Actual.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { props, t } from '../../../../.'; 3 | import User from './User'; 4 | 5 | /** 6 | * Component description here 7 | * name and surname must be both nil or both specified 8 | * @param name - name description 9 | * @param surname - surname description 10 | */ 11 | 12 | const Props = t.refinement(t.struct({ 13 | name: t.maybe(User.meta.props.name), 14 | surname: t.maybe(User.meta.props.surname) 15 | }), (x) => t.Nil.is(x.name) === t.Nil.is(x.surname)); 16 | 17 | @props(Props) 18 | export default class Component extends React.Component { 19 | 20 | static defaultProps = { 21 | name: 'Giulio', 22 | surname: 'Canti' 23 | } 24 | 25 | render() { 26 | return ( 27 |
28 |

{this.props.name}

29 |

{this.props.surname}

30 |
31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/fixtures/parse/require/User.js: -------------------------------------------------------------------------------- 1 | import { t } from '../../../../.'; 2 | 3 | export default t.struct({ 4 | name: t.String, 5 | surname: t.String 6 | }, 'User'); 7 | -------------------------------------------------------------------------------- /test/fixtures/parse/require/expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Component", 3 | "description": "Component description here\nname and surname must be both nil or both specified", 4 | "props": { 5 | "name": { 6 | "kind": "irreducible", 7 | "name": "String", 8 | "required": false, 9 | "defaultValue": "Giulio", 10 | "description": "name description" 11 | }, 12 | "surname": { 13 | "kind": "irreducible", 14 | "name": "String", 15 | "required": false, 16 | "defaultValue": "Canti", 17 | "description": "surname description" 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /test/fixtures/parse/required/Actual.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { props, t } from '../../../../.'; 3 | 4 | @props({ 5 | name: t.String 6 | }) 7 | export default class Component extends React.Component { 8 | 9 | render() { 10 | return ( 11 |
12 |

{this.props.name}

13 |
14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/fixtures/parse/required/expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Component", 3 | "description": null, 4 | "props": { 5 | "name": { 6 | "kind": "irreducible", 7 | "name": "String", 8 | "required": true 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /test/fixtures/toMarkdown/default/expected.md: -------------------------------------------------------------------------------- 1 | ## Component 2 | 3 | **Props** 4 | 5 | - `name: String` (optional, default: `"Giulio"`) 6 | -------------------------------------------------------------------------------- /test/fixtures/toMarkdown/description/expected.md: -------------------------------------------------------------------------------- 1 | ## Component 2 | 3 | component description 4 | 5 | **Props** 6 | 7 | - `name: String` 8 | -------------------------------------------------------------------------------- /test/fixtures/toMarkdown/example/expected.md: -------------------------------------------------------------------------------- 1 | ## Card 2 | 3 | Component description here 4 | 5 | **Props** 6 | 7 | - `name: String` name description here 8 | - `surname: String` (optional, default: `"Canti"`) surname description here -------------------------------------------------------------------------------- /test/fixtures/toMarkdown/optional/expected.md: -------------------------------------------------------------------------------- 1 | ## Component 2 | 3 | **Props** 4 | 5 | - `name: String` (optional) 6 | -------------------------------------------------------------------------------- /test/fixtures/toMarkdown/prop-description/expected.md: -------------------------------------------------------------------------------- 1 | ## Component 2 | 3 | **Props** 4 | 5 | - `name: String` prop description 6 | -------------------------------------------------------------------------------- /test/fixtures/toMarkdown/require/expected.md: -------------------------------------------------------------------------------- 1 | ## Component 2 | 3 | Component description here 4 | name and surname must be both nil or both specified 5 | 6 | **Props** 7 | 8 | - `name: String` (optional, default: `"Giulio"`) name description 9 | - `surname: String` (optional, default: `"Canti"`) surname description -------------------------------------------------------------------------------- /test/fixtures/toMarkdown/required/expected.md: -------------------------------------------------------------------------------- 1 | ## Component 2 | 3 | **Props** 4 | 5 | - `name: String` 6 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /* global describe,it */ 2 | 'use strict'; 3 | var assert = require('assert'); 4 | var doesNotThrow = assert.doesNotThrow; 5 | var React = require('react'); 6 | var t = require('tcomb-validation'); 7 | var library = require('../index'); 8 | var getPropTypes = library.propTypes; 9 | var ReactElement = library.ReactElement; 10 | var ReactNode = library.ReactNode; 11 | var path = require('path'); 12 | var fs = require('fs'); 13 | var parse = require('../lib/parse'); 14 | var toMarkdown = require('../lib/toMarkdown'); 15 | 16 | function throwsWithMessage(f, message) { 17 | assert.throws(f, function (err) { 18 | assert.ok(err instanceof Error); 19 | assert.strictEqual(err.message, message); 20 | return true; 21 | }); 22 | } 23 | 24 | function runPropTypes(propTypes, props) { 25 | for (var prop in propTypes) { 26 | propTypes[prop](props, prop, ''); 27 | } 28 | } 29 | 30 | function production(f) { 31 | return function () { 32 | process.env.NODE_ENV = 'production'; 33 | try { 34 | f(); 35 | } catch (e) { 36 | assert.fail(e.message); 37 | } 38 | finally { 39 | process.env.NODE_ENV = 'development'; 40 | } 41 | }; 42 | } 43 | 44 | describe('exports', function () { 45 | 46 | it('should export tcomb', function () { 47 | assert.ok(library.t === t); 48 | }); 49 | 50 | it('should export propTypes function', function () { 51 | assert.ok(typeof getPropTypes === 'function'); 52 | }); 53 | 54 | it('should export the es7 decorator', function () { 55 | assert.ok(typeof library.props === 'function'); 56 | }); 57 | 58 | it('should export ReactElement type', function () { 59 | assert.ok(ReactElement.meta.kind === 'irreducible'); 60 | }); 61 | 62 | it('should export ReactNode type', function () { 63 | assert.ok(ReactNode.meta.kind === 'irreducible'); 64 | }); 65 | 66 | }); 67 | 68 | describe('propTypes', function () { 69 | 70 | it('should check bad values', function () { 71 | var T = t.struct({name: t.String}); 72 | var propTypes = getPropTypes(T); 73 | assert.ok(typeof propTypes === 'object'); 74 | assert.deepEqual(Object.keys(propTypes), ['name', '__strict__']); 75 | throwsWithMessage(function () { 76 | runPropTypes(propTypes, {}); 77 | }, '[tcomb] Invalid prop "name" supplied to , should be a String.\n\nDetected errors (1):\n\n 1. Invalid value undefined supplied to String\n\n'); 78 | doesNotThrow(function () { 79 | runPropTypes(propTypes, {name: 'a'}); 80 | }); 81 | }); 82 | 83 | it('should accept a hash of props instead of a struct', function () { 84 | var propTypes = getPropTypes({name: t.String}); 85 | throwsWithMessage(function () { 86 | runPropTypes(propTypes, {}); 87 | }, '[tcomb] Invalid prop "name" supplied to , should be a String.\n\nDetected errors (1):\n\n 1. Invalid value undefined supplied to String\n\n'); 88 | doesNotThrow(function () { 89 | runPropTypes(propTypes, {name: 'a'}); 90 | }); 91 | doesNotThrow(function() { 92 | runPropTypes(propTypes, {name: 'a', __strict__: void 0, __subtype__: void 0}); 93 | }); 94 | }); 95 | 96 | it('should check a subtype', function () { 97 | var T = t.subtype(t.struct({name: t.String}), function startsWithA(x) { 98 | return x.name.indexOf('a') === 0; 99 | }); 100 | var propTypes = getPropTypes(T); 101 | throwsWithMessage(function () { 102 | runPropTypes(propTypes, {name: 'b'}); 103 | }, '[tcomb] Invalid props:\n\n{\n \"name\": \"b\"\n}\n\nsupplied to , should be a {Struct{name: String} | startsWithA} subtype.'); 104 | doesNotThrow(function () { 105 | runPropTypes(propTypes, {name: 'a'}); 106 | }); 107 | }); 108 | 109 | it('should check additional props', function () { 110 | var propTypes = getPropTypes({name: t.String}); 111 | throwsWithMessage(function () { 112 | runPropTypes(propTypes, {name: 'a', surname: 'b'}); 113 | }, '[tcomb] Invalid additional prop(s):\n\n[\n "surname"\n]\n\nsupplied to .'); 114 | }); 115 | 116 | it('should allow to opt-out the additional props check', function () { 117 | var propTypes = getPropTypes({name: t.String}, { strict: false }); 118 | doesNotThrow(function () { 119 | runPropTypes(propTypes, {name: 'a', surname: 'b'}); 120 | }); 121 | }); 122 | 123 | it('should be a no-op in production', function () { 124 | var T = t.subtype(t.struct({name: t.String}), function startsWithA(x) { 125 | return x.name.indexOf('a') === 0; 126 | }); 127 | var propTypes = getPropTypes(T); 128 | production(function () { 129 | runPropTypes(propTypes, {}); 130 | assert.equal(propTypes.name('s'), undefined); 131 | assert.equal(propTypes.__strict__, undefined); 132 | assert.equal(propTypes.__subtype__, undefined); 133 | }); 134 | }); 135 | 136 | }); 137 | 138 | describe('pre-defined types', function () { 139 | 140 | it('should check ReactElement(s)', function () { 141 | var propTypes = getPropTypes({el: ReactElement}); 142 | throwsWithMessage(function () { 143 | runPropTypes(propTypes, {el: 'a'}); 144 | }, '[tcomb] Invalid prop \"el\" supplied to , should be a ReactElement.\n\nDetected errors (1):\n\n 1. Invalid value "a" supplied to ReactElement\n\n'); 145 | doesNotThrow(function () { 146 | runPropTypes(propTypes, {el: React.createElement('div')}); 147 | }); 148 | }); 149 | 150 | it('should check ReactNode(s)', function () { 151 | var propTypes = getPropTypes({el: ReactNode}); 152 | throwsWithMessage(function () { 153 | runPropTypes(propTypes, {el: true}); 154 | }, '[tcomb] Invalid prop "el" supplied to , should be a ReactNode.\n\nDetected errors (1):\n\n 1. Invalid value true supplied to ReactNode\n\n'); 155 | doesNotThrow(function () { 156 | runPropTypes(propTypes, {el: 'a'}); 157 | }); 158 | doesNotThrow(function () { 159 | runPropTypes(propTypes, {el: 1}); 160 | }); 161 | doesNotThrow(function () { 162 | runPropTypes(propTypes, {el: React.createElement('div')}); 163 | }); 164 | doesNotThrow(function () { 165 | runPropTypes(propTypes, {el: ['a', React.createElement('div')]}); 166 | }); 167 | doesNotThrow(function () { 168 | runPropTypes(propTypes, {el: [1, React.createElement('div')]}); 169 | }); 170 | doesNotThrow(function () { 171 | runPropTypes(propTypes, {el: [React.createElement('div'), React.createElement('a')]}); 172 | }); 173 | }); 174 | 175 | it('should check ReactChild', function () { 176 | var propTypes = getPropTypes({el: t.ReactChild}); 177 | throwsWithMessage(function () { 178 | runPropTypes(propTypes, {el: {}}); 179 | }, '[tcomb] Invalid prop \"el\" supplied to , should be a ReactChild.\n\nDetected errors (1):\n\n 1. Invalid value {} supplied to ReactChild\n\n'); 180 | doesNotThrow(function () { 181 | runPropTypes(propTypes, {el: true}); 182 | }); 183 | doesNotThrow(function () { 184 | runPropTypes(propTypes, {el: 'a'}); 185 | }); 186 | doesNotThrow(function () { 187 | runPropTypes(propTypes, {el: 1}); 188 | }); 189 | doesNotThrow(function () { 190 | runPropTypes(propTypes, {el: React.createElement('div')}); 191 | }); 192 | }); 193 | 194 | it('should check ReactChildren', function () { 195 | var propTypes = getPropTypes({el: t.ReactChildren}); 196 | throwsWithMessage(function () { 197 | runPropTypes(propTypes, {el: {}}); 198 | }, '[tcomb] Invalid prop \"el\" supplied to , should be a ReactChildren.\n\nDetected errors (1):\n\n 1. Invalid value {} supplied to ReactChildren\n\n'); 199 | doesNotThrow(function () { 200 | runPropTypes(propTypes, {el: true}); 201 | }); 202 | doesNotThrow(function () { 203 | runPropTypes(propTypes, {el: 'a'}); 204 | }); 205 | doesNotThrow(function () { 206 | runPropTypes(propTypes, {el: 1}); 207 | }); 208 | doesNotThrow(function () { 209 | runPropTypes(propTypes, {el: React.createElement('div')}); 210 | }); 211 | doesNotThrow(function () { 212 | runPropTypes(propTypes, {el: [true, React.createElement('div')]}); 213 | }); 214 | doesNotThrow(function () { 215 | runPropTypes(propTypes, {el: ['a', React.createElement('div')]}); 216 | }); 217 | doesNotThrow(function () { 218 | runPropTypes(propTypes, {el: [1, React.createElement('div')]}); 219 | }); 220 | doesNotThrow(function () { 221 | runPropTypes(propTypes, {el: [React.createElement('div'), React.createElement('a')]}); 222 | }); 223 | doesNotThrow(function () { 224 | runPropTypes(propTypes, {el: [ 'hello', [ null, 'a' ] ]}); 225 | }); 226 | }); 227 | 228 | it('should support all tcomb types', function () { 229 | var I = t.intersection([t.interface({a: t.String}), t.interface({b: t.Number})]); 230 | var propTypes = getPropTypes(I); 231 | runPropTypes(propTypes, {a: 's', b: 1}); 232 | throwsWithMessage(function () { 233 | runPropTypes(propTypes, {}); 234 | }, '[tcomb] Invalid props supplied to , should be a {a: String} & {b: Number}.\n\nDetected errors (2):\n\n 1. Invalid value undefined supplied to /a: String\n 2. Invalid value undefined supplied to /b: Number\n\n'); 235 | }); 236 | 237 | }); 238 | 239 | var skipDirectories = { 240 | '.DS_Store': 1 241 | }; 242 | 243 | describe('parse', function () { 244 | var fixturesDir = path.join(__dirname, 'fixtures/parse'); 245 | fs.readdirSync(fixturesDir).map(function (caseName) { 246 | if ((caseName in skipDirectories)) { 247 | return; 248 | } 249 | it(caseName, function () { 250 | var fixtureDir = path.join(fixturesDir, caseName); 251 | var filepath = path.join(fixtureDir, 'Actual.js'); 252 | var expected = require(path.join(fixtureDir, 'expected.json')); 253 | assert.deepEqual(JSON.stringify(parse(filepath)), JSON.stringify(expected)); 254 | }); 255 | }); 256 | }); 257 | 258 | function trim(str) { 259 | return str.replace(/^\s+|\s+$/, ''); 260 | } 261 | 262 | describe('toMarkdown', function () { 263 | var fixturesDir = path.join(__dirname, 'fixtures/toMarkdown'); 264 | fs.readdirSync(fixturesDir).map(function (caseName) { 265 | if ((caseName in skipDirectories)) { 266 | return; 267 | } 268 | it(caseName, function () { 269 | var actualFixtureDir = path.join(__dirname, 'fixtures/parse', caseName); 270 | var filepath = path.join(actualFixtureDir, 'Actual.js'); 271 | var fixtureDir = path.join(fixturesDir, caseName); 272 | var expected = fs.readFileSync(path.join(fixtureDir, 'expected.md')).toString(); 273 | assert.equal(trim(toMarkdown(parse(filepath))), trim(expected)); 274 | }); 275 | }); 276 | }); 277 | 278 | --------------------------------------------------------------------------------