├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── codeql-analysis.yml │ ├── diagram.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .husky ├── common.sh └── pre-commit ├── .npmignore ├── .prettierignore ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── banner.png ├── diagram.svg ├── example └── typescript │ ├── index.ts │ └── routes │ ├── _ignore_me │ └── game.ts │ ├── index.ts │ ├── multipleparameters │ ├── [id]-[name].ts │ └── multiple │ │ └── [id]-[name]-[game].ts │ ├── profile │ ├── [...id] │ │ └── settings.ts │ └── [game].ts │ ├── spark │ └── [...id].ts │ └── user │ ├── [id] │ └── index.ts │ └── index.ts ├── jest.config.js ├── package.json ├── pnpm-lock.yaml ├── release.config.js ├── renovate.json ├── src ├── index.ts ├── modules │ ├── fastifyFileRoutesPlugin.ts │ ├── index.ts │ └── types.ts └── utils │ ├── autoload.ts │ ├── handleParameters.ts │ ├── index.ts │ ├── isAcceptableFile.ts │ ├── loadModule.ts │ ├── scanFolders.ts │ └── transformPathToUrl.ts ├── test ├── handleParameters.spec.ts ├── ignored │ ├── .ignored.ts │ ├── _ignore_me │ │ └── game.ts │ ├── _ignored.ts │ ├── index.spec.ts │ └── index.test.ts ├── isAcceptableFile.spec.ts ├── loadModule.spec.ts ├── routes.test.ts └── transformPathToUrl.spec.ts ├── tsconfig.json ├── tsup.config.ts └── vite.config.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | .pnp.cjs 2 | .yarn 3 | coverage -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | require("@rushstack/eslint-patch/modern-module-resolution"); 2 | 3 | module.exports = { 4 | extends: [ 5 | "@spa5k/eslint-config/profile/node", 6 | "@spa5k/eslint-config/mixins/friendly-locals", 7 | "@spa5k/eslint-config/mixins/tsdoc", 8 | ], 9 | parserOptions: { tsconfigRootDir: __dirname }, 10 | rules: { 11 | "no-console": "error", 12 | "@typescript-eslint/consistent-type-definitions": "off", 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [main] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [main] 20 | schedule: 21 | - cron: "30 19 * * 3" 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: ["javascript"] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /.github/workflows/diagram.yml: -------------------------------------------------------------------------------- 1 | name: Create diagram 2 | on: 3 | workflow_dispatch: {} 4 | push: 5 | branches: 6 | - next 7 | jobs: 8 | get_data: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@master 13 | - name: Update diagram 14 | uses: githubocto/repo-visualizer@main 15 | with: 16 | excluded_paths: "ignore,.github,pnpm-lock.yaml,README.md,.vscode,.gitignore,.npmignore,assets,.yarn,.husky,yarn.lock" 17 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | # Run the workflow when a Pull Request is opened or when changes are pushed to master 4 | on: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-20.04 12 | strategy: 13 | matrix: 14 | node-version: [14] 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: pnpm/action-setup@v2.0.1 18 | with: 19 | version: 6.31.0 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v2 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | cache: "pnpm" 25 | - name: Install dependencies 26 | run: pnpm install 27 | - name: Build 28 | run: pnpm build 29 | - name: Release 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 33 | run: pnpm semantic-release 34 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | pull_request: 4 | branches: 5 | - next 6 | push: 7 | branches: 8 | - next 9 | jobs: 10 | build: 11 | runs-on: ubuntu-20.04 12 | strategy: 13 | matrix: 14 | node-version: [14, 16, 17] 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: pnpm/action-setup@v2.0.1 18 | with: 19 | version: 6.24.2 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v2 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | cache: "pnpm" 25 | - name: Install dependencies 26 | run: pnpm install 27 | - name: Test 28 | run: pnpm ci:test 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.yarn/* 2 | !/.yarn/patches 3 | !/.yarn/plugins 4 | !/.yarn/releases 5 | !/.yarn/sdks 6 | 7 | # Swap the comments on the following lines if you don't wish to use zero-installs 8 | # Documentation here: https://yarnpkg.com/features/zero-installs 9 | !/.yarn/cache 10 | #/.pnp.* 11 | # Logs 12 | logs 13 | *.log 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | lerna-debug.log* 18 | .pnpm-debug.log* 19 | 20 | # Diagnostic reports (https://nodejs.org/api/report.html) 21 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 22 | 23 | # Runtime data 24 | pids 25 | *.pid 26 | *.seed 27 | *.pid.lock 28 | 29 | # Directory for instrumented libs generated by jscoverage/JSCover 30 | lib-cov 31 | 32 | # Coverage directory used by tools like istanbul 33 | coverage 34 | *.lcov 35 | 36 | # nyc test coverage 37 | .nyc_output 38 | 39 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 40 | .grunt 41 | 42 | # Bower dependency directory (https://bower.io/) 43 | bower_components 44 | 45 | # node-waf configuration 46 | .lock-wscript 47 | 48 | # Compiled binary addons (https://nodejs.org/api/addons.html) 49 | build/Release 50 | 51 | # Dependency directories 52 | node_modules/ 53 | jspm_packages/ 54 | 55 | # Snowpack dependency directory (https://snowpack.dev/) 56 | web_modules/ 57 | 58 | # TypeScript cache 59 | *.tsbuildinfo 60 | 61 | # Optional npm cache directory 62 | .npm 63 | 64 | # Optional eslint cache 65 | .eslintcache 66 | 67 | # Microbundle cache 68 | .rpt2_cache/ 69 | .rts2_cache_cjs/ 70 | .rts2_cache_es/ 71 | .rts2_cache_umd/ 72 | 73 | # Optional REPL history 74 | .node_repl_history 75 | 76 | # Output of 'npm pack' 77 | *.tgz 78 | 79 | # Yarn Integrity file 80 | .yarn-integrity 81 | 82 | # dotenv environment variables file 83 | .env 84 | .env.test 85 | .env.production 86 | 87 | # parcel-bundler cache (https://parceljs.org/) 88 | .cache 89 | .parcel-cache 90 | 91 | # Next.js build output 92 | .next 93 | out 94 | 95 | # Nuxt.js build / generate output 96 | .nuxt 97 | dist 98 | 99 | # Gatsby files 100 | .cache/ 101 | # Comment in the public line in if your project uses Gatsby and not Next.js 102 | # https://nextjs.org/blog/next-9-1#public-directory-support 103 | # public 104 | 105 | # vuepress build output 106 | .vuepress/dist 107 | 108 | # Serverless directories 109 | .serverless/ 110 | 111 | # FuseBox cache 112 | .fusebox/ 113 | 114 | # DynamoDB Local files 115 | .dynamodb/ 116 | 117 | # TernJS port file 118 | .tern-port 119 | 120 | # Stores VSCode versions used for testing VSCode extensions 121 | .vscode-test 122 | 123 | # yarn v2 124 | # .yarn/cache 125 | # .yarn/unplugged 126 | .yarn/build-state.yml 127 | .yarn/install-state.gz 128 | # .pnp.* 129 | 130 | coverage 131 | .dc-cache -------------------------------------------------------------------------------- /.husky/common.sh: -------------------------------------------------------------------------------- 1 | command_exists () { 2 | command -v "$1" >/dev/null 2>&1 3 | } 4 | 5 | # Workaround for Windows 10, Git Bash and Yarn 6 | if command_exists winpty && test -t 1; then 7 | exec < /dev/tty 8 | fi 9 | 10 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | . "$(dirname "$0")/common.sh" 4 | # evaluate fnm 5 | eval $(fnm env | sed 1d) 6 | export PATH=$(cygpath $FNM_MULTISHELL_PATH):$PATH 7 | 8 | pnpm format 9 | pnpm lint -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | assets 3 | .vscode 4 | .github 5 | .eslintrc 6 | test 7 | renovate.json 8 | .prettierignore 9 | .yarn 10 | 11 | CHANGELOG.md 12 | example -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | pnpm-lock.yaml 3 | .yarn 4 | .pnp.cjs 5 | coverage -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "arcanis.vscode-zipfs", 4 | "dbaeumer.vscode-eslint", 5 | "esbenp.prettier-vscode" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "conventionalCommits.scopes": [ 3 | "main", 4 | "deps", 5 | "actions", 6 | "test", 7 | "husky", 8 | "docs", 9 | "autoload", 10 | "parameters", 11 | "typescript" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.1.2](https://github.com/spa5k/fastify-file-routes/compare/v1.1.1...v1.1.2) (2022-03-10) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * **parameters:** 🐛 use replace for comptability ([94df65b](https://github.com/spa5k/fastify-file-routes/commit/94df65b47fd4e12eedced71940f22fc5de3838d9)) 7 | 8 | ## [1.1.1](https://github.com/spa5k/fastify-file-routes/compare/v1.1.0...v1.1.1) (2021-12-21) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * **typescript:** 🔥 removing type json schema until a better way to infer type is thought off ([c274cc0](https://github.com/spa5k/fastify-file-routes/commit/c274cc0f444af0f3b7912013a4d9446429f363a6)) 14 | 15 | # [1.1.0](https://github.com/spa5k/fastify-file-routes/compare/v1.0.3...v1.1.0) (2021-12-21) 16 | 17 | ### Bug Fixes 18 | 19 | - **actions:** 💚 removing tests from release CI ([6b7d850](https://github.com/spa5k/fastify-file-routes/commit/6b7d850cbd648fe21c84c494743c92121abbf593)) 20 | - **parameters:** 🐛 fixing handleparameters ([b9d7b1d](https://github.com/spa5k/fastify-file-routes/commit/b9d7b1d245684edb7238d8355a4603bf919c4142)) 21 | 22 | ### Features 23 | 24 | - **parameters:** ✨ added multiple parameters support for files and folders ([8854728](https://github.com/spa5k/fastify-file-routes/commit/885472815ef480166e95efb978e7f9e1601e4ffe)) 25 | 26 | ## [1.0.3](https://github.com/spa5k/fastify-file-routes/compare/v1.0.2...v1.0.3) (2021-12-21) 27 | 28 | ### Bug Fixes 29 | 30 | - **autoload:** 🐛 adding a try catch to make sure that wrong route does not result in a crash ([0e42d3f](https://github.com/spa5k/fastify-file-routes/commit/0e42d3f3f61a5d9d5e44be2a7b1a88a578ee1973)) 31 | - **test:** 🐛 removing empty route to let test run successfully ([2db5f71](https://github.com/spa5k/fastify-file-routes/commit/2db5f71a420f77fa405cfd6f5540357b92c68a4b)) 32 | 33 | ## [1.0.2](https://github.com/spa5k/fastify-file-routes/compare/v1.0.1...v1.0.2) (2021-12-21) 34 | 35 | ### Bug Fixes 36 | 37 | - **deps:** 🐛 moving fastify-plugin to dependency to fix missing dep error ([b9b956f](https://github.com/spa5k/fastify-file-routes/commit/b9b956f2476b9132fc8f2678ea01f74279a74a4b)) 38 | 39 | ## [1.0.1](https://github.com/spa5k/fastify-file-routes/compare/v1.0.0...v1.0.1) (2021-12-21) 40 | 41 | ### Bug Fixes 42 | 43 | - **actions:** 💚 fixing build related ci on release action ([f118ac2](https://github.com/spa5k/fastify-file-routes/commit/f118ac26170a534f7ff099dac89572fab48c70ec)) 44 | 45 | # 1.0.0 (2021-12-21) 46 | 47 | ### Features 48 | 49 | - **main:** ✨ added initial implmentation ([#2](https://github.com/spa5k/fastify-file-routes/issues/2)) ([3222faf](https://github.com/spa5k/fastify-file-routes/commit/3222fafce2dd5217bfc67b90e60f0a80ce729780)) 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 spa5k 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 File Routes 2 | 3 |
4 | 5 | ![Banner](./banner.png) 6 | 7 | [![NPM downloads](https://img.shields.io/npm/dm/fastify-file-routes.svg?style=for-the-badge)](https://www.npmjs.com/package/fastify-file-routes) 8 | [![npm](https://img.shields.io/npm/v/fastify-file-routes?logo=npm&style=for-the-badge)](https://www.npmjs.com/package/fastify-file-routes) 9 | ![node-current](https://img.shields.io/badge/Node-%3E=14-success?style=for-the-badge&logo=node) 10 | 11 | A Fastify plugin that provides a file system routes, based on the way Next.JS file system routing works, including all possible features. 12 | 13 |
14 | 15 | ## :sparkles: Features 16 | 17 | 1. File System Routing. 18 | 2. Index Routes. 19 | 3. Nested Routes. 20 | 4. Dynamic Route Segments. 21 | 5. Catch All (Wildcard \*) Routes. 22 | 6. Multiple parameters. eg /users/:id-:name 23 | 24 | ## :rocket: Installation 25 | 26 | ```sh 27 | npm install fastify-file-routes 28 | ``` 29 | 30 | ```yarn 31 | yarn add fastify-file-routes 32 | ``` 33 | 34 | ## :blue_book: Usage/Examples 35 | 36 | ### 1. Register the Plugin. 37 | 38 | ```typescript 39 | import { fileRoutes } from "fastify-file-routes"; 40 | 41 | const app: FastifyInstance = fastify({ logger: true }); 42 | 43 | await app.register(fileRoutes, { 44 | routesDir: "./routes", 45 | prefix, // -> optional 46 | }); 47 | 48 | await app.listen(3000); 49 | ``` 50 | 51 | ### 2. Create the routes directory. 52 | 53 | ```sh 54 | mkdir routes 55 | ``` 56 | 57 | ### 3. Create your first route in the routes directory 58 | 59 | ```typescript 60 | //file: `routes/some/route.ts` 61 | //url: `http://localhost/some/route` 62 | 63 | import type { Route } from "fastify-file-routes"; 64 | 65 | export const routes: Route = { 66 | get: { 67 | handler: async (request, reply) => { 68 | await reply.send({ 69 | some: "route", 70 | }); 71 | }, 72 | }, 73 | }; 74 | ``` 75 | 76 | ### 4. Access the Parameters. 77 | 78 | ```typescript 79 | //file: `routes/users/[userId]/settings.js` 80 | //mapped to: `http://localhost/users/:userId/settings` 81 | 82 | export const routes: Route = { 83 | get: { 84 | handler: async (request, reply) => { 85 | const { params } = request; 86 | await reply.send(`photos of user ${params.userId}`); 87 | }, 88 | }, 89 | }; 90 | ``` 91 | 92 | ### 5. Wildcard (\*) routes. 93 | 94 | ```typescript 95 | //file: `routes/profile/[...id].ts ` 96 | //mapped to: `http://localhost/profile/*` 97 | 98 | export const routes: Route = { 99 | get: { 100 | handler: async (request, reply) => { 101 | const { params } = request; 102 | await reply.send(`wildcard route`); 103 | }, 104 | }, 105 | }; 106 | ``` 107 | 108 | ### 6. Post Request.. 109 | 110 | ```typescript 111 | export const routes: Route = { 112 | post: { 113 | handler: async (_request, reply) => { 114 | await reply.send({ 115 | post: "post user", 116 | }); 117 | }, 118 | }, 119 | }; 120 | ``` 121 | 122 | ### 7. Prefix Route 123 | 124 | ```typescript 125 | //file: `routes/some/route.ts` 126 | //url: `http://localhost/api/some/route` 127 | 128 | await app.register(fileRoutes, { 129 | routesDir: "./routes", 130 | prefix: "/api", 131 | }); 132 | 133 | export const routes: Route = { 134 | post: { 135 | handler: async (_request, reply) => { 136 | await reply.send({ 137 | post: "post user", 138 | }); 139 | }, 140 | }, 141 | }; 142 | ``` 143 | 144 | ### 8. Multiple Parameters 145 | 146 | ```typescript 147 | //file: `routes/some/[param1]-[param2].ts` 148 | //url: `http://localhost/some/:param1-:param2` 149 | 150 | await app.register(fileRoutes, { 151 | routesDir: "./routes", 152 | }); 153 | 154 | export const routes: Route = { 155 | post: { 156 | handler: async (_request, reply) => { 157 | await reply.send({ 158 | post: "multiple params", 159 | }); 160 | }, 161 | }, 162 | }; 163 | ``` 164 | 165 | ## :information_source: Info 166 | 167 | 1. Check the examples folder in /examples to see how to use the plugin. 168 | 2. route.prefixTrailingSlash has been set to 'both'. 169 | 170 | ## :arrow_forward: Route module definition 171 | 172 | Method specification for attributes is available here: [Method specification](https://www.fastify.io/docs/latest/Routes/#full-declaration) 173 | 174 | > :information_source: attributes `url` and `method` are dynamically provided 175 | 176 | Allowed attributes mapped to Http methods in module: 177 | 178 | - delete 179 | - get 180 | - head 181 | - patch 182 | - post 183 | - put 184 | - options 185 | 186 | ## :arrow_forward: Skipping files 187 | 188 | to skip file in routes directory, prepend the `.` or `_` character to filename 189 | 190 | examples: 191 | 192 | ```text 193 | routes 194 | ├── .ignored-directory 195 | ├── _ignored-directory 196 | ├── .ignored-js-file.js 197 | ├── _ignored-js-file.js 198 | ├── .ignored-ts-file.ts 199 | ├── _ignored-ts-file.ts 200 | ├── ignored-js-test.test.js 201 | └── ignored-ts-test.test.ts 202 | ``` 203 | 204 | > :warning: also any `*.test.js` and `*.test.ts` are skipped! 205 | 206 | 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 207 | 208 | ## TODO 209 | 210 | 1. Adding support for optional wildcard routes - [[...id]]. 211 | 2. More tests. 212 | 3. Better typescript stuff for validation and inferences in routes. 213 | 214 | ## Visualization of this Repo. 215 | 216 | ![Visualization of this repo](./diagram.svg) 217 | 218 | ## License 219 | 220 | [MIT](https://choosealicense.com/licenses/mit/) 221 | 222 | ## Related/Acknowledgements 223 | 224 | [Fastify - AutoRoutes](https://github.com/GiovanniCardamone/fastify-autoroutes) - Lots of code has been used from this project. 225 | -------------------------------------------------------------------------------- /banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spa5k/fastify-file-routes/cf0d8cc7fa694dc1cbd0c4b8c57e8664f66f2b8a/banner.png -------------------------------------------------------------------------------- /diagram.svg: -------------------------------------------------------------------------------- 1 | testtestsrcsrcexample/typescriptexample/typescriptignoredignoredutilsutilsmodulesmodulesroutesroutesuseruserprofileprofilemultipleparametersmultipleparametersroutes.test.tsroutes.test.tsroutes.test.tsisAcceptableF...isAcceptableF...isAcceptableF...handlePara...handlePara...handlePara...transformP...transformP...transformP...package.jsonpackage.jsonpackage.jsonCHANGELOG.mdCHANGELOG.mdCHANGELOG.mdtsconfig.jsontsconfig.jsontsconfig.jsonLICENSELICENSELICENSErelease.c...release.c...release.c..._ignore_me/game.ts_ignore_me/game.ts_ignore_me/game.tsautoload.tsautoload.tsautoload.tshandlePara...handlePara...handlePara...scanFolde...scanFolde...scanFolde...transform...transform...transform...fastifyFileRo...fastifyFileRo...fastifyFileRo...types.tstypes.tstypes.tsspark/[...id].tsspark/[...id].tsspark/[...id].ts_ignore_me/game.ts_ignore_me/game.ts_ignore_me/game.ts[id]/index.ts[id]/index.ts[id]/index.ts[...id]/settings.ts[...id]/settings.ts[...id]/settings.tsmultiple/[id]-[na...multiple/[id]-[na...multiple/[id]-[na....js.json.md.svg.tseach dot sized by file size -------------------------------------------------------------------------------- /example/typescript/index.ts: -------------------------------------------------------------------------------- 1 | import fastify, { FastifyInstance } from "fastify"; 2 | import { fileRoutes } from "../../src"; 3 | 4 | const main = async (): Promise => { 5 | const app: FastifyInstance = fastify({ logger: true }); 6 | 7 | await app.register(fileRoutes, { 8 | routesDir: "./routes", 9 | }); 10 | 11 | app.printRoutes(); 12 | await app.listen(3000); 13 | }; 14 | main().catch((error) => { 15 | // eslint-disable-next-line no-console 16 | console.log(error); 17 | }); 18 | -------------------------------------------------------------------------------- /example/typescript/routes/_ignore_me/game.ts: -------------------------------------------------------------------------------- 1 | import type { Route } from "../../../../src"; 2 | 3 | export const routes: Route = { 4 | get: { 5 | handler: async (_request, reply) => { 6 | await reply.send("ignore me"); 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /example/typescript/routes/index.ts: -------------------------------------------------------------------------------- 1 | import type { Route } from "../../../src"; 2 | 3 | export const routes: Route = { 4 | get: { 5 | handler: async (_request, reply) => { 6 | await reply.send({ 7 | get: "index", 8 | }); 9 | }, 10 | }, 11 | head: { 12 | handler: async (_request, reply) => { 13 | await reply.send({ 14 | get: "index", 15 | }); 16 | }, 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /example/typescript/routes/multipleparameters/[id]-[name].ts: -------------------------------------------------------------------------------- 1 | import { Route } from "../../../../src"; 2 | 3 | export const routes: Route = { 4 | get: { 5 | handler: async (request, reply) => { 6 | await reply.send({ 7 | get: "index", 8 | }); 9 | }, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /example/typescript/routes/multipleparameters/multiple/[id]-[name]-[game].ts: -------------------------------------------------------------------------------- 1 | import { Route } from "../../../../../src"; 2 | 3 | export const routes: Route = { 4 | get: { 5 | handler: async (request, reply) => { 6 | await reply.send({ 7 | get: "index", 8 | }); 9 | }, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /example/typescript/routes/profile/[...id]/settings.ts: -------------------------------------------------------------------------------- 1 | import type { Route } from "../../../../../src"; 2 | 3 | export const routes: Route = { 4 | get: { 5 | handler: async (request, reply) => { 6 | const { params } = request; 7 | await reply.send(params); 8 | }, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /example/typescript/routes/profile/[game].ts: -------------------------------------------------------------------------------- 1 | import { Route } from "../../../../src"; 2 | 3 | export const routes: Route = { 4 | get: { 5 | handler: async (request, reply) => { 6 | const { params } = request; 7 | await reply.send(params); 8 | }, 9 | schema: { 10 | params: { 11 | id: { 12 | type: "string", 13 | }, 14 | }, 15 | }, 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /example/typescript/routes/spark/[...id].ts: -------------------------------------------------------------------------------- 1 | import { Route } from "../../../../src"; 2 | 3 | export const routes: Route = { 4 | get: { 5 | handler: async (_request, reply) => { 6 | await reply.send({ get: "get user" }); 7 | }, 8 | }, 9 | post: { 10 | handler: async (_request, reply) => { 11 | await reply.send({ 12 | post: "post user", 13 | }); 14 | }, 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /example/typescript/routes/user/[id]/index.ts: -------------------------------------------------------------------------------- 1 | import type { Route } from "../../../../../src"; 2 | 3 | export const routes: Route = { 4 | get: { 5 | handler: async (request, reply) => { 6 | const { params } = request; 7 | await reply.send(params); 8 | }, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /example/typescript/routes/user/index.ts: -------------------------------------------------------------------------------- 1 | import type { Route } from "../../../../src"; 2 | 3 | export const routes: Route = { 4 | get: { 5 | handler: async (_request, reply) => { 6 | await reply.send({ get: "get user" }); 7 | }, 8 | }, 9 | post: { 10 | handler: async (_request, reply) => { 11 | await reply.send({ 12 | post: "post user", 13 | }); 14 | }, 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | transform: { 4 | "\\.[jt]sx?$": [ 5 | "esbuild-jest", 6 | { 7 | loaders: { 8 | ".spec.js": "jsx", 9 | ".js": "jsx", 10 | }, 11 | }, 12 | ], 13 | }, 14 | testPathIgnorePatterns: ["/node_modules/", "/dist/", "/types/", "/ignored/"], 15 | moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], 16 | testEnvironment: "node", 17 | }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fastify-file-routes", 3 | "description": "A Fastify plugin that provides a file system routes, based on the way Next.JS file system routing works, including all possible features.", 4 | "author": "spa5k ", 5 | "bugs": { 6 | "url": "https://github.com/spa5k/fastify-file-routes/issues" 7 | }, 8 | "commitlint": { 9 | "extends": [ 10 | "@commitlint/config-conventional" 11 | ] 12 | }, 13 | "dependencies": { 14 | "fastify-plugin": "3.0.1", 15 | "picocolors": "1.0.0" 16 | }, 17 | "devDependencies": { 18 | "@commitlint/cli": "16.2.1", 19 | "@commitlint/config-conventional": "16.2.1", 20 | "@semantic-release/changelog": "6.0.1", 21 | "@semantic-release/commit-analyzer": "9.0.2", 22 | "@semantic-release/git": "10.0.1", 23 | "@semantic-release/github": "8.0.2", 24 | "@semantic-release/npm": "9.0.1", 25 | "@semantic-release/release-notes-generator": "10.0.3", 26 | "@spa5k/eslint-config": "0.0.2", 27 | "@types/jest": "27.4.1", 28 | "@types/node": "17.0.21", 29 | "@typescript-eslint/eslint-plugin": "5.14.0", 30 | "@typescript-eslint/parser": "5.14.0", 31 | "c8": "7.11.0", 32 | "commitizen": "4.2.4", 33 | "dotenv": "16.0.0", 34 | "esbuild": "0.14.25", 35 | "eslint": "8.10.0", 36 | "eslint-config-galex": "3.6.5", 37 | "eslint-import-resolver-node": "0.3.6", 38 | "eslint-plugin-import": "2.25.4", 39 | "eslint-plugin-inclusive-language": "2.2.0", 40 | "eslint-plugin-jest-formatting": "3.1.0", 41 | "eslint-plugin-promise": "6.0.0", 42 | "eslint-plugin-sonarjs": "0.12.0", 43 | "eslint-plugin-unicorn": "41.0.0", 44 | "fastify": "3.27.4", 45 | "fastify-plugin": "3.0.1", 46 | "husky": "7.0.4", 47 | "node-dev": "7.2.0", 48 | "node-fetch": "3.2.2", 49 | "npm-run-all": "4.1.5", 50 | "prettier": "2.5.1", 51 | "semantic-release": "19.0.2", 52 | "start-server-and-test": "1.14.0", 53 | "ts-node": "10.7.0", 54 | "tslib": "2.3.1", 55 | "tsup": "5.12.0", 56 | "typescript": "4.6.2", 57 | "vite": "2.8.6", 58 | "vitest": "0.6.0" 59 | }, 60 | "files": [ 61 | "dist" 62 | ], 63 | "homepage": "https://github.com/spa5k/fastify-file-routes#readme", 64 | "husky": { 65 | "hooks": { 66 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 67 | "pre-commit": "pnpm format && pnpm lint" 68 | } 69 | }, 70 | "keywords": [ 71 | "cjs", 72 | "esm", 73 | "fastify", 74 | "fastify-plugin", 75 | "file-routes", 76 | "next", 77 | "nextjs", 78 | "routes", 79 | "typescript" 80 | ], 81 | "license": "MIT", 82 | "main": "dist/index.js", 83 | "module": "dist/index.mjs", 84 | "peerDependencies": { 85 | "fastify": ">=3.0.0" 86 | }, 87 | "repository": { 88 | "type": "git", 89 | "url": "git+https://github.com/spa5k/fastify-file-routes.git" 90 | }, 91 | "scripts": { 92 | "build": "tsup-node", 93 | "build:dev": "tsup-node", 94 | "ci:test": "start-server-and-test 'pnpm dev-ts' http://localhost:3000 'pnpm test'", 95 | "coverage": "c8 vitest", 96 | "dev-ts": "node-dev example/typescript/index.ts --notify=false", 97 | "format": "prettier --write .", 98 | "lint": "eslint src --ext .ts", 99 | "semantic-release": "semantic-release", 100 | "start": "pnpm dev-ts", 101 | "test": "vitest", 102 | "test:suite": "vitest" 103 | }, 104 | "typings": "dist/index.d.ts", 105 | "version": "1.1.2", 106 | "packageManager": "pnpm@6.31.0" 107 | } 108 | -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | branches: [ 3 | "+([0-9])?(.{+([0-9]),x}).x", 4 | "main", 5 | "master", 6 | "next", 7 | "next-major", 8 | { 9 | name: "beta", 10 | prerelease: true, 11 | }, 12 | { 13 | name: "alpha", 14 | prerelease: true, 15 | }, 16 | ], 17 | plugins: [ 18 | "@semantic-release/commit-analyzer", 19 | "@semantic-release/release-notes-generator", 20 | [ 21 | "@semantic-release/changelog", 22 | { 23 | changelogFile: "CHANGELOG.md", 24 | }, 25 | ], 26 | "@semantic-release/npm", 27 | "@semantic-release/github", 28 | [ 29 | "@semantic-release/git", 30 | { 31 | assets: ["CHANGELOG.md", "package.json", "pnpm-lock.yaml", "yarn.lock"], 32 | message: 33 | "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}", 34 | }, 35 | ], 36 | ], 37 | preset: "angular", 38 | }; 39 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "schedule": ["every month"], 4 | "automerge": true, 5 | "automergeType": "pr", 6 | "baseBranches": ["next"] 7 | } 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { fileRoutes } from "./modules"; 2 | export * from "./modules/types"; 3 | -------------------------------------------------------------------------------- /src/modules/fastifyFileRoutesPlugin.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ 3 | /* eslint-disable @typescript-eslint/no-explicit-any */ 4 | import type { FastifyInstance } from "fastify"; 5 | import fs from "fs"; 6 | import path from "path"; 7 | import pc from "picocolors"; 8 | import process from "process"; 9 | import { scanFolders } from "../utils"; 10 | import type { FileRoutesOptions } from "./types"; 11 | import { errorLabel } from "./types"; 12 | 13 | export const fastifyFileRoutesPlugin = async ( 14 | fastify: FastifyInstance, 15 | options: FileRoutesOptions, 16 | next: any 17 | ): Promise => { 18 | if (!options.routesDir) { 19 | const message: string = `${errorLabel} dir must be specified`; 20 | console.error(`${pc.red(message)}`); 21 | 22 | return next(new Error(message)); 23 | } 24 | 25 | if (typeof options.routesDir !== "string") { 26 | const message: string = `${errorLabel} dir must be the path of file system routes directory`; 27 | console.error(`${pc.red(message)}`); 28 | 29 | return next(new Error(message)); 30 | } 31 | 32 | let dirPath: string; 33 | 34 | if (path.isAbsolute(options.routesDir)) { 35 | dirPath = options.routesDir; 36 | } else if (path.isAbsolute(process.argv[1])) { 37 | dirPath = path.join(process.argv[1], "..", options.routesDir); 38 | } else { 39 | dirPath = path.join( 40 | process.cwd(), 41 | process.argv[1], 42 | "..", 43 | options.routesDir 44 | ); 45 | } 46 | 47 | if (!fs.existsSync(dirPath)) { 48 | const message: string = `${errorLabel} dir ${dirPath} does not exists`; 49 | console.error(`${pc.red(message)}`); 50 | 51 | return next(new Error(message)); 52 | } 53 | 54 | if (!fs.statSync(dirPath).isDirectory()) { 55 | const message: string = `${errorLabel} dir ${dirPath} must be a directory`; 56 | console.error(`${pc.red(message)}`); 57 | 58 | return next(new Error(message)); 59 | } 60 | 61 | try { 62 | await scanFolders( 63 | fastify, 64 | dirPath, 65 | "", 66 | options.prefix ? options.prefix : "" 67 | ); 68 | } catch (error: any) { 69 | console.error(`${pc.red(error.message)}`); 70 | return next(error); 71 | } finally { 72 | next(); 73 | } 74 | }; 75 | -------------------------------------------------------------------------------- /src/modules/index.ts: -------------------------------------------------------------------------------- 1 | import { FastifyPluginCallback } from "fastify"; 2 | import fastifyPlugin from "fastify-plugin"; 3 | import { Server } from "http"; 4 | import { fastifyFileRoutesPlugin } from "./fastifyFileRoutesPlugin"; 5 | import type { FileRoutesOptions } from "./types"; 6 | 7 | export const fileRoutes: FastifyPluginCallback = 8 | fastifyPlugin(fastifyFileRoutesPlugin, { 9 | fastify: ">=3.0.0", 10 | name: "fastify-file-routes", 11 | }); 12 | -------------------------------------------------------------------------------- /src/modules/types.ts: -------------------------------------------------------------------------------- 1 | import type { RouteOptions } from "fastify"; 2 | 3 | export const errorLabel: "[ERROR] fastify-file-routes:" = 4 | "[ERROR] fastify-file-routes:"; 5 | 6 | export type ValidMethods = 7 | | "DELETE" 8 | | "GET" 9 | | "HEAD" 10 | | "PATCH" 11 | | "POST" 12 | | "PUT" 13 | | "OPTIONS"; 14 | 15 | export const validMethods: Set = new Set([ 16 | "delete", 17 | "get", 18 | "head", 19 | "patch", 20 | "post", 21 | "put", 22 | "options", 23 | ]); 24 | 25 | export type AnyRoute = Omit; 26 | 27 | export type DeleteRoute = AnyRoute; 28 | export type GetRoute = Omit; 29 | export type HeadRoute = AnyRoute; 30 | export type PatchRoute = AnyRoute; 31 | export type PostRoute = AnyRoute; 32 | export type PutRoute = AnyRoute; 33 | export type OptionsRoute = AnyRoute; 34 | 35 | export type Security = { 36 | [key: string]: string[]; 37 | }; 38 | 39 | export type Route = { 40 | delete?: DeleteRoute; 41 | get?: GetRoute; 42 | head?: HeadRoute; 43 | patch?: PatchRoute; 44 | post?: PostRoute; 45 | put?: PutRoute; 46 | options?: OptionsRoute; 47 | }; 48 | 49 | export type FileRoutesOptions = { 50 | routesDir: string; 51 | prefix?: string; 52 | }; 53 | -------------------------------------------------------------------------------- /src/utils/autoload.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import type { FastifyInstance, RouteOptions } from "fastify"; 3 | import pc from "picocolors"; 4 | import { loadModule, ModuleType } from "."; 5 | import type { ValidMethods } from "../modules/types"; 6 | import { validMethods } from "../modules/types"; 7 | 8 | export async function autoload( 9 | fastify: FastifyInstance, 10 | fullPath: string, 11 | url: string, 12 | prefix: string 13 | ): Promise { 14 | const module: ModuleType = await loadModule(fullPath); 15 | 16 | try { 17 | for (const [method, route] of Object.entries(module)) { 18 | if (validMethods.has(method)) { 19 | route.url = prefix ? `/${prefix}${url}` : url; 20 | route.method = method.toUpperCase() as ValidMethods; 21 | route.prefixTrailingSlash = "both"; 22 | 23 | fastify.route(route); 24 | switch (route.method) { 25 | case "POST": { 26 | console.info( 27 | `${pc.yellow(method.toUpperCase())} ${route.url}` 28 | ); 29 | 30 | break; 31 | } 32 | case "HEAD": { 33 | console.info( 34 | `${pc.bgMagenta(method.toUpperCase())} ${route.url}` 35 | ); 36 | 37 | break; 38 | } 39 | case "OPTIONS": { 40 | console.info(`${pc.bgCyan(method.toUpperCase())} ${route.url}`); 41 | 42 | break; 43 | } 44 | default: { 45 | console.info( 46 | `${pc.green(method.toUpperCase())} ${route.url}` 47 | ); 48 | } 49 | } 50 | } 51 | } 52 | } catch (err) { 53 | throw new Error( 54 | `Error loading module at ${fullPath}, check if this file is a correct route module or not` 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/utils/handleParameters.ts: -------------------------------------------------------------------------------- 1 | export const handleParameters = (token: string): string => { 2 | const squareBracketRegex: RegExp = /\[(.*)\]/gu; 3 | const tsRegex: RegExp = /\.ts$/u; 4 | const jsRegex: RegExp = /\.js$/u; 5 | const wildCardRouteRegex: RegExp = /\[\.\.\..+\]/gu; 6 | const multipleParamRegex: RegExp = /\]-\[/gu; 7 | const routeParamRegex: RegExp = /\]\/\[/gu; 8 | 9 | // This will clean the url extensions like .ts or .js 10 | const tokenToBeReplaced: string = token 11 | .replace(tsRegex, "") 12 | .replace(jsRegex, ""); 13 | // This will handle wild card based routes - users/[...id]/profile.ts -> users/*/profile 14 | const wildCardRouteHandled: string = tokenToBeReplaced.replace( 15 | wildCardRouteRegex, 16 | () => "*" 17 | ); 18 | 19 | // This will handle the generic square bracket based routes - users/[id]/index.ts -> users/:id 20 | const url: string = wildCardRouteHandled.replace( 21 | squareBracketRegex, 22 | (subString, match) => `:${String(match)}` 23 | ); 24 | 25 | // This will handle the case when multiple parameters are present in one file like - 26 | //users / [id] - [name].ts to users /: id -:name and users / [id] - [name] / [age].ts to users /: id -: name /: age 27 | return url.replace(multipleParamRegex, "-:").replace(routeParamRegex, "/:"); 28 | }; 29 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./autoload"; 2 | export * from "./handleParameters"; 3 | export * from "./isAcceptableFile"; 4 | export * from "./loadModule"; 5 | export * from "./scanFolders"; 6 | export * from "./transformPathToUrl"; 7 | -------------------------------------------------------------------------------- /src/utils/isAcceptableFile.ts: -------------------------------------------------------------------------------- 1 | import type fs from "fs"; 2 | import path from "path"; 3 | 4 | export function isAcceptableFile(file: string, stat: fs.Stats): boolean { 5 | const regex: RegExp = /\.test\.|\.spec\./u; 6 | const fileFolderPath: string = path.basename(file); 7 | 8 | // check if file starts with _ or . 9 | if (fileFolderPath.startsWith(".") || fileFolderPath.startsWith("_")) { 10 | return false; 11 | } 12 | // check if string contains .test. or .spec. 13 | if (regex.test(fileFolderPath)) { 14 | return false; 15 | } 16 | 17 | return stat.isFile(); 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/loadModule.ts: -------------------------------------------------------------------------------- 1 | import type { RouteOptions } from "fastify"; 2 | import type { RouteGenericInterface } from "fastify/types/route"; 3 | import type { IncomingMessage, Server, ServerResponse } from "http"; 4 | 5 | export type ModuleType = { 6 | [s: string]: RouteOptions< 7 | Server, 8 | IncomingMessage, 9 | ServerResponse, 10 | RouteGenericInterface, 11 | unknown 12 | >; 13 | }; 14 | 15 | export async function loadModule(path: string): Promise { 16 | const module: { 17 | routes: ModuleType; 18 | } = await import(path); 19 | 20 | return module.routes; 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/scanFolders.ts: -------------------------------------------------------------------------------- 1 | import type { FastifyInstance } from "fastify"; 2 | import fs from "fs"; 3 | import path from "path"; 4 | import { isAcceptableFile } from "."; 5 | import { autoload } from "./autoload"; 6 | import { transformPathToUrl } from "./transformPathToUrl"; 7 | 8 | export async function scanFolders( 9 | fastify: FastifyInstance, 10 | baseDir: string, 11 | current: string, 12 | prefix: string 13 | ): Promise { 14 | const combined: string = path.join(baseDir, current); 15 | const combinedStat: fs.Stats = fs.statSync(combined); 16 | 17 | if (combinedStat.isDirectory()) { 18 | if (!path.basename(current).startsWith("_")) { 19 | for (const entry of fs.readdirSync(combined)) { 20 | await scanFolders(fastify, baseDir, path.join(current, entry), prefix); 21 | } 22 | } 23 | } else if (isAcceptableFile(combined, combinedStat)) { 24 | await autoload(fastify, combined, transformPathToUrl(current), prefix); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/transformPathToUrl.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { handleParameters } from "./handleParameters"; 3 | 4 | // This function will transform the path of a route to a url 5 | 6 | export const transformPathToUrl = (filePath: string): string => { 7 | const url: string = `/${filePath}`; 8 | 9 | if (url.length === 1) { 10 | return url; 11 | } 12 | 13 | let resultUrl: string = url 14 | .split(path.sep) 15 | .map((part) => handleParameters(part)) 16 | .join("/"); 17 | 18 | if (resultUrl.endsWith("index")) { 19 | resultUrl = resultUrl.replace("index", ""); 20 | } 21 | 22 | // This removes the last slash from the string if it exists 23 | if (resultUrl.endsWith("/")) { 24 | resultUrl = resultUrl.slice(0, -1); 25 | } 26 | 27 | // This handle the case when only index remains, so a default route is created 28 | if (resultUrl.length === 0) { 29 | return "/"; 30 | } 31 | 32 | return resultUrl.replace("//", "/"); 33 | }; 34 | -------------------------------------------------------------------------------- /test/handleParameters.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { handleParameters } from "../src/utils"; 3 | 4 | describe("handleParameters", () => { 5 | it(`1 - [spark]`, () => { 6 | expect(handleParameters("[spark]")).toBe(":spark"); 7 | }); 8 | 9 | it(`2 - index`, () => { 10 | expect(handleParameters("index")).toBe("index"); 11 | }); 12 | 13 | it(`3 - user/:id`, () => { 14 | expect(handleParameters("/user/[id].ts")).toBe("/user/:id"); 15 | }); 16 | 17 | it(`4 - user Profile`, () => { 18 | expect(handleParameters("/user/[...id]/profile")).toBe("/user/*/profile"); 19 | }); 20 | 21 | it(`5 - profile/settings.ts`, () => { 22 | expect(handleParameters("/profile/settings")).toBe("/profile/settings"); 23 | }); 24 | 25 | it(`6 - profile/:id/spark`, () => { 26 | expect(handleParameters("/profile/[id]/spark.ts")).toBe( 27 | "/profile/:id/spark" 28 | ); 29 | }); 30 | 31 | it(`7 - profile/:id-:spark`, () => { 32 | expect(handleParameters("/profile/[id]-[spark].ts")).toBe( 33 | "/profile/:id-:spark" 34 | ); 35 | }); 36 | 37 | it(`8 - profile/:id-:spark/:nice`, () => { 38 | expect(handleParameters("/profile/[id]-[spark]/[nice].ts")).toBe( 39 | "/profile/:id-:spark/:nice" 40 | ); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /test/ignored/.ignored.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spa5k/fastify-file-routes/cf0d8cc7fa694dc1cbd0c4b8c57e8664f66f2b8a/test/ignored/.ignored.ts -------------------------------------------------------------------------------- /test/ignored/_ignore_me/game.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spa5k/fastify-file-routes/cf0d8cc7fa694dc1cbd0c4b8c57e8664f66f2b8a/test/ignored/_ignore_me/game.ts -------------------------------------------------------------------------------- /test/ignored/_ignored.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spa5k/fastify-file-routes/cf0d8cc7fa694dc1cbd0c4b8c57e8664f66f2b8a/test/ignored/_ignored.ts -------------------------------------------------------------------------------- /test/ignored/index.spec.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spa5k/fastify-file-routes/cf0d8cc7fa694dc1cbd0c4b8c57e8664f66f2b8a/test/ignored/index.spec.ts -------------------------------------------------------------------------------- /test/ignored/index.test.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spa5k/fastify-file-routes/cf0d8cc7fa694dc1cbd0c4b8c57e8664f66f2b8a/test/ignored/index.test.ts -------------------------------------------------------------------------------- /test/isAcceptableFile.spec.ts: -------------------------------------------------------------------------------- 1 | // in this test we will check whether the function isAcceptableFile works as expected. 2 | // it will take urls of files and then check whether it returns true or fastifyFileRoutesPlugin 3 | import { statSync } from "fs"; 4 | import { describe, expect, it } from "vitest"; 5 | import { isAcceptableFile } from "../src/utils"; 6 | 7 | describe("isAcceptableFile", () => { 8 | // testing acceptable file - returns true 9 | it("1 - Acceptable File", () => { 10 | const absolutePath = require.resolve( 11 | "../example/typescript/routes/index.ts" 12 | ); 13 | const combinedStat = statSync(absolutePath); 14 | 15 | expect(isAcceptableFile(absolutePath, combinedStat)).toBe(true); 16 | }); 17 | 18 | it("2 - Unacceptable File", () => { 19 | const absolutePath = require.resolve("./ignored/_ignored.ts"); 20 | const combinedStat = statSync(absolutePath); 21 | 22 | expect(isAcceptableFile(absolutePath, combinedStat)).toBe(false); 23 | }); 24 | 25 | it("3 - test File", () => { 26 | const absolutePath = require.resolve("./ignored/index.test.ts"); 27 | const combinedStat = statSync(absolutePath); 28 | 29 | expect(isAcceptableFile(absolutePath, combinedStat)).toBe(false); 30 | }); 31 | 32 | it("4 - Spec File", () => { 33 | const absolutePath = require.resolve("./ignored/index.spec.ts"); 34 | const combinedStat = statSync(absolutePath); 35 | 36 | expect(isAcceptableFile(absolutePath, combinedStat)).toBe(false); 37 | }); 38 | 39 | it("5 - Ignored . File", () => { 40 | const absolutePath = require.resolve("./ignored/.ignored.ts"); 41 | const combinedStat = statSync(absolutePath); 42 | 43 | expect(isAcceptableFile(absolutePath, combinedStat)).toBe(false); 44 | }); 45 | 46 | it("6 - Acceptable File 2", () => { 47 | const absolutePath = require.resolve( 48 | "../example/typescript/routes/profile/[game].ts" 49 | ); 50 | const combinedStat = statSync(absolutePath); 51 | 52 | expect(isAcceptableFile(absolutePath, combinedStat)).toBe(true); 53 | }); 54 | 55 | it("7 - Ignored Folder", () => { 56 | const absolutePath = require.resolve("./ignored/_ignore_me/game.ts"); 57 | const combinedStat = statSync(absolutePath); 58 | 59 | expect(isAcceptableFile(absolutePath, combinedStat)).toBe(true); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/loadModule.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { loadModule } from "../src/utils"; 3 | 4 | describe("loadModule", () => { 5 | it(`1 - Load Module`, () => { 6 | // get absolute path to the routes/index file. 7 | 8 | const absolutePath = require.resolve( 9 | "../example/typescript/routes/index.ts" 10 | ); 11 | 12 | expect(JSON.stringify(loadModule(absolutePath))).toBe("{}"); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /test/routes.test.ts: -------------------------------------------------------------------------------- 1 | import fetch from "node-fetch"; 2 | import { describe, expect, it } from "vitest"; 3 | 4 | describe("Route Test", () => { 5 | it(`1 - Index Route`, async () => { 6 | // get absolute path to the routes/index file. 7 | 8 | const response = await fetch("http://localhost:3000"); 9 | const data: { get: string } = (await response.json()) as { get: string }; 10 | 11 | expect(data.get).toBe("index"); 12 | }); 13 | 14 | it(`2 - User Index Route`, async () => { 15 | // get absolute path to the routes/index file. 16 | 17 | const response = await fetch("http://localhost:3000/user"); 18 | const data: { get: string } = (await response.json()) as { 19 | get: string; 20 | }; 21 | 22 | expect(data.get).toBe("get user"); 23 | }); 24 | 25 | it(`3 - User Index Route - Post`, async () => { 26 | // get absolute path to the routes/index file. 27 | 28 | const response = await fetch("http://localhost:3000/user", { 29 | method: "POST", 30 | }); 31 | const data: { post: string } = (await response.json()) as { 32 | post: string; 33 | }; 34 | 35 | expect(data.post).toBe("post user"); 36 | }); 37 | 38 | it(`4 - User [id] Route`, async () => { 39 | // get absolute path to the routes/index file. 40 | 41 | const response = await fetch("http://localhost:3000/user/1"); 42 | const data: { id: string } = (await response.json()) as { 43 | id: string; 44 | }; 45 | 46 | expect(data.id).toBe("1"); 47 | }); 48 | 49 | it(`5 - Profile [game] Route`, async () => { 50 | // get absolute path to the routes/index file. 51 | 52 | const response = await fetch("http://localhost:3000/profile/noice"); 53 | const data: { game: string } = (await response.json()) as { 54 | game: string; 55 | }; 56 | 57 | expect(data.game).toBe("noice"); 58 | }); 59 | 60 | it(`6 - Profile [...id] settings Route`, async () => { 61 | // get absolute path to the routes/index file. 62 | 63 | const response = await fetch( 64 | "http://localhost:3000/profile/spark/settings" 65 | ); 66 | const data: { "*": string } = (await response.json()) as { 67 | "*": string; 68 | }; 69 | 70 | expect(data["*"]).toBe("spark/settings"); 71 | }); 72 | 73 | it(`7 - Ignored Route Route`, async () => { 74 | // get absolute path to the routes/index file. 75 | 76 | const response = await fetch("http://localhost:3000/_ignore_me"); 77 | const data: { message: string } = (await response.json()) as { 78 | message: string; 79 | }; 80 | 81 | expect(data.message).toBe("Route GET:/_ignore_me not found"); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /test/transformPathToUrl.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { transformPathToUrl } from "./../src/utils/transformPathToUrl"; 3 | 4 | describe("cleanUrlString", () => { 5 | it(`1 - profile`, () => { 6 | expect(transformPathToUrl("user/profile/index.ts")).toBe("/user/profile"); 7 | }); 8 | 9 | it(`2 - profile [id]`, () => { 10 | expect(transformPathToUrl("user/profile/[id].ts")).toBe( 11 | "/user/profile/:id" 12 | ); 13 | }); 14 | 15 | it(`3 - profile wildcard`, () => { 16 | expect(transformPathToUrl("user/[...profile]/settings.ts")).toBe( 17 | "/user/*/settings" 18 | ); 19 | }); 20 | 21 | it(`4 - user [game]`, () => { 22 | expect(transformPathToUrl("user/[game].ts")).toBe("/user/:game"); 23 | }); 24 | 25 | it(`5 - index`, () => { 26 | expect(transformPathToUrl("/")).toBe("/"); 27 | }); 28 | 29 | it(`6 - index.ts`, () => { 30 | expect(transformPathToUrl("/index.ts")).toBe("/"); 31 | }); 32 | 33 | it(`7 - index.js`, () => { 34 | expect(transformPathToUrl("/index.js")).toBe("/"); 35 | }); 36 | 37 | it(`8 - /profile/[game]/index.ts`, () => { 38 | expect(transformPathToUrl("/profile/[game]/index.ts")).toBe( 39 | "/profile/:game" 40 | ); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "lib": ["esnext"], 6 | "importHelpers": true, 7 | "allowJs": false, 8 | 9 | // output .d.ts declaration files for consumers 10 | "declaration": true, 11 | "resolveJsonModule": true, 12 | "downlevelIteration": true, 13 | // output .js.map sourcemap files for consumers 14 | "sourceMap": true, 15 | "outDir": "dist", 16 | // match output dir to input dir. e.g. dist/index instead of dist/src/index 17 | "rootDir": ".", 18 | // stricter type-checking for stronger correctness. Recommended by TS 19 | "strict": true, 20 | // linter checks for common issues 21 | "noImplicitReturns": true, 22 | "noFallthroughCasesInSwitch": true, 23 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative 24 | "noUnusedLocals": false, 25 | "noUnusedParameters": false, 26 | // use Node's module resolution algorithm, instead of the legacy TS one 27 | "moduleResolution": "node", 28 | // transpile JSX to React.createElement 29 | "jsx": "react", 30 | // interop between ESM and CJS modules. Recommended by TS 31 | "esModuleInterop": true, 32 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS 33 | "skipLibCheck": true, 34 | // error out if import and file system have a casing mismatch. Recommended by TS 35 | "forceConsistentCasingInFileNames": true, 36 | // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` 37 | "noEmit": false 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import type { Options } from "tsup"; 2 | 3 | const env: string | undefined = process.env.NODE_ENV; 4 | 5 | export const tsup: Options = { 6 | splitting: false, 7 | sourcemap: false, 8 | clean: true, 9 | dts: true, 10 | format: ["cjs", "esm"], 11 | minify: false, 12 | bundle: true, 13 | skipNodeModulesBundle: true, 14 | entryPoints: ["src/index.ts"], 15 | watch: env === "development", 16 | target: "node14", 17 | }; 18 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | exclude: ["**/ignored/**", "**/node_modules/**"], 6 | watch: false, 7 | root: "./src", 8 | // include: ["./src/**"], 9 | }, 10 | }); 11 | --------------------------------------------------------------------------------