├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── examples ├── .babelrc ├── package.json ├── src │ └── index.js └── test │ ├── index.js │ ├── mocha.opts │ └── setup.js ├── package-lock.json ├── package.json ├── src ├── index.js └── parser.js └── test ├── index.js ├── mocha.opts └── setup.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es6": true 5 | }, 6 | "globals": { 7 | "__SERVER__": true, 8 | "__CLIENT__": true, 9 | "__TEST__": true, 10 | "__DEV__": true, 11 | "__PROD__": true, 12 | "__STAGING__": true 13 | }, 14 | "ecmaFeatures": { 15 | "arrowFunctions": true, 16 | "binaryLiterals": true, 17 | "blockBindings": true, 18 | "classes": true, 19 | "defaultParams": true, 20 | "destructuring": true, 21 | "forOf": true, 22 | "generators": true, 23 | "modules": true, 24 | "objectLiteralComputedProperties": true, 25 | "objectLiteralShorthandMethods": true, 26 | "objectLiteralShorthandProperties": true, 27 | "octalLiterals": true, 28 | "regexUFlag": true, 29 | "regexYFlag": true, 30 | "spread": true, 31 | "superInFunctions": true, 32 | "templateStrings": true, 33 | "unicodeCodePointEscapes": true, 34 | "jsx": true 35 | }, 36 | "parser": "babel-eslint", 37 | "rules": { 38 | "prefer-reflect": [ 39 | 2, 40 | { 41 | "exceptions": [ 42 | "apply", 43 | "call", 44 | "delete" 45 | ] 46 | } 47 | ], 48 | "babel/new-cap": 2, 49 | "no-return-assign": 2, 50 | "no-invalid-this": 0, 51 | "no-void": 2, 52 | "one-var": [2, "never"], 53 | "react/jsx-closing-bracket-location": 0, 54 | "no-undef": 2, 55 | "max-nested-callbacks": [ 56 | 2, 57 | 3 58 | ], 59 | "no-empty": 2, 60 | "no-loop-func": 2, 61 | "keyword-spacing": 2, 62 | "babel/object-shorthand": [ 63 | 2, 64 | "always" 65 | ], 66 | "wrap-iife": [ 67 | 2, 68 | "inside" 69 | ], 70 | "valid-typeof": 2, 71 | "react/jsx-no-literals": 2, 72 | "handle-callback-err": 2, 73 | "operator-linebreak": [2, "after"], 74 | "no-label-var": 2, 75 | "no-process-env": 2, 76 | "no-irregular-whitespace": 2, 77 | "block-spacing": 2, 78 | "padded-blocks": [ 79 | 2, 80 | "never" 81 | ], 82 | "react/jsx-pascal-case": 2, 83 | "no-empty-pattern": 2, 84 | "radix": 2, 85 | "no-undefined": 2, 86 | "semi-spacing": 2, 87 | "eqeqeq": [ 88 | 2, 89 | "allow-null" 90 | ], 91 | "no-negated-condition": 2, 92 | "require-yield": 2, 93 | "new-cap": 2, 94 | "no-const-assign": 2, 95 | "no-bitwise": 2, 96 | "dot-notation": 2, 97 | "camelcase": 2, 98 | "prefer-const": 2, 99 | "no-negated-in-lhs": 2, 100 | "prefer-arrow-callback": 2, 101 | "no-extra-bind": 2, 102 | "react/prefer-es6-class": 2, 103 | "no-sequences": 2, 104 | "babel/generator-star-spacing": 2, 105 | "comma-dangle": [ 106 | 2, 107 | "always-multiline" 108 | ], 109 | "no-spaced-func": 2, 110 | "react/require-extension": 2, 111 | "no-labels": 2, 112 | "no-unreachable": 2, 113 | "no-eval": 2, 114 | "react/no-did-mount-set-state": 2, 115 | "no-unneeded-ternary": 2, 116 | "no-process-exit": 2, 117 | "no-empty-character-class": 2, 118 | "constructor-super": 2, 119 | "no-dupe-class-members": 2, 120 | "strict": [ 121 | 2, 122 | "never" 123 | ], 124 | "no-case-declarations": 2, 125 | "array-bracket-spacing": 2, 126 | "react/no-set-state": 2, 127 | "block-scoped-var": 2, 128 | "arrow-body-style": 2, 129 | "space-in-parens": 2, 130 | "no-confusing-arrow": 2, 131 | "no-control-regex": 2, 132 | "consistent-return": 2, 133 | "no-console": 2, 134 | "comma-spacing": 2, 135 | "no-redeclare": 2, 136 | "computed-property-spacing": 2, 137 | "no-invalid-regexp": 2, 138 | "use-isnan": 2, 139 | "no-new-require": 2, 140 | "indent": [ 141 | 2, 142 | 2 143 | ], 144 | "react/react-in-jsx-scope": 2, 145 | "no-native-reassign": 2, 146 | "no-func-assign": 2, 147 | "max-len": [ 148 | 2, 149 | 120, 150 | 4, 151 | { 152 | "ignoreUrls": true 153 | } 154 | ], 155 | "no-shadow": [ 156 | 2, 157 | { 158 | "builtinGlobals": true 159 | } 160 | ], 161 | "no-mixed-requires": 2, 162 | "react/no-did-update-set-state": 2, 163 | "react/jsx-uses-react": 2, 164 | "max-statements": [ 165 | 2, 166 | 20 167 | ], 168 | "space-unary-ops": [ 169 | 2, 170 | { 171 | "words": true, 172 | "nonwords": false 173 | } 174 | ], 175 | "no-lone-blocks": 2, 176 | "no-debugger": 2, 177 | "arrow-parens": [ 178 | 2, 179 | "always" 180 | ], 181 | "space-before-blocks": [ 182 | 2, 183 | "always" 184 | ], 185 | "no-implied-eval": 2, 186 | "no-useless-concat": 2, 187 | "no-multi-spaces": 2, 188 | "curly": [2, "multi-line"], 189 | "no-extra-boolean-cast": 2, 190 | "space-infix-ops": 2, 191 | "babel/no-await-in-loop": 2, 192 | "react/sort-comp": 2, 193 | "react/jsx-no-undef": 2, 194 | "no-multiple-empty-lines": [ 195 | 2, 196 | { 197 | "max": 2 198 | } 199 | ], 200 | "semi": 2, 201 | "no-param-reassign": 2, 202 | "no-cond-assign": 2, 203 | "no-dupe-keys": 2, 204 | "import/named": 0, 205 | "max-params": [ 206 | 2, 207 | 4 208 | ], 209 | "linebreak-style": 2, 210 | "react/jsx-sort-props": [ 211 | 0, 212 | { 213 | "shorthandFirst": true, 214 | "callbacksLast": true 215 | } 216 | ], 217 | "no-octal-escape": 2, 218 | "no-this-before-super": 2, 219 | "no-alert": 2, 220 | "react/jsx-no-duplicate-props": [ 221 | 2, 222 | { 223 | "ignoreCase": true 224 | } 225 | ], 226 | "no-unused-expressions": 2, 227 | "react/jsx-sort-prop-types": 0, 228 | "no-class-assign": 2, 229 | "spaced-comment": 2, 230 | "no-path-concat": 2, 231 | "prefer-spread": 2, 232 | "no-self-compare": 2, 233 | "guard-for-in": 2, 234 | "no-nested-ternary": 2, 235 | "no-multi-str": 2, 236 | "react/jsx-key": 1, 237 | "import/namespace": 2, 238 | "no-warning-comments": 1, 239 | "no-delete-var": 2, 240 | "babel/arrow-parens": [ 241 | 2, 242 | "always" 243 | ], 244 | "no-with": 2, 245 | "no-extra-parens": 2, 246 | "no-trailing-spaces": 2, 247 | "import/no-unresolved": 1, 248 | "no-obj-calls": 2, 249 | "accessor-pairs": 2, 250 | "yoda": [ 251 | 2, 252 | "never", 253 | { 254 | "exceptRange": true 255 | } 256 | ], 257 | "no-continue": 1, 258 | "react/no-unknown-property": 2, 259 | "no-new": 2, 260 | "object-curly-spacing": 2, 261 | "react/jsx-curly-spacing": [ 262 | 2, 263 | "never" 264 | ], 265 | "jsx-quotes": 2, 266 | "react/no-direct-mutation-state": 2, 267 | "key-spacing": 2, 268 | "no-underscore-dangle": [ 269 | 2, 270 | { "allowAfterThis": true } 271 | ], 272 | "new-parens": 2, 273 | "no-mixed-spaces-and-tabs": 2, 274 | "no-floating-decimal": 2, 275 | "operator-assignment": [ 276 | 2, 277 | "always" 278 | ], 279 | "no-shadow-restricted-names": 2, 280 | "no-use-before-define": [ 281 | 2, 282 | "nofunc" 283 | ], 284 | "no-useless-call": 2, 285 | "no-caller": 2, 286 | "quotes": [ 287 | 2, 288 | "single", 289 | "avoid-escape" 290 | ], 291 | "react/jsx-handler-names": [ 292 | 1, 293 | { 294 | "eventHandlerPrefix": "handle", 295 | "eventHandlerPropPrefix": "on" 296 | } 297 | ], 298 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 299 | "no-unused-vars": 2, 300 | "import/default": 1, 301 | "no-lonely-if": 2, 302 | "no-extra-semi": 2, 303 | "prefer-template": 2, 304 | "react/forbid-prop-types": 1, 305 | "react/self-closing-comp": 2, 306 | "no-else-return": 2, 307 | "react/jsx-max-props-per-line": [ 308 | 2, 309 | { 310 | "maximum": 3 311 | } 312 | ], 313 | "no-dupe-args": 2, 314 | "no-new-object": 2, 315 | "callback-return": 2, 316 | "no-new-wrappers": 2, 317 | "comma-style": 2, 318 | "no-script-url": 2, 319 | "consistent-this": 2, 320 | "react/wrap-multilines": 0, 321 | "dot-location": [ 322 | 2, 323 | "property" 324 | ], 325 | "no-implicit-coercion": 2, 326 | "max-depth": [ 327 | 2, 328 | 4 329 | ], 330 | "babel/object-curly-spacing": [ 331 | 2, 332 | "never" 333 | ], 334 | "no-array-constructor": 2, 335 | "no-iterator": 2, 336 | "react/jsx-no-bind": 2, 337 | "sort-vars": 2, 338 | "no-var": 2, 339 | "no-sparse-arrays": 2, 340 | "space-before-function-paren": [ 341 | 2, 342 | "never" 343 | ], 344 | "no-throw-literal": 2, 345 | "no-proto": 2, 346 | "default-case": 2, 347 | "no-inner-declarations": 2, 348 | "react/jsx-indent-props": [ 349 | 2, 350 | 2 351 | ], 352 | "no-new-func": 2, 353 | "object-shorthand": 2, 354 | "no-ex-assign": 2, 355 | "no-unexpected-multiline": 2, 356 | "no-undef-init": 2, 357 | "no-duplicate-case": 2, 358 | "no-fallthrough": 2, 359 | "no-catch-shadow": 2, 360 | "import/export": 2, 361 | "no-constant-condition": 2, 362 | "complexity": [ 363 | 2, 364 | 25 365 | ], 366 | "react/jsx-boolean-value": [ 367 | 2, 368 | "never" 369 | ], 370 | "valid-jsdoc": 2, 371 | "no-extend-native": 2, 372 | "react/prop-types": 2, 373 | "no-regex-spaces": 2, 374 | "react/no-multi-comp": 2, 375 | "no-octal": 2, 376 | "arrow-spacing": 2, 377 | "quote-props": [ 378 | 2, 379 | "as-needed" 380 | ], 381 | "no-div-regex": 2, 382 | "react/jsx-uses-vars": 2, 383 | "react/no-danger": 1 384 | }, 385 | "settings": { 386 | "ecmascript": 6, 387 | "jsx": true, 388 | "import/parser": "babel-eslint", 389 | "import/ignore": [ 390 | "node_modules", 391 | "\\.scss$" 392 | ], 393 | "import/resolve": { 394 | "moduleDirectory": [ 395 | "node_modules" 396 | ] 397 | } 398 | }, 399 | "plugins": [ 400 | "react", 401 | "import", 402 | "babel" 403 | ] 404 | } 405 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # Compiled source 40 | lib 41 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .babelrc 2 | .eslintrc 3 | .gitignore 4 | .npmignore 5 | .nyc_output 6 | .travis.yml 7 | coverage 8 | examples 9 | src 10 | test 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "5" 4 | - "5.1" 5 | - "4" 6 | - "4.2" 7 | - "4.1" 8 | - "4.0" 9 | - "0.12" 10 | - "0.11" 11 | - "0.10" 12 | - "iojs" 13 | after_success: npm run coverage 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Joon Ho Cho 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 | # proptypes-parser 2 | [![Build Status](https://travis-ci.org/joonhocho/proptypes-parser.svg?branch=master)](https://travis-ci.org/joonhocho/proptypes-parser) 3 | [![Coverage Status](https://coveralls.io/repos/github/joonhocho/proptypes-parser/badge.svg?branch=master)](https://coveralls.io/github/joonhocho/proptypes-parser?branch=master) 4 | [![npm version](https://badge.fury.io/js/proptypes-parser.svg)](https://badge.fury.io/js/proptypes-parser) 5 | [![Dependency Status](https://david-dm.org/joonhocho/proptypes-parser.svg)](https://david-dm.org/joonhocho/proptypes-parser) 6 | [![License](http://img.shields.io/:license-mit-blue.svg)](http://doge.mit-license.org) 7 | 8 | PropTypes parser / generator for React and React Native components with GraphQL-like syntax. 9 | 10 | Don't you just hate writing PropTypes for React components? 11 | 12 | `proptypes-parser` is cleaner, easier and less error prone way to define your PropTypes for both React and React Native applications. 13 | 14 | It uses GraphQL schema like syntax to define PropTypes in string. 15 | 16 | It also allows Type Composition via named definitions and spread operator `...`. 17 | 18 | 19 | ### Install 20 | ``` 21 | npm install --save proptypes-parser 22 | ``` 23 | 24 | ### Now `proptypes-parser` provides a default parser as `parsePropTypes`. 25 | ```javascript 26 | import {parsePropTypes} from 'proptypes-parser'; 27 | 28 | const propTypes = parsePropTypes(`{ 29 | number: Number 30 | string: String! 31 | }`) 32 | 33 | ``` 34 | 35 | Alternatively, if you like [template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals), 36 | 37 | ```javascript 38 | import {PT} from 'proptypes-parser'; 39 | 40 | const propTypes = PT`{ 41 | number: Number 42 | string: String! 43 | }` 44 | 45 | ``` 46 | 47 | 48 | ### Advanced Usage 49 | in `proptypes.js`. 50 | ```javascript 51 | import createPropTypesParser from 'proptypes-parser'; 52 | import {PropTypes} from 'react'; 53 | 54 | // Provide PropTypes to the parser (Required). 55 | // Also, provide any custom type definitions (Optional). 56 | const parsePropTypes = createPropTypesParser(PropTypes, { 57 | Message: class Message {} // To use 'Message' instance type. 58 | }); 59 | 60 | export default parsePropTypes; 61 | ``` 62 | 63 | in `component.js`. 64 | ```javascript 65 | const propTypes = parsePropTypes(`{ 66 | number: Number 67 | string: String! 68 | boolean: Boolean 69 | function: Function! 70 | date: Date! 71 | object: Object! 72 | shape: { 73 | nested: Number 74 | array: [Number] 75 | must: Boolean! 76 | }! 77 | array: [Number!]! 78 | arrayOfObjects: [{ 79 | value: String 80 | }!] 81 | node: Node 82 | element: Element! 83 | message: Message! 84 | any: Any! 85 | optionalUnion: String | Number | Boolean 86 | union: (String | Number)! 87 | }`); 88 | ``` 89 | 90 | is equivalent to 91 | ```javascript 92 | const propTypes = { 93 | number: PropTypes.number, 94 | string: PropTypes.string.isRequired, 95 | boolean: PropTypes.bool, 96 | function: PropTypes.func.isRequired, 97 | date: PropTypes.instanceOf(Date).isRequired, 98 | object: PropTypes.object.isRequired, 99 | shape: PropTypes.shape({ 100 | nested: PropTypes.number, 101 | array: PropTypes.arrayOf(PropTypes.number), 102 | must: PropTypes.bool.isRequired, 103 | }).isRequired, 104 | array: PropTypes.arrayOf( 105 | PropTypes.Number.isRequired, 106 | ).isRequired, 107 | arrayOfObjects: PropTypes.arrayOf( 108 | PropTypes.shape({ 109 | value: PropTypes.string, 110 | }).isRequired 111 | ), 112 | node: PropTypes.node 113 | element: PropTypes.element.isRequired, 114 | message: PropTypes.instanceOf(Message).isRequired, 115 | any: PropTypes.any.isRequired, 116 | optionalUnion: PropTypes.oneOfType([ 117 | PropTypes.string, 118 | PropTypes.number, 119 | PropTypes.bool, 120 | ]), 121 | union: PropTypes.oneOfType([ 122 | PropTypes.string, 123 | PropTypes.number, 124 | ]).isRequired, 125 | }; 126 | ``` 127 | How wonderful! 128 | 129 | 130 | ### Composition via named definition 131 | Compose types with named definitions and spread operator `...`. 132 | 133 | ```javascript 134 | // Define 'Car' type. 135 | const carPropTypes = parsePropTypes(` 136 | Car { 137 | year: Number! 138 | model: String! 139 | } 140 | `); 141 | 142 | // Use previously defined 'Car' type. 143 | const garagePropTypes = parsePropTypes(` 144 | Garage { 145 | address: String! 146 | cars: [Car!]! 147 | } 148 | `); 149 | 150 | // Use spread operator on 'Car' type. 151 | const carWithMakePropTypes = parsePropTypes(` 152 | CarWithMake { 153 | ...Car 154 | make: String! 155 | } 156 | `); 157 | ``` 158 | 159 | ### addType(name, type) 160 | Add new types to the type dictionary. 161 | ```javascript 162 | // Add class instance type. 163 | class Message {} 164 | parsePropTypes.addType('Message', Message); 165 | 166 | // Add propTypes definition. 167 | // Same as named definition. 168 | const carPropTypes = parsePropTypes(`{ 169 | year: Number! 170 | model: String! 171 | }`); 172 | parsePropTypes.addType('Car', carPropTypes); 173 | 174 | // Add React.PropTypes type. 175 | const newsOrPhotosEnum = PropTypes.oneOf(['News', 'Photos']); 176 | parsePropTypes.addType('NewsOrPhotos', newsOrPhotosEnum); 177 | 178 | // Use above types. 179 | parsePropTypes(`{ 180 | message: Message 181 | car: Car 182 | mediaType: NewsOrPhotos 183 | }`); 184 | 185 | ``` 186 | 187 | ### Enums 188 | Currently, Enums are not supported. 189 | However, you can do this instead: 190 | ```javascript 191 | // Provide type extensions for this parser. 192 | const parsePropTypes = createPropTypesParser(PropTypes, { 193 | OptionalEnum: PropTypes.oneOf(['News', 'Photos']), 194 | }); 195 | 196 | const propTypes = parsePropTypes(`{ 197 | optionalEnumValue: OptionalEnum 198 | requiredEnumValue: OptionalEnum! 199 | }`); 200 | ``` 201 | or 202 | ```javascript 203 | // Provide local one-time type extensions. 204 | const propTypes = parsePropTypes(`{ 205 | optionalEnumValue: OptionalEnum 206 | requiredEnumValue: OptionalEnum! 207 | }`, { 208 | OptionalEnum: PropTypes.oneOf(['News', 'Photos']), 209 | }); 210 | ``` 211 | 212 | ### Examples 213 | See [test](https://github.com/joonhocho/proptypes-parser/blob/master/test/index.js). 214 | 215 | 216 | ### Production Use 217 | Use [babel-plugin-transform-react-remove-prop-types](https://github.com/oliviertassinari/babel-plugin-transform-react-remove-prop-types) to strip propTypes from your react components. 218 | 219 | 220 | ### TODO 221 | - Support Enums: `value: ['News', 'Photos']` 222 | - More extensive validations 223 | - Babel plugin 224 | - Webpack plugin 225 | - PRs are welcome! 226 | 227 | 228 | ### License 229 | ``` 230 | The MIT License (MIT) 231 | 232 | Copyright (c) 2016 Joon Ho Cho 233 | 234 | Permission is hereby granted, free of charge, to any person obtaining a copy 235 | of this software and associated documentation files (the "Software"), to deal 236 | in the Software without restriction, including without limitation the rights 237 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 238 | copies of the Software, and to permit persons to whom the Software is 239 | furnished to do so, subject to the following conditions: 240 | 241 | The above copyright notice and this permission notice shall be included in all 242 | copies or substantial portions of the Software. 243 | 244 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 245 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 246 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 247 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 248 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 249 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 250 | SOFTWARE. 251 | ``` 252 | -------------------------------------------------------------------------------- /examples/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0", "react"], 3 | "plugins": ["transform-react-remove-prop-types"] 4 | } 5 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "babel src --out-dir lib", 8 | "build-watch": "babel src --watch --out-dir lib", 9 | "clear": "rm -rf ./lib", 10 | "pretest": "npm run build", 11 | "start": "npm test", 12 | "test": "mocha", 13 | "test-watch": "mocha --watch", 14 | "watch": "npm run build-watch & npm run test-watch" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/joonhocho/proptypes-parser.git" 19 | }, 20 | "author": "Joon Ho Cho", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/joonhocho/proptypes-parser/issues" 24 | }, 25 | "homepage": "https://github.com/joonhocho/proptypes-parser#readme", 26 | "dependencies": { 27 | "react": "^15.1.0" 28 | }, 29 | "devDependencies": { 30 | "babel-cli": "^6.10.1", 31 | "babel-plugin-transform-react-remove-prop-types": "^0.2.7", 32 | "babel-preset-es2015": "^6.9.0", 33 | "babel-preset-react": "^6.5.0", 34 | "babel-preset-stage-0": "^6.5.0", 35 | "babel-register": "^6.9.0", 36 | "chai": "^3.5.0", 37 | "mocha": "^2.5.3", 38 | "proptypes-parser": "^0.1.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/src/index.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {PT} from '../../lib'; 3 | 4 | class ES6 extends Component { 5 | static propTypes = PT`{ 6 | value: Number! 7 | }`; 8 | render() { 9 | return (
); 10 | } 11 | } 12 | 13 | 14 | const ES5 = React.createClass({ 15 | propTypes: PT`{ 16 | value: Number! 17 | }`, 18 | render: function() { 19 | return (
); 20 | }, 21 | }); 22 | 23 | 24 | class ES6Assign extends Component { 25 | render() { 26 | return (
); 27 | } 28 | } 29 | ES6Assign.propTypes = PT`{ 30 | value: Number! 31 | }`; 32 | 33 | 34 | const ES5Assign = React.createClass({ 35 | render: function() { 36 | return (
); 37 | } 38 | }); 39 | ES5Assign.propTypes = PT`{ 40 | value: Number! 41 | }`; 42 | 43 | 44 | function FuncAssign() { 45 | return (
); 46 | } 47 | FuncAssign.propTypes = PT`{ 48 | value: Number! 49 | }`; 50 | 51 | 52 | class ES6VarAssign extends Component { 53 | render() { 54 | return (
); 55 | } 56 | } 57 | const fPropTypes = PT`{ 58 | value: Number! 59 | }`; 60 | ES6VarAssign.propTypes = fPropTypes; 61 | 62 | 63 | export { 64 | ES6, ES5, ES6Assign, ES5Assign, FuncAssign, ES6VarAssign, 65 | }; 66 | -------------------------------------------------------------------------------- /examples/test/index.js: -------------------------------------------------------------------------------- 1 | import {describe, it} from 'mocha'; 2 | import {expect} from 'chai'; 3 | import {ES6, ES5, ES6Assign, ES5Assign, FuncAssign, ES6VarAssign} from '../lib'; 4 | 5 | 6 | describe('PT', () => { 7 | it('babel plugin should remove ES6.propTypes', () => { 8 | expect(ES6.propTypes).to.be.undefined; 9 | }); 10 | 11 | it('babel plugin should remove ES5.propTypes', () => { 12 | expect(ES5.propTypes).to.be.undefined; 13 | }); 14 | 15 | it('babel plugin should remove ES6Assign.propTypes', () => { 16 | expect(ES6Assign.propTypes).to.be.undefined; 17 | }); 18 | 19 | it('babel plugin should remove ES5Assign.propTypes', () => { 20 | expect(ES5Assign.propTypes).to.be.undefined; 21 | }); 22 | 23 | it('babel plugin should remove FuncAssign.propTypes', () => { 24 | expect(FuncAssign.propTypes).to.be.undefined; 25 | }); 26 | 27 | it('babel plugin should remove ES6VarAssign.propTypes', () => { 28 | expect(ES6VarAssign.propTypes).to.be.undefined; 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /examples/test/mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers js:babel-register 2 | --require ./test/setup.js 3 | --reporter spec 4 | --timeout 5000 5 | -------------------------------------------------------------------------------- /examples/test/setup.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | chai.use(require('chai-as-promised')); 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "proptypes-parser", 3 | "version": "0.2.1", 4 | "description": "PropTypes parser / generator for React and React Native components with GraphQL-like syntax.", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "babel src --out-dir lib", 8 | "build-watch": "babel src --watch --out-dir lib", 9 | "clear": "rm -rf ./lib ./coverage ./.nyc_output", 10 | "coverage": "nyc npm test && nyc report --reporter=text-lcov | coveralls", 11 | "nyc": "nyc npm test && nyc report --reporter=lcov", 12 | "prepublish": "npm run clear && npm test", 13 | "pretest": "npm run build", 14 | "start": "npm test", 15 | "test": "mocha", 16 | "test-watch": "mocha --watch", 17 | "update-D": "npm install --save-dev babel-cli@latest babel-preset-es2015@latest babel-preset-stage-0@latest babel-register@latest chai@latest chai-as-promised@latest coveralls@latest mocha@latest nyc@latest", 18 | "watch": "npm run build-watch & npm run test-watch" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/joonhocho/proptypes-parser.git" 23 | }, 24 | "keywords": [ 25 | "react", 26 | "react-native", 27 | "react native", 28 | "proptypes", 29 | "graphql", 30 | "schema", 31 | "parse", 32 | "parser" 33 | ], 34 | "author": "Joon Ho Cho", 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/joonhocho/proptypes-parser/issues" 38 | }, 39 | "homepage": "https://github.com/joonhocho/proptypes-parser#readme", 40 | "peerDependencies": { 41 | "prop-types": "*" 42 | }, 43 | "devDependencies": { 44 | "babel-cli": "^6.9.0", 45 | "babel-preset-es2015": "^6.9.0", 46 | "babel-preset-stage-0": "^6.5.0", 47 | "babel-register": "^6.9.0", 48 | "chai": "^3.5.0", 49 | "chai-as-promised": "^5.3.0", 50 | "coveralls": "^2.11.9", 51 | "mocha": "^2.5.3", 52 | "nyc": "^6.4.4", 53 | "prop-types": "^15.5.10" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | 4 | let createParser; 5 | if (process && process.env && process.env.NODE_ENV === 'production') { 6 | // No-op if production 7 | createParser = () => { 8 | const fn = () => ({}); 9 | fn.PT = fn; 10 | fn.addType = () => {}; 11 | return fn; 12 | }; 13 | } else { 14 | createParser = require('./parser.js').default; 15 | } 16 | 17 | 18 | const parsePropTypes = createParser(PropTypes); 19 | const PT = parsePropTypes.PT; 20 | 21 | 22 | export { 23 | PropTypes, 24 | parsePropTypes, 25 | PT, 26 | }; 27 | 28 | export default createParser; 29 | -------------------------------------------------------------------------------- /src/parser.js: -------------------------------------------------------------------------------- 1 | const CHAR_LIST_OPEN = '['; 2 | const CHAR_LIST_CLOSE = ']'; 3 | const CHAR_SHAPE_OPEN = '{'; 4 | const CHAR_SHAPE_CLOSE = '}'; 5 | const CHAR_GROUP_OPEN = '('; 6 | const CHAR_GROUP_CLOSE = ')'; 7 | const CHAR_QUOTE_OPEN = "'"; 8 | const CHAR_QUOTE_CLOSE = "'"; 9 | 10 | const NODE_TYPE_LEAF = 'LEAF'; 11 | const NODE_TYPE_ASSIGNMENT = 'ASSIGNMENT'; 12 | const NODE_TYPE_SPREAD = 'SPREAD'; 13 | const NODE_TYPE_REQUIRED = 'REQUIRED'; 14 | const NODE_TYPE_UNION = 'UNION'; 15 | const NODE_TYPE_LIST = 'LIST'; 16 | const NODE_TYPE_SHAPE = 'SHAPE'; 17 | const NODE_TYPE_GROUP = 'GROUP'; 18 | const NODE_TYPE_QUOTE = 'QUOTE'; 19 | 20 | const GROUPS = { 21 | [CHAR_LIST_OPEN]: { 22 | type: NODE_TYPE_LIST, 23 | opening: CHAR_LIST_OPEN, 24 | closing: CHAR_LIST_CLOSE, 25 | }, 26 | [CHAR_SHAPE_OPEN]: { 27 | type: NODE_TYPE_SHAPE, 28 | opening: CHAR_SHAPE_OPEN, 29 | closing: CHAR_SHAPE_CLOSE, 30 | }, 31 | [CHAR_GROUP_OPEN]: { 32 | type: NODE_TYPE_GROUP, 33 | opening: CHAR_GROUP_OPEN, 34 | closing: CHAR_GROUP_CLOSE, 35 | }, 36 | [CHAR_QUOTE_OPEN]: { 37 | type: NODE_TYPE_QUOTE, 38 | opening: CHAR_QUOTE_OPEN, 39 | closing: CHAR_QUOTE_CLOSE, 40 | }, 41 | }; 42 | 43 | const OPERATOR_ASSIGNMENT = ':'; 44 | const OPERATOR_REQUIRED = '!'; 45 | const OPERATOR_UNION = '|'; 46 | const OPERATOR_SPREAD = '...'; 47 | 48 | const createLeafNode = (value) => ({ 49 | type: NODE_TYPE_LEAF, 50 | value, 51 | }); 52 | 53 | const createRequiredNode = (value) => ({ 54 | type: NODE_TYPE_REQUIRED, 55 | value: OPERATOR_REQUIRED, 56 | children: [value], 57 | }); 58 | 59 | const createSpreadNode = (node) => ({ 60 | type: NODE_TYPE_SPREAD, 61 | value: OPERATOR_SPREAD, 62 | children: [node], 63 | }); 64 | 65 | const createUnionNode = (left, right) => ({ 66 | type: NODE_TYPE_UNION, 67 | value: OPERATOR_UNION, 68 | children: [left, right], 69 | }); 70 | 71 | const createAssignmentNode = (name, value) => ({ 72 | type: NODE_TYPE_ASSIGNMENT, 73 | value: OPERATOR_ASSIGNMENT, 74 | children: [name, value], 75 | }); 76 | 77 | const OPERATOR_TYPE_PREFIX = 'PREFIX'; 78 | const OPERATOR_TYPE_POSTFIX = 'POSTFIX'; 79 | const OPERATOR_TYPE_BINARY = 'BINARY'; 80 | 81 | const operators = [ 82 | { 83 | operator: OPERATOR_REQUIRED, 84 | type: OPERATOR_TYPE_POSTFIX, 85 | build: (left) => createRequiredNode(left), 86 | }, 87 | { 88 | operator: OPERATOR_SPREAD, 89 | type: OPERATOR_TYPE_PREFIX, 90 | build: (right) => createSpreadNode(right), 91 | }, 92 | { 93 | operator: OPERATOR_UNION, 94 | type: OPERATOR_TYPE_BINARY, 95 | build: (left, right) => createUnionNode(left, right), 96 | }, 97 | { 98 | operator: OPERATOR_ASSIGNMENT, 99 | type: OPERATOR_TYPE_BINARY, 100 | build: (left, right) => createAssignmentNode(left, right), 101 | }, 102 | ]; 103 | 104 | const punctuatorRegexp = /([\!\(\)\:\[\]\{\}\'\,]|\.\.\.)/g; 105 | 106 | // https://facebook.github.io/graphql/#sec-Names 107 | const nameRegexp = /[_A-Za-z][_0-9A-Za-z]*/; 108 | 109 | const isValidName = (name) => nameRegexp.test(name); 110 | 111 | const last = (list) => list[list.length - 1]; 112 | 113 | const forEach = (obj, fn) => 114 | Object.keys(obj).forEach((name) => fn(obj[name], name, obj)); 115 | 116 | const copy = (dest, src) => 117 | forEach(src, (value, key) => { dest[key] = value; }); 118 | 119 | const createCleanObject = (props) => { 120 | const obj = Object.create(null); 121 | if (props) copy(obj, props); 122 | return obj; 123 | }; 124 | 125 | const getInnerTokens = (tokens, opening, closing, start = 0) => { 126 | let level = 0; 127 | for (let i = start; i < tokens.length; i++) { 128 | const token = tokens[i]; 129 | if (token === closing) { 130 | if (!level) { 131 | return tokens.slice(start, i); 132 | } 133 | level--; 134 | } else if (token === opening) { 135 | level++; 136 | } 137 | } 138 | throw new Error(`No closing char is found. char=${closing}`); 139 | }; 140 | 141 | const buildTreeByGrouping = (tokens) => { 142 | const node = { 143 | children: [], 144 | }; 145 | for (let i = 0; i < tokens.length; i++) { 146 | const token = tokens[i]; 147 | let child; 148 | if (GROUPS[token]) { 149 | const group = GROUPS[token]; 150 | const innerTokens = getInnerTokens(tokens, token, group.closing, i + 1); 151 | child = { 152 | ...group, 153 | ...buildTreeByGrouping(innerTokens), 154 | }; 155 | i += innerTokens.length + 1; 156 | } else { 157 | child = createLeafNode(token); 158 | } 159 | node.children.push(child); 160 | } 161 | return node; 162 | }; 163 | 164 | const transformChildren = (children, {operator, type, build}) => { 165 | const newChildren = children.slice(); 166 | for (let i = 0; i < newChildren.length; i++) { 167 | const child = newChildren[i]; 168 | if (child.value === operator) { 169 | let left, right; 170 | switch (type) { 171 | case OPERATOR_TYPE_PREFIX: 172 | right = newChildren[i + 1]; 173 | if (!right) { 174 | throw new Error(`Prefix unary operator '${operator}' requires right side argument.`); 175 | } 176 | newChildren.splice(i, 2, build(right)); 177 | break; 178 | case OPERATOR_TYPE_POSTFIX: 179 | left = newChildren[i - 1]; 180 | if (!left) { 181 | throw new Error(`Prefix unary operator '${operator}' requires left side argument.`); 182 | } 183 | newChildren.splice(i - 1, 2, build(left)); 184 | i--; 185 | break; 186 | case OPERATOR_TYPE_BINARY: 187 | left = newChildren[i - 1]; 188 | if (!left) { 189 | throw new Error(`Binary operator '${operator}' requires left side argument.`); 190 | } 191 | right = newChildren[i + 1]; 192 | if (!right) { 193 | throw new Error(`Binary operator '${operator}' requires right side argument.`); 194 | } 195 | newChildren.splice(i - 1, 3, build(left, right)); 196 | i--; 197 | break; 198 | } 199 | } 200 | } 201 | return newChildren; 202 | }; 203 | 204 | const traverseTreePostOrder = (node, fn) => { 205 | const {children} = node; 206 | if (children) children.forEach((child) => { traverseTreePostOrder(child, fn); }); 207 | fn(node); 208 | }; 209 | 210 | const reduceTreePostOrder = (node, fn) => { 211 | const newNode = {...node}; 212 | if (node.children) { 213 | newNode.children = node.children.map((child) => reduceTreePostOrder(child, fn)).filter((x) => x); 214 | } 215 | return fn(newNode); 216 | }; 217 | 218 | const transformTreeWithOperators = (node) => { 219 | traverseTreePostOrder(node, (child) => { 220 | if (child.children) { 221 | child.children = operators.reduce(transformChildren, child.children); 222 | } 223 | }); 224 | }; 225 | 226 | const assertNodeType = (node, expected) => { 227 | if (node.type === expected) return true; 228 | console.error(node); 229 | throw new Error(`Expected node type '${expected}', but was '${node.type}'.`); 230 | }; 231 | 232 | const assertNameNode = (node) => { 233 | assertNodeType(node, NODE_TYPE_LEAF); 234 | if (isValidName(node.value)) return true; 235 | console.error(node); 236 | throw new Error('Invalid name node.'); 237 | }; 238 | 239 | const isPropType = (node) => typeof node === 'function'; 240 | 241 | const isOptionalPropType = (type) => isPropType(type) && Boolean(type.isRequired); 242 | 243 | const isRequiredPropType = (type) => isPropType(type) && !type.isRequired; 244 | 245 | const reduceNameNode = (node) => { 246 | assertNameNode(node); 247 | return node; 248 | }; 249 | 250 | 251 | export default (PropTypes, extension) => { 252 | if (!PropTypes) { 253 | throw new Error('Must provide React.PropTypes.'); 254 | } 255 | 256 | const types = createCleanObject({ 257 | Array: PropTypes.array, 258 | Boolean: PropTypes.bool, 259 | Function: PropTypes.func, 260 | Number: PropTypes.number, 261 | Object: PropTypes.object, 262 | String: PropTypes.string, 263 | Node: PropTypes.node, 264 | Element: PropTypes.element, 265 | Any: PropTypes.any, 266 | Date: PropTypes.instanceOf(Date), 267 | RegExp: PropTypes.instanceOf(RegExp), 268 | }); 269 | 270 | const namedPropTypes = createCleanObject(); 271 | 272 | let tmpTypes; 273 | 274 | 275 | const maybeConvertClassToType = (type) => { 276 | if (typeof type === 'function' && type.prototype) { 277 | return PropTypes.instanceOf(type); 278 | } 279 | return type; 280 | }; 281 | 282 | const addTypes = (dest, typeOverrides) => { 283 | forEach(typeOverrides, (type, name) => { 284 | dest[name] = maybeConvertClassToType(type); 285 | }); 286 | }; 287 | 288 | if (extension) { 289 | addTypes(types, extension); 290 | } 291 | 292 | const addPropTypes = (name, propTypes) => { 293 | if (types[name]) { 294 | throw new Error(`'${name}' type is already defined.`); 295 | } 296 | namedPropTypes[name] = propTypes; 297 | types[name] = PropTypes.shape(propTypes); 298 | }; 299 | 300 | const getType = (name) => { 301 | if (tmpTypes[name]) return tmpTypes[name]; 302 | if (types[name]) return types[name]; 303 | throw new Error(`Expected valid named type. Instead, saw '${name}'.`); 304 | }; 305 | 306 | const assertSpreadableTypeName = (name) => { 307 | if (namedPropTypes[name]) return true; 308 | throw new Error(`Unknown type to spread. name=${name}`); 309 | }; 310 | 311 | const reduceNamedTypeNode = (node) => { 312 | assertNameNode(node); 313 | const {value} = node; 314 | 315 | const propType = getType(value); 316 | if (propType) return propType; 317 | 318 | throw new Error(`Unknown type name. name=${value}`); 319 | }; 320 | 321 | const reduceTypeNode = (node) => { 322 | if (isPropType(node)) return node; 323 | 324 | switch (node.type) { 325 | case NODE_TYPE_LEAF: 326 | return reduceNamedTypeNode(node); 327 | case NODE_TYPE_SHAPE: 328 | return PropTypes.shape(node.value); 329 | default: 330 | console.error(node); 331 | throw new Error(`Unexpected type to reduce as type. ${node.type}`); 332 | } 333 | }; 334 | 335 | const reduceRequiredNode = (node) => { 336 | assertNodeType(node, NODE_TYPE_REQUIRED); 337 | 338 | const propType = reduceTypeNode(node.children[0]); 339 | if (isOptionalPropType(propType)) return propType.isRequired; 340 | 341 | console.error(propType); 342 | throw new Error(`PropType does not support 'isRequired'.`); 343 | }; 344 | 345 | const reduceAssignmentNode = (node) => { 346 | assertNodeType(node, NODE_TYPE_ASSIGNMENT); 347 | 348 | const { 349 | type, 350 | children: [nameNode, valueNode], 351 | } = node; 352 | 353 | assertNameNode(nameNode); 354 | 355 | return { 356 | type, 357 | name: nameNode.value, 358 | value: reduceTypeNode(valueNode), 359 | }; 360 | }; 361 | 362 | const reduceSpreadNode = (node) => { 363 | assertNodeType(node, NODE_TYPE_SPREAD); 364 | 365 | const { 366 | type, 367 | children: [nameNode], 368 | } = node; 369 | 370 | assertNameNode(nameNode); 371 | assertSpreadableTypeName(nameNode.value); 372 | 373 | return { 374 | type, 375 | value: namedPropTypes[nameNode.value], 376 | }; 377 | }; 378 | 379 | const reduceUnionNode = (node) => { 380 | assertNodeType(node, NODE_TYPE_UNION); 381 | 382 | const [left, right] = node.children; 383 | const isRequired = isRequiredPropType(left) && isRequiredPropType(right); 384 | 385 | const propType = PropTypes.oneOfType([ 386 | reduceTypeNode(left), 387 | reduceTypeNode(right), 388 | ]); 389 | 390 | return isRequired ? propType.isRequired : propType; 391 | }; 392 | 393 | const reduceListTypeNode = (node) => { 394 | // TODO could be enums. 395 | assertNodeType(node, NODE_TYPE_LIST); 396 | 397 | if (node.children.length !== 1) { 398 | console.error(node); 399 | throw new Error('List type should have only one child.'); 400 | } 401 | 402 | const [typeNode] = node.children; 403 | 404 | return PropTypes.arrayOf(reduceTypeNode(typeNode)); 405 | }; 406 | 407 | const reduceGroupNode = (node) => { 408 | assertNodeType(node, NODE_TYPE_GROUP); 409 | 410 | if (node.children.length !== 1) { 411 | console.error(node); 412 | throw new Error('Group type should have only one child.'); 413 | } 414 | 415 | const [typeNode] = node.children; 416 | return reduceTypeNode(typeNode); 417 | }; 418 | 419 | const reduceShapeNode = (node) => { 420 | assertNodeType(node, NODE_TYPE_SHAPE); 421 | 422 | const {type, children} = node; 423 | if (!children.length) { 424 | console.error(node); 425 | throw new Error('Empty shape.'); 426 | } 427 | 428 | const shape = {}; 429 | 430 | children.forEach((node) => { 431 | const {type: childType, name, value} = node; 432 | switch (childType) { 433 | case NODE_TYPE_SPREAD: 434 | copy(shape, value); 435 | break; 436 | case NODE_TYPE_ASSIGNMENT: 437 | shape[name] = value; 438 | break; 439 | default: 440 | console.error(node); 441 | throw new Error(`Unexpected type inside shape. ${childType}`); 442 | } 443 | }); 444 | 445 | return { 446 | type, 447 | value: shape, 448 | }; 449 | }; 450 | 451 | const reduceTreeToPropTypes = (root) => { 452 | return reduceTreePostOrder(root, (node) => { 453 | switch (node.type) { 454 | case NODE_TYPE_LEAF: 455 | // All Leaves should be valid names at this point. 456 | return reduceNameNode(node); 457 | case NODE_TYPE_ASSIGNMENT: 458 | return reduceAssignmentNode(node); 459 | case NODE_TYPE_SPREAD: 460 | return reduceSpreadNode(node); 461 | case NODE_TYPE_REQUIRED: 462 | return reduceRequiredNode(node); 463 | case NODE_TYPE_UNION: 464 | return reduceUnionNode(node); 465 | case NODE_TYPE_LIST: 466 | return reduceListTypeNode(node); 467 | case NODE_TYPE_SHAPE: 468 | return reduceShapeNode(node); 469 | case NODE_TYPE_GROUP: 470 | return reduceGroupNode(node); 471 | case NODE_TYPE_QUOTE: 472 | // TODO Support enums. 473 | break; 474 | default: 475 | console.error(node); 476 | throw new Error(`Unknown node type. '${node.type}'`); 477 | } 478 | }); 479 | }; 480 | 481 | const parser = (string, typeOverrides) => { 482 | let tokens = string.replace(punctuatorRegexp, ' $1 ').split(/[\n\s,;]+/g).filter((x) => x); 483 | 484 | let name; 485 | if (isValidName(tokens[0])) { 486 | name = tokens[0]; 487 | tokens = tokens.slice(1); 488 | } 489 | 490 | if (tokens[0] !== CHAR_SHAPE_OPEN || last(tokens) !== CHAR_SHAPE_CLOSE) { 491 | throw new Error('Must wrap definition with { }.'); 492 | } 493 | 494 | tmpTypes = createCleanObject(); 495 | if (typeOverrides) addTypes(tmpTypes, typeOverrides); 496 | 497 | if (types[name] || tmpTypes[name]) { 498 | throw new Error(`'${name}' type is already defined.`); 499 | } 500 | 501 | const node = buildTreeByGrouping(tokens); 502 | transformTreeWithOperators(node); 503 | 504 | if (node.children.length !== 1) { 505 | throw new Error('Only one definition is allowed.'); 506 | } 507 | const [shapeNode] = node.children; 508 | assertNodeType(shapeNode, NODE_TYPE_SHAPE); 509 | 510 | const propTypesFromTree = reduceTreeToPropTypes(shapeNode).value; 511 | 512 | if (name) addPropTypes(name, propTypesFromTree); 513 | return propTypesFromTree; 514 | }; 515 | 516 | parser.getType = (name) => types[name] || null; 517 | 518 | parser.getPropTypes = (name) => namedPropTypes[name] || null; 519 | 520 | parser.addType = (name, type) => { 521 | if (types[name]) { 522 | throw new Error(`'${name}' type is already defined.`); 523 | } 524 | if (type.constructor === Object || 525 | (type && typeof type === 'object' && !type.constructor)) { 526 | addPropTypes(name, type); 527 | } else { 528 | types[name] = maybeConvertClassToType(type); 529 | } 530 | }; 531 | 532 | parser.PT = (strings) => parser(strings.raw[0]); 533 | 534 | return parser; 535 | }; 536 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import {describe, it} from 'mocha'; 2 | import {expect} from 'chai'; 3 | import createParser, { 4 | PropTypes, 5 | parsePropTypes as defaultParser, 6 | PT as defaultPT, 7 | } from '../lib'; 8 | import ReactPropTypesSecret from 'prop-types/lib/ReactPropTypesSecret'; 9 | 10 | 11 | const log = (value) => console.log(JSON.stringify(value, null, ' ')); 12 | 13 | const test = (propType, value) => { 14 | return propType( 15 | {value}, 16 | 'value', 17 | 'Test', 18 | 'prop', 19 | null, 20 | ReactPropTypesSecret 21 | ); 22 | }; 23 | 24 | const testPass = (propType, value) => { 25 | expect(test(propType, value)).to.be.null; 26 | }; 27 | 28 | const testFail = (propType, value) => { 29 | expect(test(propType, value)).to.be.an.instanceof(Error); 30 | }; 31 | 32 | 33 | describe('PropTypes', () => { 34 | it('should provide default parsePropTypes.', () => { 35 | const propTypes = defaultParser(`{ 36 | number: Number 37 | string: String! 38 | boolean: Boolean 39 | }`); 40 | 41 | expect(propTypes.number).to.equal(PropTypes.number); 42 | 43 | expect(propTypes.string).to.equal(PropTypes.string.isRequired); 44 | 45 | expect(propTypes.boolean).to.equal(PropTypes.bool); 46 | }); 47 | 48 | 49 | it('should provide default PT.', () => { 50 | const propTypes = defaultPT`{ 51 | number: Number 52 | string: String! 53 | boolean: Boolean 54 | }`; 55 | 56 | expect(propTypes.number).to.equal(PropTypes.number); 57 | 58 | expect(propTypes.string).to.equal(PropTypes.string.isRequired); 59 | 60 | expect(propTypes.boolean).to.equal(PropTypes.bool); 61 | }); 62 | 63 | 64 | it('should successfully parse and return valid propTypes.', () => { 65 | class Message {} 66 | 67 | const propTypes = createParser(PropTypes, {Message})(`{ 68 | number: Number 69 | string: String! 70 | boolean: Boolean 71 | function: Function! 72 | date: Date! 73 | object: Object! 74 | shape: { 75 | nested: Number 76 | array: [Number] 77 | must: Boolean! 78 | }! 79 | array: [Number!]! 80 | arrayOfObjects: [{ 81 | value: String 82 | }!] 83 | node: Node 84 | element: Element! 85 | message: Message! 86 | any: Any! 87 | }`); 88 | 89 | expect(propTypes.number).to.equal(PropTypes.number); 90 | 91 | expect(propTypes.string).to.equal(PropTypes.string.isRequired); 92 | 93 | expect(propTypes.boolean).to.equal(PropTypes.bool); 94 | 95 | expect(propTypes.function).to.equal(PropTypes.func.isRequired); 96 | 97 | testPass(propTypes.date, new Date()); 98 | testFail(propTypes.date, null); 99 | testFail(propTypes.date, Date); 100 | testFail(propTypes.date, 1); 101 | 102 | expect(propTypes.object).to.equal(PropTypes.object.isRequired); 103 | 104 | testPass(propTypes.shape, { 105 | nested: 3, 106 | array: [1, 2], 107 | must: true, 108 | }); 109 | testPass(propTypes.shape, { 110 | nested: null, 111 | array: [null, 3], 112 | must: false, 113 | }); 114 | testPass(propTypes.shape, { 115 | nested: null, 116 | array: null, 117 | must: false, 118 | }); 119 | testFail(propTypes.shape, { 120 | nested: '3', 121 | array: null, 122 | must: false, 123 | }); 124 | testFail(propTypes.shape, { 125 | nested: 3, 126 | array: ['3'], 127 | must: false, 128 | }); 129 | testFail(propTypes.shape, null); 130 | 131 | testPass(propTypes.array, [3]); 132 | testPass(propTypes.array, []); 133 | testFail(propTypes.array, null); 134 | testFail(propTypes.array, [null]); 135 | testFail(propTypes.array, [3, '3']); 136 | 137 | testPass(propTypes.arrayOfObjects, [{value: ''}]); 138 | testPass(propTypes.arrayOfObjects, [{value: '', a: 3}]); 139 | testPass(propTypes.arrayOfObjects, [{value: null}]); 140 | testPass(propTypes.arrayOfObjects, [{}]); 141 | testPass(propTypes.arrayOfObjects, null); 142 | testFail(propTypes.arrayOfObjects, [null]); 143 | 144 | expect(propTypes.node).to.equal(PropTypes.node); 145 | expect(propTypes.element).to.equal(PropTypes.element.isRequired); 146 | 147 | testPass(propTypes.message, new Message()); 148 | testFail(propTypes.message, null); 149 | testFail(propTypes.message, Message); 150 | testFail(propTypes.message, new Date()); 151 | 152 | expect(propTypes.any).to.equal(PropTypes.any.isRequired); 153 | }); 154 | 155 | 156 | it('should allow local type overrides.', () => { 157 | class Message {} 158 | 159 | const parsePropTypes = createParser(PropTypes, {Message}); 160 | 161 | class LocalDate {} 162 | class LocalElement {} 163 | class LocalMessage {} 164 | 165 | const propTypes = parsePropTypes(`{ 166 | date: Date 167 | element: Element 168 | message: Message 169 | }`, { 170 | Date: LocalDate, 171 | Element: LocalElement, 172 | Message: LocalMessage, 173 | }); 174 | 175 | testPass(propTypes.date, new LocalDate()); 176 | testFail(propTypes.date, new Date()); 177 | 178 | testPass(propTypes.element, new LocalElement()); 179 | expect(propTypes.element).to.not.equal(PropTypes.element); 180 | 181 | testFail(propTypes.message, new Message()); 182 | testPass(propTypes.message, new LocalMessage()); 183 | }); 184 | 185 | 186 | it('should allow manually adding PropTypes.', () => { 187 | class Message {} 188 | 189 | const propTypes = createParser(PropTypes, { 190 | OptionalEnum: PropTypes.oneOf(['News', 'Photos']), 191 | OptionalUnion: PropTypes.oneOfType([ 192 | PropTypes.string, 193 | PropTypes.number, 194 | PropTypes.instanceOf(Message), 195 | ]), 196 | })(`{ 197 | optionalEnumValue: OptionalEnum 198 | requiredEnumValue: OptionalEnum! 199 | unionValue: OptionalUnion 200 | arrayUnionValue: [OptionalUnion!]! 201 | }`); 202 | 203 | testPass(propTypes.optionalEnumValue, 'News'); 204 | testPass(propTypes.optionalEnumValue, 'Photos'); 205 | testPass(propTypes.optionalEnumValue, null); 206 | testFail(propTypes.optionalEnumValue, 'Others'); 207 | 208 | testPass(propTypes.requiredEnumValue, 'News'); 209 | testPass(propTypes.requiredEnumValue, 'Photos'); 210 | testFail(propTypes.requiredEnumValue, null); 211 | testFail(propTypes.requiredEnumValue, 'Others'); 212 | 213 | testPass(propTypes.unionValue, '1'); 214 | testPass(propTypes.unionValue, 1); 215 | testPass(propTypes.unionValue, new Message()); 216 | testPass(propTypes.unionValue, null); 217 | testFail(propTypes.unionValue, true); 218 | 219 | testPass(propTypes.arrayUnionValue, ['1']); 220 | testPass(propTypes.arrayUnionValue, [1]); 221 | testPass(propTypes.arrayUnionValue, [new Message()]); 222 | testPass(propTypes.arrayUnionValue, []); 223 | testFail(propTypes.arrayUnionValue, [null]); 224 | }); 225 | 226 | 227 | it('should accept named PropTypes definition.', () => { 228 | const parser = createParser(PropTypes); 229 | 230 | const propTypes = parser(` 231 | Car { 232 | year: Number! 233 | model: String! 234 | } 235 | `); 236 | 237 | expect(propTypes).to.equal(parser.getPropTypes('Car')); 238 | 239 | expect(propTypes.year).to.equal(PropTypes.number.isRequired); 240 | expect(propTypes.model).to.equal(PropTypes.string.isRequired); 241 | }); 242 | 243 | 244 | it('should not allow name collisions.', () => { 245 | const parser = createParser(PropTypes); 246 | 247 | expect(() => { 248 | // Cannot override default type, String. 249 | const propTypes = parser(` 250 | String { 251 | year: Number! 252 | model: String! 253 | } 254 | `); 255 | }).to.throw(/already defined/i); 256 | 257 | const propTypes = parser(` 258 | Car { 259 | year: Number! 260 | model: String! 261 | } 262 | `); 263 | 264 | expect(() => { 265 | // Cannot override previously defined, Car. 266 | const propTypes = parser(` 267 | Car { 268 | year: Number! 269 | model: String! 270 | wheelCount: Number! 271 | } 272 | `); 273 | }).to.throw(/already defined/i); 274 | }); 275 | 276 | 277 | it('should allow composition with named PropTypes definitions.', () => { 278 | const parser = createParser(PropTypes); 279 | 280 | const carPropTypes = parser(` 281 | Car { 282 | year: Number! 283 | model: String! 284 | } 285 | `); 286 | 287 | const garagePropTypes = parser(` 288 | Garage { 289 | address: String! 290 | cars: [Car!]! 291 | } 292 | `); 293 | 294 | testPass(garagePropTypes.cars, [{year: 2014, model: 'Model 3'}]); 295 | testPass(garagePropTypes.cars, [{year: 2014, model: 'Model 3', make: 'Tesla'}]); 296 | testPass(garagePropTypes.cars, []); 297 | testFail(garagePropTypes.cars, [{model: 'Model 3'}]); 298 | testFail(garagePropTypes.cars, [null]); 299 | testFail(garagePropTypes.cars, null); 300 | }); 301 | 302 | 303 | it('should allow composition with spread operator.', () => { 304 | const parser = createParser(PropTypes); 305 | 306 | expect(() => { 307 | // Cannot spread unknown type. 308 | const carWithMakePropTypes = parser(` 309 | CarWithMake { 310 | ...Car 311 | make: String! 312 | } 313 | `); 314 | }).to.throw(/unknown type/i); 315 | 316 | const carPropTypes = parser(` 317 | Car { 318 | year: Number! 319 | model: String! 320 | } 321 | `); 322 | 323 | const carWithMakePropTypes = parser(` 324 | CarWithMake { 325 | ...Car 326 | make: String! 327 | } 328 | `); 329 | 330 | // carPropTypes stays untouched. 331 | expect(carPropTypes.make).to.be.undefined; 332 | 333 | // Inherited from Car 334 | expect(carWithMakePropTypes.year).to.equal(PropTypes.number.isRequired); 335 | expect(carWithMakePropTypes.model).to.equal(PropTypes.string.isRequired); 336 | 337 | // Additional field 338 | expect(carWithMakePropTypes.make).to.equal(PropTypes.string.isRequired); 339 | }); 340 | 341 | 342 | it('should allow adding types.', () => { 343 | const parser = createParser(PropTypes); 344 | 345 | class Message {} 346 | 347 | expect(() => { 348 | parser(`{ message: Message }`); 349 | }).to.throw(/Message/i); 350 | 351 | parser.addType('Message', Message); 352 | 353 | const propTypes = parser(`{ message: Message }`); 354 | 355 | testPass(propTypes.message, new Message()); 356 | testPass(propTypes.message, null); 357 | testFail(propTypes.message, Message); 358 | testFail(propTypes.message, {}); 359 | }); 360 | 361 | 362 | it('should allow adding propTypes.', () => { 363 | const parser = createParser(PropTypes); 364 | 365 | expect(() => { 366 | parser(`{ message: Message }`); 367 | }).to.throw(/Message/i); 368 | 369 | const messagePropTypes = parser(`{ 370 | from: String! 371 | to: String! 372 | text: String! 373 | }`); 374 | 375 | parser.addType('Message', messagePropTypes); 376 | 377 | const propTypes = parser(`{ message: Message }`); 378 | 379 | testPass(propTypes.message, { 380 | from: 'me', 381 | to: 'you', 382 | text: 'hi', 383 | }); 384 | 385 | testFail(propTypes.message, { 386 | from: 'me', 387 | text: 'hi', 388 | }); 389 | 390 | const messagePropTypesWithDate = parser(`{ 391 | ...Message 392 | date: Date! 393 | }`); 394 | 395 | expect(messagePropTypesWithDate.text).to.equal(PropTypes.string.isRequired); 396 | testPass(messagePropTypesWithDate.date, new Date()); 397 | testFail(messagePropTypesWithDate.date, null); 398 | 399 | parser.addType('NewsOrPhotos', PropTypes.oneOf(['News', 'Photos'])); 400 | const propTypesWithEnum = parser(`{ 401 | mediaType: NewsOrPhotos 402 | }`); 403 | 404 | testPass(propTypesWithEnum.mediaType, undefined); 405 | testPass(propTypesWithEnum.mediaType, 'News'); 406 | testPass(propTypesWithEnum.mediaType, 'Photos'); 407 | testFail(propTypesWithEnum.mediaType, 'Video'); 408 | }); 409 | 410 | 411 | it('should support union type.', () => { 412 | const parser = createParser(PropTypes); 413 | 414 | const propTypes = parser(`{ 415 | stringOrNumberOrNull: String | Number 416 | stringOrNumber: String! | Number! 417 | stringOrNumberGroup: (String | Number)! 418 | stringOrNumberOrBoolean: (String | Number | Boolean)! 419 | stringOrNumberOrBoolean2: ((String | Number)! | Boolean)! 420 | }`); 421 | 422 | testPass(propTypes.stringOrNumberOrNull, 'a'); 423 | testPass(propTypes.stringOrNumberOrNull, 1); 424 | testPass(propTypes.stringOrNumberOrNull, null); 425 | testFail(propTypes.stringOrNumberOrNull, true); 426 | 427 | testPass(propTypes.stringOrNumber, 'a'); 428 | testPass(propTypes.stringOrNumber, 1); 429 | testFail(propTypes.stringOrNumber, null); 430 | testFail(propTypes.stringOrNumber, true); 431 | 432 | testPass(propTypes.stringOrNumberGroup, 'a'); 433 | testPass(propTypes.stringOrNumberGroup, 1); 434 | testFail(propTypes.stringOrNumberGroup, null); 435 | testFail(propTypes.stringOrNumberGroup, true); 436 | 437 | testPass(propTypes.stringOrNumberOrBoolean, 'a'); 438 | testPass(propTypes.stringOrNumberOrBoolean, 1); 439 | testPass(propTypes.stringOrNumberOrBoolean, true); 440 | testFail(propTypes.stringOrNumberOrBoolean, null); 441 | testFail(propTypes.stringOrNumberOrBoolean, undefined); 442 | testFail(propTypes.stringOrNumberOrBoolean, []); 443 | testFail(propTypes.stringOrNumberOrBoolean, {}); 444 | 445 | testPass(propTypes.stringOrNumberOrBoolean2, 'a'); 446 | testPass(propTypes.stringOrNumberOrBoolean2, 1); 447 | testPass(propTypes.stringOrNumberOrBoolean2, true); 448 | testFail(propTypes.stringOrNumberOrBoolean2, null); 449 | testFail(propTypes.stringOrNumberOrBoolean2, undefined); 450 | testFail(propTypes.stringOrNumberOrBoolean2, []); 451 | testFail(propTypes.stringOrNumberOrBoolean2, {}); 452 | }); 453 | }); 454 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers js:babel-register 2 | --require ./test/setup.js 3 | --reporter spec 4 | --timeout 5000 5 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | chai.use(require('chai-as-promised')); 3 | --------------------------------------------------------------------------------