├── .all-contributorsrc ├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md └── workflows │ └── ci.yml ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── .vscode └── tasks.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── banner.png ├── docs ├── .nojekyll ├── README.md ├── _sidebar.md ├── googlebe3d4fe76c235fa6.html └── index.html ├── eslint.json ├── examples ├── simple-js-app │ ├── index.js │ └── routes │ │ ├── .ignored-route.js │ │ ├── _ignored-route.js │ │ ├── _igored-subpath │ │ └── index.js │ │ ├── index.js │ │ └── users │ │ ├── index.js │ │ └── {userId} │ │ ├── comments.js │ │ └── index.js └── simple-ts-app │ ├── index.ts │ └── routes │ ├── .ignored-route.ts │ ├── _ignored-route.ts │ ├── _igored-subpath │ └── index.ts │ ├── index.ts │ └── users │ ├── index.ts │ └── {userId} │ ├── comments.ts │ └── index.ts ├── jest.config.js ├── package.json ├── src └── index.ts ├── tests ├── invalid-routes.test.ts ├── options.test.ts ├── routes.test.ts └── utils │ └── mock.ts └── tsconfig.json /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "projectName": "fastify-autoroutes", 6 | "projectOwner": "GiovanniCardamone", 7 | "repoType": "github", 8 | "repoHost": "https://github.com", 9 | "imageSize": 100, 10 | "commit": true, 11 | "commitConvention": "none", 12 | "contributors": [ 13 | { 14 | "login": "GiovanniCardamone", 15 | "name": "Giovanni Cardamone", 16 | "avatar_url": "https://avatars0.githubusercontent.com/u/5117748?v=4", 17 | "profile": "http://giovannicardamone.github.io", 18 | "contributions": [ 19 | "code", 20 | "doc", 21 | "example", 22 | "maintenance" 23 | ] 24 | }, 25 | { 26 | "login": "genbs", 27 | "name": "Gennaro", 28 | "avatar_url": "https://avatars0.githubusercontent.com/u/6159598?v=4", 29 | "profile": "https://github.com/genbs", 30 | "contributions": [ 31 | "design" 32 | ] 33 | } 34 | ], 35 | "contributorsPerLine": 7, 36 | "skipCi": true 37 | } 38 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | [*.{js,ts}] 12 | indent_size = 2 13 | indent_style = space 14 | 15 | 16 | [*.{json,yml}] 17 | indent_size = 4 18 | indent_style = space 19 | 20 | [.prettierrc] 21 | indent_size = 4 22 | indent_style = space 23 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: GiovanniCardamone 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug report 3 | about: Create a report to help us improve 4 | --- 5 | 6 | 7 | 8 | ### Versions 9 | 10 | 11 | 12 | - **Logger Version:** v1.0.0 13 | - **Node Version:** v13.0.0 14 | - **Operating System:** Windows 10 15 | - **Terminal:** Windows Powershell 16 | 17 | ### Expected Behavior 18 | 19 | 20 | 21 | ### Actual Behavior 22 | 23 | 25 | 26 | ### Steps to Reproduce 27 | 28 | 30 | 31 | 1. ... 32 | 2. ... 33 | 3. ... 34 | 35 | ### Screenshots (Optional) 36 | 37 | 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 💫 Feature request 3 | about: Suggest an idea for this project 4 | --- 5 | 6 | 7 | 8 | ### Feature description 9 | 10 | 11 | 12 | ### Feature motivation 13 | 14 | 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🗨 Question 3 | about: Ask a question 4 | --- 5 | 6 | 7 | 8 | ### Question 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - 'docs/**' 9 | - '*.md' 10 | 11 | jobs: 12 | # build / test 13 | Build: 14 | name: Build Package 15 | runs-on: ubuntu-latest 16 | steps: 17 | # Checkout Repo 18 | - uses: actions/checkout@v2 19 | 20 | # Setup Node 21 | - uses: actions/setup-node@v2 22 | with: 23 | node-version: 14 24 | 25 | # Install Dependencies 26 | - run: npm ci 27 | 28 | # Build Package 29 | - run: npm run build 30 | 31 | # Run Tests 32 | - run: npm run test:coverage 33 | 34 | # Report Coverage 35 | - name: Coveralls GitHub Action 36 | uses: coverallsapp/github-action@1.1.3 37 | with: 38 | github-token: ${{ secrets.GITHUB_TOKEN }} 39 | 40 | # create / release 41 | Release: 42 | needs: Build 43 | name: Create Release Package 44 | runs-on: ubuntu-latest 45 | steps: 46 | # Checkout Repo 47 | - uses: actions/checkout@v2 48 | 49 | # Setup Node 50 | - uses: actions/setup-node@v2 51 | with: 52 | node-version: 14 53 | 54 | # Show current package 55 | - name: cat package.json 56 | run: cat ./package.json 57 | 58 | # Install Dependencies 59 | - run: npm ci 60 | 61 | # Build package 62 | - run: npm run build 63 | 64 | # Bump version, tag commit and generate a changelog 65 | - name: Conventional Changelog Action 66 | uses: TriPSs/conventional-changelog-action@v3 67 | with: 68 | github-token: ${{ secrets.GITHUB_TOKEN }} 69 | git-message: 'chore(release): {version}' 70 | tag-prefix: 'v' 71 | output-file: 'CHANGELOG.md' 72 | release-count: '10' 73 | skip-on-empty: 'false' 74 | skip-version-file: 'false' 75 | skip-commit: 'false' 76 | 77 | # Publish to NPM 78 | - name: Publish to NPM 79 | uses: JS-DevTools/npm-publish@v1 80 | with: 81 | token: ${{ secrets.NPM_TOKEN }} 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | package-lock.json 61 | 62 | dist 63 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm test 5 | 6 | npm run docs 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | README.md 3 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "f1", 6 | "type": "npm", 7 | "script": "build:watch", 8 | "group": "build", 9 | "problemMatcher": ["$tsc"] 10 | }, 11 | { 12 | "label": "f2", 13 | "type": "npm", 14 | "script": "test:watch", 15 | "group": "test", 16 | "problemMatcher": ["$tsc"] 17 | }, 18 | { 19 | "label": "f3", 20 | "type": "npm", 21 | "script": "build:watch", 22 | "group": "build", 23 | "problemMatcher": ["$tsc"] 24 | }, 25 | { 26 | "label": "f4", 27 | "type": "npm", 28 | "script": "test", 29 | "group": "test", 30 | "problemMatcher": ["$tsc"] 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to fastify-autoroutes 2 | 3 | ## Developer's Certificate of Origin 1.1 4 | 5 | By making a contribution to this project, I certify that: 6 | 7 | - (a) The contribution was created in whole or in part by me and I 8 | have the right to submit it under the open source license 9 | indicated in the file; or 10 | 11 | - (b) The contribution is based upon previous work that, to the best 12 | of my knowledge, is covered under an appropriate open source 13 | license and I have the right under that license to submit that 14 | work with modifications, whether created in whole or in part 15 | by me, under the same open source license (unless I am 16 | permitted to submit under a different license), as indicated 17 | in the file; or 18 | 19 | - (c) The contribution was provided directly to me by some other 20 | person who certified (a), (b) or (c) and I have not modified 21 | it. 22 | 23 | - (d) I understand and agree that this project and the contribution 24 | are public and that a record of the contribution (including all 25 | personal information I submit with it, including my sign-off) is 26 | maintained indefinitely and may be redistributed consistent with 27 | this project or the open source license(s) involved. 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 fastify-autoroutes 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 | # fastify-autoroutes 2 | 3 |
4 | 5 | ![Banner](./banner.png) 6 | 7 | ![JavaScript](https://img.shields.io/badge/ES6-Supported-yellow.svg?style=for-the-badge&logo=JavaScript)   ![TypeScript](https://img.shields.io/badge/TypeScript-Supported-blue.svg?style=for-the-badge) 8 | 9 | [![NPM version](https://img.shields.io/npm/v/fastify-autoroutes.svg?style=flat)](https://www.npmjs.com/package/fastify-autoroutes) 10 | [![NPM downloads](https://img.shields.io/npm/dm/fastify-autoroutes.svg?style=flat)](https://www.npmjs.com/package/fastify-autoroutes) 11 | [![Known Vulnerabilities](https://snyk.io/test/github/GiovanniCardamone/fastify-autoroutes/badge.svg)](https://snyk.io/test/github/GiovanniCardamone/fastify-autoroutes) 12 | [![GitHub license](https://img.shields.io/github/license/GiovanniCardamone/fastify-autoroutes.svg)](https://github.com/GiovanniCardamone/fastify-autoroutes/blob/master/LICENSE) 13 | 14 | ![CI](https://github.com/GiovanniCardamone/fastify-autoroutes/workflows/CI/badge.svg?branch=master) 15 | [![Coverage Status](https://coveralls.io/repos/github/GiovanniCardamone/fastify-autoroutes/badge.svg?branch=master)](https://coveralls.io/github/GiovanniCardamone/fastify-autoroutes?branch=master) 16 | 17 |
18 | 19 | > :star: Thanks to everyone who has starred the project, it means a lot! 20 | 21 | Plugin to handle routes in fastify automatically based on directory structure. 22 | 23 | ## :newspaper: **[Full Documentation](https://giovannicardamone.github.io/fastify-autoroutes/)** 24 | 25 | [fastify-autoroutes](https://giovannicardamone.github.io/fastify-autoroutes/) 26 | 27 | ## :rocket: Install 28 | 29 | ```sh 30 | npm install --save fastify-autoroutes 31 | ``` 32 | 33 | ## :blue_book: Usage 34 | 35 | ### Register plugin 36 | 37 | ```js 38 | const fastify = require('fastify') 39 | const server = fastify() 40 | 41 | server.register(require('fastify-autoroutes'), { 42 | dir: './', // relative to your cwd 43 | prefix: '/api', // optional, don't use if you do not need prefixes 44 | }) 45 | ``` 46 | 47 | ### Create file in autoroutes directory 48 | 49 | ```js 50 | //file: `/some/route.js` 51 | //url: `http://your-host/some/route` 52 | 53 | export default (fastifyInstance) => ({ 54 | get: { 55 | handler: async (request, reply) => 'Hello, Route' 56 | }, 57 | }) 58 | ``` 59 | 60 | ### Using typescript support for module 61 | 62 | ```typescript 63 | //file: `/some/route.ts` 64 | //url: `http://your-host/some/route` 65 | 66 | import { FastifyInstance } from 'fastify' 67 | import { Resource } from 'fastify-autoroutes' 68 | 69 | export default (fastify: FastifyInstance) => { 70 | get: { 71 | handler: async (request: FastifyRequest, reply: FastifyReply) => 'Hello, Route!' 72 | } 73 | } 74 | ``` 75 | 76 | ### Accepts params in autoroutes 77 | 78 | > :information_source: file/directory name must follow syntax `:paramName` or `{paramName}` 79 | 80 | ```js 81 | //file: `/users/{userId}/photos.js` 82 | //mapped to: `/users/:userId/photos` 83 | 84 | export default (fastifyInstance) => ({ 85 | get: { 86 | handler: (request, reply) => { 87 | reply.send(`photos of user ${request.params.userId}`) 88 | } 89 | }, 90 | }) 91 | ``` 92 | 93 | ## :arrow_forward: Route module definition 94 | 95 | Method specification for attributes is available here: [Method specification](https://www.fastify.io/docs/latest/Routes/#full-declaration) 96 | 97 | > :information_source: attributes `url` and `method` are dynamically provided 98 | 99 | Allowed attributes mapped to Http methods in module: 100 | 101 | - delete 102 | - get 103 | - head 104 | - patch 105 | - post 106 | - put 107 | - options 108 | 109 | ## :arrow_forward: Skipping files 110 | 111 | to skip file in routes directory, prepend the `.` or `_` character to filename 112 | 113 | examples: 114 | 115 | ```text 116 | routes 117 | ├── .ignored-directory 118 | ├── _ignored-directory 119 | ├── .ignored-js-file.js 120 | ├── _ignored-js-file.js 121 | ├── .ignored-ts-file.ts 122 | ├── _ignored-ts-file.ts 123 | ├── ignored-js-test.test.js 124 | └── ignored-ts-test.test.ts 125 | ``` 126 | 127 | > :warning: also any `*.test.js` and `*.test.ts` are skipped! 128 | 129 | this is useful if you want to have a lib file which contains functions that don't have to be a route, so just create the file with `_` prepending character 130 | 131 | ## :page_facing_up: License 132 | 133 | Licensed under [MIT](./LICENSE) 134 | 135 | ## :sparkles: Contributors 136 | 137 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 |

Giovanni Cardamone

💻 📖 💡 🚧

Gennaro

🎨
148 | 149 | 150 | 151 | 152 | 153 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. 154 | 155 | Contributions of any kind welcome! 156 | -------------------------------------------------------------------------------- /banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GiovanniCardamone/fastify-autoroutes/559d27ff2400734d09dd4976e2161380f5dfa900/banner.png -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GiovanniCardamone/fastify-autoroutes/559d27ff2400734d09dd4976e2161380f5dfa900/docs/.nojekyll -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # fastify-autoroutes 2 | 3 | fastify-autoroutes is an innovative way to declare routes in your fastify app. 4 | 5 | Just specify a directory for routes and fastify-autoroutes will care of import any routes in a same way is defined in your filesystem structures. 6 | 7 | ## Getting started 8 | 9 | ### Install 10 | 11 | Install with npm: 12 | 13 | ```sh 14 | npm i fastify-autoroutes --save 15 | ``` 16 | 17 | Install with yarn: 18 | 19 | ```sh 20 | yarn add fastify-autoroutes 21 | ``` 22 | 23 | ### Initialize plugin 24 | 25 | create a root directory for your autoroutes and pass it to plugin 26 | 27 | ```javascript 28 | const fastify = require('fastify') 29 | const server = fastify() 30 | server.register(require('fastify-autoroutes'), { 31 | dir: './routes', // routes is my directory in this example 32 | }) 33 | ``` 34 | 35 | ### Write your first route 36 | 37 | Javascript: 38 | 39 | ```javascript 40 | //file: ./routes/index.js 41 | //url: http://your-host 42 | 43 | export default (fastifyInstance) => ({ 44 | get: { 45 | handler: async () => 'Hello, Index!' 46 | } 47 | }) 48 | 49 | ``` 50 | 51 | Typescript: 52 | 53 | ```typescript 54 | //file: ./routes/index.js 55 | //url: http://your-host 56 | 57 | import { FastifyInstance } from 'fastify' 58 | import { Resource } from 'fastify-autoroutes' 59 | 60 | export default (fastifyInstance: FastifyInstance) => { 61 | get: { 62 | handler: async () => 'Hello, Index!' 63 | } 64 | } 65 | ``` 66 | 67 | Directory tree of your file system will look like this: 68 | 69 | ```text 70 | / 71 | ├── index.js 72 | ├── package.json 73 | └── routes 74 | └── index.js 75 | ``` 76 | 77 | ## Nested autoroutes 78 | 79 | Autoroutes directory scenario: 80 | 81 | ```text 82 | / 83 | ├── index.js 84 | ├── package.json 85 | └── routes 86 | └── hello => http://your-host/hello 87 | └── world.js => http://your-host/hello/world 88 | ``` 89 | 90 | in this case, the plugin will recursively scan any routes in directory and map it urls 91 | 92 | > :warning: those two directory structure are **NOT** equivalent: 93 | > 94 | > ```text 95 | > / | / 96 | > ├── index.js | ├── index.js 97 | > ├── package.json | ├── package.json 98 | > └── routes | └── routes 99 | > └── hello | └── hello 100 | > └── world.js | └── world 101 | > | └── index.js 102 | > | 103 | > mapped to url: | mapped to url: 104 | > http://your-host/hello/world | http://your-host/hello/world/ 105 | > ``` 106 | 107 | > :information_source: you have to set `ignoreTrailingSlash: true` to make it the same. 108 | 109 | ## Ignore routes 110 | 111 | to ignore routes there are few way: 112 | 113 | - prepend '.' character to your file/directory name 114 | - prepend '_' character to your file/directory name 115 | 116 | > :information_source: files `*.test.js` and `*.test.ts` are automatically ignored 117 | 118 | examples: 119 | 120 | ```text 121 | routes 122 | ├── .ignored-directory 123 | ├── _ignored-directory 124 | ├── .ignored-js-file.js 125 | ├── _ignored-js-file.js 126 | ├── .ignored-ts-file.ts 127 | ├── _ignored-ts-file.ts 128 | ├── ignored-js-test.test.js 129 | └── ignored-ts-test.test.ts 130 | ``` 131 | 132 | ## Url parameters in autoroutes 133 | 134 | parameters in URL can be specified using `liquid-variable-syntax` or (Not on windows) prepending `:` to the name of file or directory 135 | 136 | examples: 137 | 138 | using liquid variable syntax 139 | 140 | ```text 141 | . 142 | ├── index.js 143 | ├── package.json 144 | └── routes 145 | └── users 146 | ├── {userId} 147 | │ └── posts.js => http://your-host/users//posts 148 | └── {userId}.js => http://your-host/users/ 149 | ``` 150 | 151 | using `:` syntax (Not on windows) 152 | 153 | ```text 154 | . 155 | ├── index.js 156 | ├── package.json 157 | └── routes 158 | └── users 159 | ├── :userId 160 | │ └── posts.js => http://your-host/users//posts 161 | └── :userId.js => http://your-host/users/ 162 | ``` 163 | 164 | ### Retrieve parameters 165 | 166 | Parameters will be injected in route just like normal url matching syntax: 167 | 168 | ```javascript 169 | //file: ./routes/{userId}/posts.js 170 | 171 | export default (fastifyInstance) => ({ 172 | get: { 173 | handler: async (request) => `returning posts of user: ${request.params.userId}` 174 | } 175 | }) 176 | ``` 177 | 178 | ## Accepted methods in module 179 | 180 | each file must export a function that accept fastify as parameter, and return an object with the following properties: 181 | 182 | ```javascript 183 | export default (fastifyInstance) => ({ 184 | delete: { 185 | // your handler logic 186 | }, 187 | get: { 188 | // your handler logic 189 | }, 190 | head: { 191 | // your handler logic 192 | }, 193 | patch: { 194 | // your handler logic 195 | }, 196 | post: { 197 | // your handler logic 198 | }, 199 | put: { 200 | // your handler logic 201 | }, 202 | options: { 203 | // your handler logic 204 | }, 205 | }) 206 | 207 | ``` 208 | 209 | ### Route definition 210 | 211 | Each route should be compliant to fastify route: [Method Specification](https://www.fastify.io/docs/latest/Routes/#full-declaration) 212 | 213 | the only exceptions is for `url` and `method` which are automatically mapped by project structure. 214 | 215 | ## Example using Javascript version es3 216 | 217 | ```javascript 218 | module.exports = function (fastifyInstance) { 219 | return { 220 | get: { 221 | handler: function (request, reply) { 222 | reply.send('this is get method') 223 | }, 224 | }, 225 | } 226 | } 227 | ``` 228 | 229 | ## Typescript support for modules 230 | 231 | is useful to have typescript for strict type check of the module. 232 | use definition `Resource` for type check your route 233 | 234 | ```typescript 235 | import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify' 236 | import { Resource } from 'fastify-autoroutes' 237 | 238 | export default (fastify: FastifyInstance) => { 239 | return { 240 | post: { 241 | handler: async (request: FastifyRequest, reply: FastifyReply) => { 242 | return 'Hello, World!' 243 | }, 244 | }, 245 | } 246 | } 247 | ``` 248 | 249 | ## Contribute 250 | 251 | That's all, i hope you like my little module and contribution of any kind are welcome! 252 | 253 | I will mention you in my README for any of yours contribution 254 | 255 | Consider to leave a :star: if you like the project :blush: 256 | -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | - [README](README.md) 2 | -------------------------------------------------------------------------------- /docs/googlebe3d4fe76c235fa6.html: -------------------------------------------------------------------------------- 1 | google-site-verification: googlebe3d4fe76c235fa6.html -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Document 6 | 7 | 8 | 12 | 16 | 17 | 18 |
19 | 27 | 31 | 32 | 33 | 34 | 35 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended", "prettier"], 3 | "env": { 4 | "es6": true, 5 | "node": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/simple-js-app/index.js: -------------------------------------------------------------------------------- 1 | import fastify from 'fastify' 2 | import autoroutes from 'fastify-autoroutes' 3 | 4 | const server = fastify() 5 | 6 | server.register(autoroutes, { 7 | dir: './routes', 8 | }) 9 | 10 | server.listen(9999) 11 | -------------------------------------------------------------------------------- /examples/simple-js-app/routes/.ignored-route.js: -------------------------------------------------------------------------------- 1 | export default (fastify) => { 2 | return { 3 | get: { 4 | handler: (request, reply) => { 5 | reply.send('i am ignored') 6 | }, 7 | }, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/simple-js-app/routes/_ignored-route.js: -------------------------------------------------------------------------------- 1 | export default (fastify) => { 2 | return { 3 | get: { 4 | handler: (request, reply) => { 5 | reply.send('i am ignored') 6 | }, 7 | }, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/simple-js-app/routes/_igored-subpath/index.js: -------------------------------------------------------------------------------- 1 | export default (fastify) => { 2 | return { 3 | get: { 4 | handler: (request, reply) => { 5 | reply.send('i am ignored') 6 | }, 7 | }, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/simple-js-app/routes/index.js: -------------------------------------------------------------------------------- 1 | export default (fastify) => { 2 | return { 3 | get: { 4 | handler: (request, reply) => { 5 | reply.send('hello index') 6 | }, 7 | }, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/simple-js-app/routes/users/index.js: -------------------------------------------------------------------------------- 1 | export default (fastify) => { 2 | return { 3 | get: { 4 | handler: (request, reply) => { 5 | reply.send('hello users') 6 | }, 7 | }, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/simple-js-app/routes/users/{userId}/comments.js: -------------------------------------------------------------------------------- 1 | export default (fastify) => { 2 | return { 3 | get: { 4 | handler: (request, reply) => { 5 | reply.send(`hello user ${request.params.userId} comments`) 6 | }, 7 | }, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/simple-js-app/routes/users/{userId}/index.js: -------------------------------------------------------------------------------- 1 | export default (fastify) => { 2 | return { 3 | get: { 4 | handler: (request, reply) => { 5 | reply.send(`hello user ${request.params.userId}`) 6 | }, 7 | }, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/simple-ts-app/index.ts: -------------------------------------------------------------------------------- 1 | import fastify from 'fastify' 2 | import autoroutes from 'fastify-autoroutes' 3 | 4 | const server = fastify() 5 | 6 | server.register(autoroutes, { 7 | dir: './routes', 8 | }) 9 | 10 | server.listen(9999) 11 | -------------------------------------------------------------------------------- /examples/simple-ts-app/routes/.ignored-route.ts: -------------------------------------------------------------------------------- 1 | import fastify, { FastifyInstance } from 'fastify' 2 | import { Resource } from '../../../dist' 3 | 4 | export default (fastify) => { 5 | return { 6 | get: { 7 | handler: (request, reply) => { 8 | reply.send('i am ignored') 9 | }, 10 | }, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/simple-ts-app/routes/_ignored-route.ts: -------------------------------------------------------------------------------- 1 | import fastify, { FastifyInstance } from 'fastify' 2 | import { Resource } from '../../../dist' 3 | 4 | export default (fastify) => { 5 | return { 6 | get: { 7 | handler: (request, reply) => { 8 | reply.send('i am ignored') 9 | }, 10 | }, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/simple-ts-app/routes/_igored-subpath/index.ts: -------------------------------------------------------------------------------- 1 | import fastify, { FastifyInstance } from 'fastify' 2 | import { Resource } from '../../../../dist' 3 | 4 | export default (fastify: FastifyInstance) => { 5 | return { 6 | get: { 7 | handler: (request, reply) => { 8 | reply.send('i am ignored') 9 | }, 10 | }, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/simple-ts-app/routes/index.ts: -------------------------------------------------------------------------------- 1 | import fastify, { FastifyInstance } from 'fastify' 2 | import { Resource } from '../../../dist' 3 | 4 | export default (fastify: FastifyInstance) => { 5 | return { 6 | get: { 7 | handler: (request, reply) => { 8 | reply.send('hello index') 9 | }, 10 | }, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/simple-ts-app/routes/users/index.ts: -------------------------------------------------------------------------------- 1 | import fastify, { FastifyInstance } from 'fastify' 2 | import { Resource } from '../../../../dist' 3 | 4 | export default (fastify: FastifyInstance) => { 5 | return { 6 | get: { 7 | handler: (request, reply) => { 8 | reply.send('hello users') 9 | }, 10 | }, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/simple-ts-app/routes/users/{userId}/comments.ts: -------------------------------------------------------------------------------- 1 | import fastify, { FastifyInstance } from 'fastify' 2 | import { Resource } from '../../../../../dist' 3 | 4 | interface Params { 5 | userId: string 6 | } 7 | 8 | export default (fastify: FastifyInstance) => { 9 | return { 10 | get: { 11 | handler: (request, reply) => { 12 | reply.send(`hello user ${request.params.userId} comments`) 13 | }, 14 | }, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/simple-ts-app/routes/users/{userId}/index.ts: -------------------------------------------------------------------------------- 1 | import fastify, { FastifyInstance } from 'fastify' 2 | import { Resource } from '../../../../../dist' 3 | 4 | interface Params { 5 | userId: string 6 | } 7 | 8 | export default (fastify: FastifyInstance) => { 9 | return { 10 | get: { 11 | handler: (request, reply) => { 12 | reply.send(`hello user ${request.params.userId}`) 13 | }, 14 | }, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fastify-autoroutes", 3 | "version": "3.0.2", 4 | "description": "Map directory structure to routes", 5 | "main": "dist/index.js", 6 | "prettier": "@giovannicardamone/prettier-config", 7 | "eslintConfig": { 8 | "extends": "@giovannicardamone/eslint-config" 9 | }, 10 | "scripts": { 11 | "lint": "eslint", 12 | "build": "tsc", 13 | "build:watch": "tsc --watch", 14 | "test": "jest", 15 | "test:watch": "jest --watchAll", 16 | "test:coverage": "jest --collectCoverage", 17 | "docs": "docsify generate ./docs", 18 | "docs:watch": "docsify serve ./docs", 19 | "prepare": "husky install", 20 | "prepublishOnly": "npm run build" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/GiovanniCardamone/fastify-autoroutes.git" 25 | }, 26 | "keywords": [ 27 | "fastify", 28 | "fastify-plugin", 29 | "fastify-autoroutes", 30 | "autoroute", 31 | "autorouting", 32 | "autoroutes" 33 | ], 34 | "author": "Giovanni Cardamone", 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/GiovanniCardamone/fastify-autoroutes/issues" 38 | }, 39 | "homepage": "https://github.com/GiovanniCardamone/fastify-autoroutes#readme", 40 | "devDependencies": { 41 | "@giovannicardamone/eslint-config": "^0.6.0", 42 | "@giovannicardamone/prettier-config": "^0.1.3", 43 | "@types/jest": "^29.2.0", 44 | "@types/mock-fs": "^4.13.1", 45 | "@types/node": "^14.14.6", 46 | "coveralls": "^3.1.0", 47 | "docsify": "^4.11.4", 48 | "eslint": "^7.6.0", 49 | "fastify": "^4.9.2", 50 | "fastify-plugin": "4.2.1", 51 | "husky": "^7.0.4", 52 | "jest": "^29.2.1", 53 | "light-my-request": "^5.6.1", 54 | "mock-fs": "^5.1.4", 55 | "ts-jest": "^29.0.3", 56 | "typescript": "^4.8.4" 57 | }, 58 | "dependencies": { 59 | "@fastify/error": "^3.0.0", 60 | "fastify-plugin": "^4.0.0", 61 | "glob-promise": "^4.2.2" 62 | }, 63 | "peerDependencies": { 64 | "fastify": "^4.0.0" 65 | }, 66 | "files": [ 67 | "dist" 68 | ], 69 | "contributors": [ 70 | { 71 | "name": "Giovanni Cardamone", 72 | "email": "g.cardamone2@gmail.com", 73 | "url": "https://giovannicardamone.github.io" 74 | }, 75 | { 76 | "name": "Gennaro Bosone", 77 | "email": "gennaro.bs@gmail.com", 78 | "url": "https://www.desidus.it" 79 | } 80 | ] 81 | } 82 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import createError from '@fastify/error' 2 | import type { FastifyInstance, RouteOptions } from 'fastify' 3 | import fastifyPlugin from 'fastify-plugin' 4 | import fs from 'fs' 5 | import glob from 'glob-promise' 6 | import path from 'path' 7 | import process from 'process' 8 | 9 | 10 | export const ERROR_LABEL = 'fastify-autoroutes' 11 | 12 | export type ValidMethods = 13 | | 'DELETE' 14 | | 'GET' 15 | | 'HEAD' 16 | | 'PATCH' 17 | | 'POST' 18 | | 'PUT' 19 | | 'OPTIONS' 20 | 21 | // const validMethods = [ 22 | // 'delete', 23 | // 'get', 24 | // 'head', 25 | // 'patch', 26 | // 'post', 27 | // 'put', 28 | // 'options', 29 | // ] 30 | 31 | export type AnyRoute = Omit 32 | 33 | export type DeleteRoute = AnyRoute 34 | export type GetRoute = Omit 35 | export type HeadRoute = AnyRoute 36 | export type PatchRoute = AnyRoute 37 | export type PostRoute = AnyRoute 38 | export type PutRoute = AnyRoute 39 | export type OptionsRoute = AnyRoute 40 | 41 | interface Security { 42 | [key: string]: string[] 43 | } 44 | 45 | interface StrictAnyRoute extends AnyRoute { 46 | schema: { 47 | summary?: string 48 | description?: string 49 | security?: Security[] 50 | tags?: string[] 51 | consumes?: string[] 52 | produces?: string[] 53 | body?: any 54 | querystring?: any 55 | params?: any 56 | headers?: any 57 | response?: { [key: number]: any } 58 | } 59 | } 60 | 61 | export type StrictDeleteRoute = StrictAnyRoute 62 | export type StrictGetRoute = Omit 63 | export type StrictHeadRoute = StrictAnyRoute 64 | export type StrictPatchRoute = StrictAnyRoute 65 | export type StrictPostRoute = StrictAnyRoute 66 | export type StrictPutRoute = StrictAnyRoute 67 | export type StrictOptionsRoute = StrictAnyRoute 68 | 69 | export interface Resource { 70 | delete?: DeleteRoute 71 | get?: GetRoute 72 | head?: HeadRoute 73 | patch?: PatchRoute 74 | post?: PostRoute 75 | put?: PutRoute 76 | options?: OptionsRoute 77 | } 78 | 79 | export interface StrictResource { 80 | delete?: StrictDeleteRoute 81 | get?: StrictGetRoute 82 | head?: StrictHeadRoute 83 | patch?: StrictPatchRoute 84 | post?: StrictPostRoute 85 | put?: StrictPutRoute 86 | options?: StrictOptionsRoute 87 | } 88 | 89 | interface FastifyAutoroutesOptions { 90 | dir?: string 91 | prefix?: string 92 | } 93 | 94 | export default fastifyPlugin( 95 | async ( 96 | fastify: FastifyInstance, 97 | options: FastifyAutoroutesOptions, 98 | next: CallableFunction 99 | ) => { 100 | const { dir, prefix: routePrefix } = { 101 | ...options, 102 | dir: options.dir || './routes', 103 | prefix: options.prefix || '', 104 | } 105 | 106 | let dirPath: string 107 | 108 | if (path.isAbsolute(dir)) { 109 | dirPath = dir 110 | } else if (path.isAbsolute(process.argv[1])) { 111 | dirPath = path.join(process.argv[1], dir) 112 | } else { 113 | dirPath = path.join(process.cwd(), process.argv[1], dir) 114 | } 115 | 116 | if (!fs.existsSync(dirPath)) { 117 | const CustomError = createError( 118 | '1', 119 | `${ERROR_LABEL} dir ${dirPath} must be a directory` 120 | ) 121 | return next(new CustomError()) 122 | } 123 | 124 | if (!fs.statSync(dirPath).isDirectory()) { 125 | const CustomError = createError( 126 | '2', 127 | `${ERROR_LABEL} dir ${dirPath} must be a directory` 128 | ) 129 | return next(new CustomError()) 130 | } 131 | 132 | // glob returns ../../, but windows returns ..\..\ 133 | dirPath = path.normalize(dirPath).replace(/\\/g, '/') 134 | 135 | const routes = await glob(`${dirPath}/**/[!._]!(*.test).{ts,js}`) 136 | const routesModules: Record = {} 137 | 138 | // console.log({ routes }) 139 | 140 | for (const route of routes) { 141 | let routeName = route 142 | .replace(dirPath, '') 143 | .replace('.js', '') 144 | .replace('.ts', '') 145 | .replace('index', '') 146 | .split('/') 147 | .map((part) => part.replace(/{(.+)}/g, ':$1')) 148 | .join('/') 149 | 150 | routeName = !routeName ? '/' : `${routePrefix}${routeName}` 151 | 152 | // console.log({ routeName }) 153 | 154 | routesModules[routeName] = loadModule(routeName, route)(fastify) 155 | } 156 | 157 | for (const [url, module] of Object.entries(routesModules)) { 158 | for (const [method, options] of Object.entries(module)) { 159 | fastify.route({ 160 | method: method.toUpperCase(), 161 | url: url, 162 | ...options, 163 | }) 164 | } 165 | } 166 | }, 167 | { 168 | fastify: '>=4.0.0', 169 | name: 'fastify-autoroutes', 170 | } 171 | ) 172 | 173 | function loadModule( 174 | name: string, 175 | path: string 176 | ): (instance: FastifyInstance) => StrictResource { 177 | // eslint-disable-next-line @typescript-eslint/no-var-requires 178 | const module = require(path) 179 | 180 | if (typeof module === 'function') { 181 | return module as (instance: any) => StrictResource 182 | } 183 | 184 | if ( 185 | typeof module === 'object' && 186 | 'default' in module && 187 | typeof module.default === 'function' 188 | ) { 189 | return module.default as (instance: any) => StrictResource 190 | } 191 | 192 | throw new Error( 193 | `${ERROR_LABEL}: invalid route module definition (${name}) ${path}. Must export a function` 194 | ) 195 | } 196 | -------------------------------------------------------------------------------- /tests/invalid-routes.test.ts: -------------------------------------------------------------------------------- 1 | import fastify from 'fastify' 2 | import path from 'path' 3 | import autoroutes, { ERROR_LABEL } from '../src' 4 | import { mock, restore } from './utils/mock' 5 | 6 | const exampleErrorModule = ` 7 | thisSyntaxIsInvalid :( 8 | ` 9 | 10 | const exampleInvalidModule = ` 11 | var whateverIdontCare = 3 12 | ` 13 | 14 | 15 | describe('Invalid Routes', () => { 16 | 17 | beforeEach(() => { 18 | // 19 | }) 20 | 21 | afterEach(() => { 22 | restore() 23 | }) 24 | 25 | test('invalid type routes directory', (done) => { 26 | const server = fastify() 27 | 28 | const dir = mock('dir', { 29 | dirAsFile: '', 30 | }) 31 | 32 | server.register(autoroutes, { 33 | dir: path.join(dir, 'dirAsFile'), 34 | }) 35 | 36 | server.inject( 37 | { 38 | method: 'GET', 39 | url: '/', 40 | }, 41 | (err, res) => { 42 | expect(err.message.startsWith(ERROR_LABEL)).toBeTruthy() 43 | done() 44 | } 45 | ) 46 | }) 47 | 48 | test('empty routes module', (done) => { 49 | const server = fastify() 50 | 51 | const dir = mock('dir', { 52 | 'index.js': '', // empty 53 | }) 54 | 55 | server.register(autoroutes, { 56 | dir, 57 | }) 58 | 59 | server.inject( 60 | { 61 | method: 'GET', 62 | url: '/', 63 | }, 64 | (err, res) => { 65 | expect(err.message.startsWith(ERROR_LABEL)).toBeTruthy() 66 | done() 67 | } 68 | ) 69 | }) 70 | 71 | test('modules with error', (done) => { 72 | const server = fastify() 73 | 74 | const dir = mock('dir', { 75 | 'index.js': exampleErrorModule, 76 | }) 77 | 78 | server.register(autoroutes, { 79 | dir, 80 | }) 81 | 82 | server.inject( 83 | { 84 | method: 'GET', 85 | url: '/', 86 | }, 87 | (err, res) => { 88 | expect(err).toBeInstanceOf(Error) 89 | done() 90 | } 91 | ) 92 | }) 93 | 94 | test('modules without valid routes', (done) => { 95 | const server = fastify() 96 | 97 | const dir = mock('dir', { 98 | 'index.js': exampleInvalidModule, 99 | }) 100 | 101 | server.register(autoroutes, { 102 | dir, 103 | }) 104 | 105 | server.inject( 106 | { 107 | method: 'GET', 108 | url: '/', 109 | }, 110 | (err, res) => { 111 | expect(err.message.startsWith(ERROR_LABEL)).toBeTruthy() 112 | done() 113 | } 114 | ) 115 | }) 116 | }) 117 | 118 | -------------------------------------------------------------------------------- /tests/options.test.ts: -------------------------------------------------------------------------------- 1 | import fastify from 'fastify' 2 | import autoroutes, { ERROR_LABEL } from '../src' 3 | import { restore } from './utils/mock' 4 | 5 | 6 | describe('Options', () => { 7 | 8 | beforeEach(() => { 9 | // 10 | }) 11 | 12 | afterEach(() => { 13 | restore() 14 | }) 15 | 16 | test('no dir parameters', (done) => { 17 | const server = fastify() 18 | 19 | server.register(autoroutes) 20 | 21 | server.inject( 22 | { 23 | method: 'GET', 24 | url: '/does-not-really-matter', 25 | }, 26 | (error) => { 27 | expect(error.message.startsWith(ERROR_LABEL)).toBeTruthy() 28 | done() 29 | } 30 | ) 31 | }) 32 | 33 | test('invalid dir parameters', (done) => { 34 | const server = fastify() 35 | 36 | // @ts-expect-error not valid 33 as dir 37 | server.register(autoroutes, { 38 | dir: 33, 39 | }) 40 | 41 | server.inject( 42 | { 43 | method: 'GET', 44 | url: '/does-not-really-matter', 45 | }, 46 | (error, res) => { 47 | console.log({ error, res }) 48 | expect(error).not.toBe(undefined) 49 | done() 50 | } 51 | ) 52 | }) 53 | 54 | test('dir does not exists', (done) => { 55 | const server = fastify() 56 | 57 | server.register(autoroutes, { 58 | dir: './this-directory-does-not-exists', 59 | }) 60 | 61 | server.inject( 62 | { 63 | method: 'GET', 64 | url: '/does-not-really-matter', 65 | }, 66 | (error) => { 67 | expect(error.message.startsWith(ERROR_LABEL)).toBeTruthy() 68 | done() 69 | } 70 | ) 71 | }) 72 | }) 73 | -------------------------------------------------------------------------------- /tests/routes.test.ts: -------------------------------------------------------------------------------- 1 | import fastify from 'fastify' 2 | import autoroutes from '../src' 3 | import { mock, restore } from './utils/mock' 4 | 5 | const exampleGetRoute = `module.exports = function (server) { 6 | return { 7 | get: { 8 | handler: function (request, reply) { 9 | return reply.send('get') 10 | } 11 | } 12 | } 13 | } 14 | ` 15 | 16 | const exampleGetRouteUrlParam = `module.exports = function (server) { 17 | return { 18 | get: { 19 | handler: function (request, reply) { 20 | return reply.send(request.params.PARAM) 21 | } 22 | } 23 | } 24 | } 25 | ` 26 | 27 | const exampleGetRouteJSONParam = `module.exports = function (server) { 28 | return { 29 | get: { 30 | handler: function (request, reply) { 31 | return reply.send(JSON.stringify(request.params)) 32 | } 33 | } 34 | } 35 | } 36 | ` 37 | 38 | const exampleGetRouteDefaultModule = ` 39 | 'use strict'; 40 | 41 | Object.defineProperty(exports, "__esModule", { 42 | value: true 43 | }); 44 | 45 | exports.default = function () { 46 | return { 47 | get: { 48 | handler: function (request, reply) { 49 | return reply.send('get') 50 | } 51 | } 52 | }; 53 | }; 54 | ` 55 | describe('Routes', () => { 56 | 57 | beforeEach(() => { 58 | // 59 | }) 60 | 61 | afterEach(() => { 62 | restore() 63 | }) 64 | 65 | test('simple index', (done) => { 66 | const server = fastify() 67 | 68 | const dir = mock('routes', { 69 | 'index.js': exampleGetRoute, 70 | }) 71 | 72 | server.register(autoroutes, { 73 | dir 74 | }) 75 | 76 | server.inject( 77 | { 78 | method: 'GET', 79 | url: '/', 80 | }, 81 | (err, res) => { 82 | expect(err).toBe(null) 83 | expect(res.payload).toBe('get') 84 | done() 85 | } 86 | ) 87 | }) 88 | 89 | test('nested routes', (done) => { 90 | const server = fastify() 91 | 92 | const dir = mock('routes', { 93 | users: { 94 | 'foo.js': exampleGetRoute, 95 | }, 96 | }) 97 | 98 | server.register(autoroutes, { 99 | dir 100 | }) 101 | 102 | server.inject( 103 | { 104 | method: 'GET', 105 | url: '/users/foo', 106 | }, 107 | (err, res) => { 108 | expect(err).toBe(null) 109 | expect(res.payload).toBe('get') 110 | done() 111 | } 112 | ) 113 | }) 114 | 115 | test('nested routes with trailing slashes', (done) => { 116 | const server = fastify() 117 | 118 | const dir = mock('routes', { 119 | users: { 120 | foo: { 121 | 'index.js': exampleGetRoute, 122 | }, 123 | }, 124 | }) 125 | 126 | server.register(autoroutes, { 127 | dir 128 | }) 129 | 130 | server.inject( 131 | { 132 | method: 'GET', 133 | url: '/users/foo/', 134 | }, 135 | (err, res) => { 136 | expect(err).toBe(null) 137 | expect(res.payload).toBe('get') 138 | done() 139 | } 140 | ) 141 | }) 142 | 143 | test('nested routes with url parameter', (done) => { 144 | const server = fastify() 145 | 146 | const dir = mock('routes', { 147 | 148 | users: { 149 | '{PARAM}.js': exampleGetRouteUrlParam, 150 | }, 151 | }) 152 | 153 | server.register(autoroutes, { 154 | dir 155 | }) 156 | 157 | const userId = 'foo' 158 | 159 | server.inject( 160 | { 161 | method: 'GET', 162 | url: `/users/${userId}`, 163 | }, 164 | (err, res) => { 165 | expect(err).toBe(null) 166 | expect(res.payload).toBe(userId) 167 | done() 168 | } 169 | ) 170 | }) 171 | 172 | test( 173 | 'url parameters with : (not on windows)', (done) => { 174 | if (process.platform === 'win32') { 175 | done() 176 | } else { 177 | const server = fastify() 178 | 179 | const dir = mock('routes', { 180 | users: { 181 | '{USERID}.js': exampleGetRouteJSONParam, 182 | '{USERID}': { 183 | 'index.js': exampleGetRouteJSONParam, 184 | }, 185 | }, 186 | }) 187 | 188 | server.register(autoroutes, { 189 | dir, 190 | }) 191 | 192 | const USERID = 'foo' 193 | 194 | server.inject( 195 | { 196 | method: 'GET', 197 | url: `/users/${USERID}`, 198 | }, 199 | (err, res) => { 200 | expect(err).toBe(null) 201 | expect(JSON.parse(res.payload).USERID).toBe(USERID) 202 | 203 | server.inject( 204 | { 205 | method: 'GET', 206 | url: `/users/${USERID}/`, 207 | }, 208 | (err, res) => { 209 | expect(err).toBe(null) 210 | expect(JSON.parse(res.payload).USERID).toBe(USERID) 211 | done() 212 | } 213 | ) 214 | } 215 | ) 216 | } 217 | } 218 | ) 219 | 220 | test( 221 | 'nested routes with url parameter with trailing slashes', (done) => { 222 | const server = fastify() 223 | 224 | const dir = mock('dir', { 225 | users: { 226 | '{PARAM}': { 227 | 'index.js': exampleGetRouteUrlParam, 228 | }, 229 | }, 230 | }) 231 | 232 | server.register(autoroutes, { 233 | dir, 234 | }) 235 | 236 | const userId = 'foo' 237 | 238 | server.inject( 239 | { 240 | method: 'GET', 241 | url: `/users/${userId}/`, 242 | }, 243 | (err, res) => { 244 | expect(err).toBe(null) 245 | expect(res.payload).toBe(userId) 246 | done() 247 | } 248 | ) 249 | } 250 | ) 251 | 252 | test('example es6 exports default module', (done) => { 253 | const server = fastify() 254 | 255 | const dir = mock('dir', { 256 | 'index.js': exampleGetRouteDefaultModule, 257 | }) 258 | 259 | server.register(autoroutes, { 260 | dir 261 | }) 262 | 263 | server.inject( 264 | { 265 | method: 'GET', 266 | url: '/', 267 | }, 268 | (err, res) => { 269 | expect(err).toBe(null) 270 | expect(res.payload).toBe('get') 271 | done() 272 | } 273 | ) 274 | }) 275 | 276 | test( 277 | 'skip routes with starting . charater', (done) => { 278 | const server = fastify() 279 | 280 | const dir = mock('dir', { 281 | 282 | '.hello.js': exampleGetRouteDefaultModule, 283 | }) 284 | 285 | server.register(autoroutes, { 286 | dir, 287 | }) 288 | 289 | server.inject( 290 | { 291 | method: 'GET', 292 | url: '/hello', 293 | }, 294 | (err, res) => { 295 | expect(res.statusCode).toBe(404) 296 | 297 | server.inject( 298 | { 299 | method: 'GET', 300 | url: '/.hello', 301 | }, 302 | (err, res) => { 303 | expect(res.statusCode).toBe(404) 304 | 305 | done() 306 | } 307 | ) 308 | } 309 | ) 310 | } 311 | ) 312 | 313 | test( 314 | 'skip routes with starting _ charater', (done) => { 315 | const server = fastify() 316 | 317 | const dir = mock('dir', { 318 | '_hello.js': exampleGetRouteDefaultModule, 319 | 320 | }) 321 | 322 | server.register(autoroutes, { 323 | dir 324 | }) 325 | 326 | server.inject( 327 | { 328 | method: 'GET', 329 | url: '/hello', 330 | }, 331 | (err, res) => { 332 | expect(res.statusCode).toBe(404) 333 | 334 | server.inject( 335 | { 336 | method: 'GET', 337 | url: '/_hello', 338 | }, 339 | (err, res) => { 340 | expect(res.statusCode).toBe(404) 341 | done() 342 | } 343 | ) 344 | } 345 | ) 346 | } 347 | ) 348 | 349 | test('skip routes ending with .test.js or .test.ts', (done) => { 350 | const server = fastify() 351 | 352 | const dir = mock('dir', { 353 | 'someJsRoute.test.js': exampleGetRouteDefaultModule, 354 | 'someTsRoute.test.ts': exampleGetRouteDefaultModule, 355 | }) 356 | 357 | server.register(autoroutes, { 358 | dir 359 | }) 360 | 361 | server.inject( 362 | { 363 | method: 'GET', 364 | url: '/someJsRoute', 365 | }, 366 | (err, res) => { 367 | expect(res.statusCode).toBe(404) 368 | 369 | server.inject( 370 | { 371 | method: 'GET', 372 | url: '/someTsRoute', 373 | }, 374 | (err, res) => { 375 | expect(res.statusCode).toBe(404) 376 | done() 377 | } 378 | ) 379 | } 380 | ) 381 | }) 382 | 383 | test('expect route /status to work', (done) => { 384 | const server = fastify() 385 | 386 | const dir = mock('routes', { 387 | a: {'status.js': exampleGetRoute, } 388 | }) 389 | 390 | server.register(autoroutes, { 391 | dir 392 | }) 393 | 394 | server.inject( 395 | { 396 | method: 'GET', 397 | url: '/a/status', 398 | }, 399 | (err, res) => { 400 | expect(err).toBe(null) 401 | expect(res.payload).toBe('get') 402 | done() 403 | } 404 | ) 405 | }) 406 | }) 407 | -------------------------------------------------------------------------------- /tests/utils/mock.ts: -------------------------------------------------------------------------------- 1 | import mockFs from 'mock-fs' 2 | import path from 'path' 3 | 4 | export function mock(dir: string, filesystem: { [key: string]: any }): string { 5 | 6 | 7 | mockFs({ 8 | [dir]: { 9 | ...filesystem, 10 | }, 11 | 'node_modules': mockFs.load(path.resolve(process.cwd(), 'node_modules')), 12 | }, { createCwd: true }) 13 | 14 | return path.join(process.cwd(), dir) 15 | } 16 | 17 | export function restore() { 18 | mockFs.restore() 19 | } 20 | -------------------------------------------------------------------------------- /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": "es6", /* 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": [ 10 | "es2021" 11 | ], /* Specify library files to be included in the compilation. */ 12 | // "allowJs": true, /* Allow javascript files to be compiled. */ 13 | // "checkJs": true, /* Report errors in .js files. */ 14 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 15 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 16 | "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 17 | "sourceMap": true, /* Generates corresponding '.map' file. */ 18 | // "outFile": "./", /* Concatenate and emit output to single file. */ 19 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 20 | "outDir": "./dist", /* Redirect output structure to the directory. */ 21 | // "composite": true, /* Enable project compilation */ 22 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 23 | // "removeComments": true, /* Do not emit comments to output. */ 24 | // "noEmit": true, /* Do not emit outputs. */ 25 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 26 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 27 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 28 | 29 | /* Strict Type-Checking Options */ 30 | "strict": true, /* Enable all strict type-checking options. */ 31 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 32 | // "strictNullChecks": true, /* Enable strict null checks. */ 33 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 34 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 35 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 36 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 37 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 38 | 39 | /* Additional Checks */ 40 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 41 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 42 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 43 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 44 | 45 | /* Module Resolution Options */ 46 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 47 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 48 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 49 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 50 | // "typeRoots": [], /* List of folders to include type definitions from. */ 51 | // "types": [], /* Type declaration files to be included in compilation. */ 52 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 53 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 54 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 55 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 56 | 57 | /* Source Map Options */ 58 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 61 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 62 | 63 | /* Experimental Options */ 64 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 65 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 66 | 67 | /* Advanced Options */ 68 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 69 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 70 | }, 71 | "include": ["src"], 72 | "exclude": ["examples", "tests", "dist"] 73 | } 74 | --------------------------------------------------------------------------------