├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── __tests__ ├── csr.test.tsx └── ssr.test.tsx ├── package-lock.json ├── package.json ├── src └── index.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | index.js 3 | index.d.ts 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | __tests__/ 3 | tsconfig.json 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Raiden 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 | # react-router-prop-types 2 | 3 | Runtime type checking for [react-router](https://github.com/ReactTraining/react-router) props 4 | 5 | ## Installation 6 | 7 | ```shell 8 | npm install react-router-prop-types --save 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```jsx 14 | import React from 'react'; 15 | import ReactRouterPropTypes from 'react-router-prop-types'; 16 | 17 | class MyComponent extends React.Component { 18 | static propTypes = { 19 | // You can chain any of the above with `isRequired` to make sure a warning 20 | // is shown if the prop isn't provided. 21 | history: ReactRouterPropTypes.history.isRequired, 22 | location: ReactRouterPropTypes.location.isRequired, 23 | match: ReactRouterPropTypes.match.isRequired, 24 | route: ReactRouterPropTypes.route.isRequired, // for react-router-config 25 | } 26 | render() { 27 | // ... 28 | } 29 | } 30 | ``` 31 | -------------------------------------------------------------------------------- /__tests__/csr.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Router, Route } from 'react-router'; 4 | import { createBrowserHistory, createHashHistory, createMemoryHistory } from 'history'; 5 | import ReactRouterPropTypes from '../src'; 6 | 7 | describe('client side rendering', () => { 8 | const warning = expect.stringMatching(/(Invalid prop|Failed prop type)/gi); 9 | 10 | const node = document.createElement('div'); 11 | 12 | afterEach(() => { 13 | ReactDOM.unmountComponentAtNode(node); 14 | }); 15 | 16 | class TestComponent extends React.Component { 17 | static propTypes = { 18 | history: ReactRouterPropTypes.history.isRequired, 19 | location: ReactRouterPropTypes.location.isRequired, 20 | match: ReactRouterPropTypes.match.isRequired, 21 | } 22 | render() { 23 | return null; 24 | } 25 | } 26 | 27 | test('browser history', () => { 28 | spyOn(console, 'error'); 29 | ReactDOM.render( 30 | 31 | 32 | , 33 | node, 34 | ); 35 | expect(console.error).not.toHaveBeenCalledWith(warning); 36 | }); 37 | 38 | test('hash history', () => { 39 | spyOn(console, 'error'); 40 | ReactDOM.render( 41 | 42 | 43 | , 44 | node, 45 | ); 46 | expect(console.error).not.toHaveBeenCalledWith(warning); 47 | }); 48 | 49 | test('memory history', () => { 50 | spyOn(console, 'error'); 51 | ReactDOM.render( 52 | 53 | 54 | , 55 | node, 56 | ); 57 | expect(console.error).not.toHaveBeenCalledWith(warning); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /__tests__/ssr.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOMServer from 'react-dom/server'; 3 | import { StaticRouter, Route } from 'react-router'; 4 | import { renderRoutes } from 'react-router-config'; 5 | import ReactRouterPropTypes from '../src'; 6 | 7 | describe('server side rendering', () => { 8 | const warning = expect.stringMatching(/(Invalid prop|Failed prop type)/gi); 9 | 10 | test('simple ssr', () => { 11 | spyOn(console, 'error'); 12 | class TestComponent extends React.Component { 13 | static propTypes = { 14 | history: ReactRouterPropTypes.history.isRequired, 15 | location: ReactRouterPropTypes.location.isRequired, 16 | match: ReactRouterPropTypes.match.isRequired, 17 | } 18 | render() { 19 | return null; 20 | } 21 | } 22 | ReactDOMServer.renderToString( 23 | 24 | 25 | 26 | ); 27 | expect(console.error).not.toHaveBeenCalledWith(warning); 28 | }); 29 | 30 | test('react-router-config', () => { 31 | spyOn(console, 'error'); 32 | class TestComponent extends React.Component { 33 | static propTypes = { 34 | history: ReactRouterPropTypes.history.isRequired, 35 | location: ReactRouterPropTypes.location.isRequired, 36 | match: ReactRouterPropTypes.match.isRequired, 37 | route: ReactRouterPropTypes.route.isRequired, 38 | } 39 | render() { 40 | return null; 41 | } 42 | } 43 | const routes = [ 44 | { 45 | path: '/root', 46 | component: TestComponent, 47 | routes: [ 48 | { 49 | path: '/child', 50 | component: TestComponent, 51 | } 52 | ], 53 | } 54 | ]; 55 | ReactDOMServer.renderToString( 56 | 57 | {renderRoutes(routes)} 58 | 59 | ); 60 | expect(console.error).not.toHaveBeenCalledWith(warning); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-router-prop-types", 3 | "version": "1.0.5", 4 | "description": "Runtime type checking for react-router props", 5 | "main": "index.js", 6 | "types": "index.d.js", 7 | "scripts": { 8 | "prepublishOnly": "tsc", 9 | "test": "jest" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/Javascript-Ninja/react-router-prop-types.git" 14 | }, 15 | "keywords": [ 16 | "react", 17 | "route", 18 | "router", 19 | "react-router", 20 | "proptype", 21 | "proptypes", 22 | "props" 23 | ], 24 | "author": "Raiden ", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/Javascript-Ninja/react-router-prop-types/issues" 28 | }, 29 | "homepage": "https://github.com/Javascript-Ninja/react-router-prop-types#readme", 30 | "dependencies": { 31 | "@types/prop-types": "^15.7.3", 32 | "prop-types": "^15.7.2" 33 | }, 34 | "devDependencies": { 35 | "@types/history": "^4.7.7", 36 | "@types/jest": "^26.0.12", 37 | "@types/react": "^16.9.49", 38 | "@types/react-dom": "^16.9.8", 39 | "@types/react-router": "^5.1.8", 40 | "@types/react-router-config": "^5.0.1", 41 | "history": "^5.0.0", 42 | "jest": "^26.4.2", 43 | "react": "^16.13.1", 44 | "react-dom": "^16.13.1", 45 | "react-router": "^5.2.0", 46 | "react-router-config": "^5.1.1", 47 | "ts-jest": "^26.3.0", 48 | "typescript": "^4.0.2" 49 | }, 50 | "jest": { 51 | "transform": { 52 | "^.+\\.tsx?$": "ts-jest" 53 | }, 54 | "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$", 55 | "moduleFileExtensions": [ 56 | "ts", 57 | "tsx", 58 | "js", 59 | "jsx", 60 | "json" 61 | ] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as PropTypes from 'prop-types'; 2 | 3 | export const location = PropTypes.shape({ 4 | hash: PropTypes.string.isRequired, 5 | key: PropTypes.string, // only in createBrowserHistory and createMemoryHistory 6 | pathname: PropTypes.string.isRequired, 7 | search: PropTypes.string.isRequired, 8 | state: PropTypes.oneOfType([ 9 | PropTypes.array, 10 | PropTypes.bool, 11 | PropTypes.number, 12 | PropTypes.object, 13 | PropTypes.string, 14 | ]), // only in createBrowserHistory and createMemoryHistory 15 | }); 16 | 17 | export const history = PropTypes.shape({ 18 | action: PropTypes.oneOf(['PUSH', 'REPLACE', 'POP']).isRequired, 19 | block: PropTypes.func.isRequired, 20 | createHref: PropTypes.func.isRequired, 21 | go: PropTypes.func.isRequired, 22 | goBack: PropTypes.func, // only in server side rendering 23 | goForward: PropTypes.func, // only in server side rendering 24 | back: PropTypes.func, // only in client side rendering 25 | forward: PropTypes.func, // only in client side rendering 26 | index: PropTypes.number, // only in createMemoryHistory 27 | length: PropTypes.number, 28 | listen: PropTypes.func.isRequired, 29 | location: location.isRequired, 30 | push: PropTypes.func.isRequired, 31 | replace: PropTypes.func.isRequired, 32 | }); 33 | 34 | export const match = PropTypes.shape({ 35 | isExact: PropTypes.bool, 36 | params: PropTypes.object.isRequired, 37 | path: PropTypes.string.isRequired, 38 | url: PropTypes.string.isRequired, 39 | }); 40 | 41 | const routeShape: any = { 42 | path: PropTypes.oneOfType([ 43 | PropTypes.string, 44 | PropTypes.arrayOf(PropTypes.string), 45 | ]), 46 | exact: PropTypes.bool, 47 | strict: PropTypes.bool, 48 | sensitive: PropTypes.bool, 49 | component: PropTypes.func, 50 | }; 51 | routeShape.routes = PropTypes.arrayOf(PropTypes.shape(routeShape)); 52 | 53 | export const route = PropTypes.shape(routeShape); 54 | 55 | export default { 56 | location, 57 | history, 58 | match, 59 | route, 60 | }; 61 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "./", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | 43 | /* Module Resolution Options */ 44 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 48 | // "typeRoots": [], /* List of folders to include type definitions from. */ 49 | // "types": [], /* Type declaration files to be included in compilation. */ 50 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 51 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 52 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 53 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 54 | 55 | /* Source Map Options */ 56 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 59 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 60 | 61 | /* Experimental Options */ 62 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 63 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 64 | 65 | /* Advanced Options */ 66 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 67 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 68 | }, 69 | "include": ["src/**/*"], 70 | "exclude": ["node_modules"], 71 | } 72 | --------------------------------------------------------------------------------