├── .eslintignore ├── .eslintrc ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md ├── images │ ├── cover.png │ ├── logo.png │ ├── nuxt-typed-router.gif │ └── redirectDoc.svg └── workflows │ ├── build-test.yml │ └── release.yml ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── .gitignore ├── README.md ├── app.config.ts ├── assets │ └── theme.css ├── components │ └── Logo.vue ├── content │ ├── 0.index.md │ ├── 1.guide │ │ ├── 0.index.md │ │ ├── 1.configuration.md │ │ └── _dir.yml │ ├── 2.usage │ │ ├── 0.how-to-use.md │ │ ├── 1.NuxtLink.md │ │ ├── 2.useRoute.md │ │ ├── 3.useRouter.md │ │ ├── 4.navigateTo.md │ │ ├── 5.i18n.md │ │ ├── 6.definePageMeta().md │ │ ├── 7.helpers.md │ │ ├── 8.typing-components.md │ │ └── _dir.yml │ ├── 3.options │ │ ├── 0.plugin.md │ │ ├── 1.strict.md │ │ ├── 2.pathCheck.md │ │ ├── 3.removeNuxtDefs.md │ │ ├── 4.ignoreRoutes.md │ │ └── _dir.yml │ ├── 4.api │ │ ├── 0.types.md │ │ ├── 2.importAliases.md │ │ ├── 3.outside-vue.md │ │ ├── 4.plugin.md │ │ ├── 5.route-names.md │ │ └── _dir.yml │ └── 5.playground │ │ ├── 0.index.md │ │ └── _dir.yml ├── layouts │ └── Default.vue ├── nuxt.config.ts ├── package.json ├── public │ └── favicon.ico ├── renovate.json ├── tokens.config.ts └── tsconfig.json ├── package.json ├── playground ├── app │ ├── components │ │ └── TestLink.vue │ ├── modules │ │ └── test-module.ts │ ├── pages │ │ ├── [...404].vue │ │ ├── [lang] │ │ │ └── post │ │ │ │ └── [slug].vue │ │ ├── admin │ │ │ ├── [444] │ │ │ │ ├── [...languages].vue │ │ │ │ └── index.vue │ │ │ └── profil.vue │ │ ├── catch-[...all].vue │ │ ├── index.vue │ │ ├── index │ │ │ └── index.vue │ │ ├── test │ │ │ └── [foo].vue │ │ └── user │ │ │ ├── [id].vue │ │ │ ├── [id] │ │ │ ├── [slug].vue │ │ │ ├── [slug] │ │ │ │ ├── [foo]-test-[[bar]].vue │ │ │ │ ├── articles.vue │ │ │ │ └── index.vue │ │ │ ├── index.vue │ │ │ └── posts.vue │ │ │ ├── index.vue │ │ │ └── test-[[optional]].vue │ └── store │ │ └── index.ts ├── nuxt.config.ts └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── src ├── core │ ├── config │ │ ├── index.ts │ │ └── moduleOptions.ts │ ├── fs │ │ ├── index.ts │ │ ├── prettierFormat.ts │ │ └── writeFile.ts │ ├── index.ts │ ├── output │ │ ├── fileSave │ │ │ ├── definitions.save.ts │ │ │ ├── index.ts │ │ │ └── plugin.save.ts │ │ ├── generators │ │ │ ├── blocks │ │ │ │ ├── index.ts │ │ │ │ └── routes │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── routes-named-locations-resolved.block.ts │ │ │ │ │ ├── routes-named-locations.block.ts │ │ │ │ │ ├── routes-names-list.block.ts │ │ │ │ │ ├── routes-params-record-resolved.block.ts │ │ │ │ │ ├── routes-params-record.block.ts │ │ │ │ │ └── routes-paths.block.ts │ │ │ ├── files │ │ │ │ ├── __NuxtLinkLocale.file.ts │ │ │ │ ├── __definePageMeta.file.ts │ │ │ │ ├── __helpers.file.ts │ │ │ │ ├── __i18n-router.file.ts │ │ │ │ ├── __navigateTo.file.ts │ │ │ │ ├── __paths.file.ts │ │ │ │ ├── __router.d.file.ts │ │ │ │ ├── __routes.file.ts │ │ │ │ ├── __typed-router.d.file.ts │ │ │ │ ├── __types-utils.d.file.ts │ │ │ │ ├── __useTypedLink.file.ts │ │ │ │ ├── __useTypedRoute.file.ts │ │ │ │ ├── __useTypedRouter.file.ts │ │ │ │ ├── index.file.ts │ │ │ │ ├── index.ts │ │ │ │ └── plugin.file.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ └── static │ │ │ ├── index.ts │ │ │ └── watermark.template.ts │ └── parser │ │ ├── base.ts │ │ ├── extractChunks.ts │ │ ├── i18n.modifiers.ts │ │ ├── index.ts │ │ ├── params │ │ ├── destructurePath.ts │ │ ├── extractParams.ts │ │ ├── index.ts │ │ └── replaceParams.ts │ │ ├── removeNuxtDefs.ts │ │ └── walkRoutes.ts ├── module.ts ├── types │ ├── config.types.ts │ ├── generator.types.ts │ └── index.ts └── utils │ ├── index.ts │ └── misc.utils.ts ├── test ├── README.md ├── e2e │ ├── complex │ │ └── complex.spec.ts │ ├── simple │ │ └── simple.spec.ts │ └── utils.ts ├── fixtures │ ├── complex │ │ ├── .gitignore │ │ ├── README.md │ │ ├── app.vue │ │ ├── app │ │ │ ├── components │ │ │ │ └── testModule.vue │ │ │ ├── modules │ │ │ │ └── testAddRoute.ts │ │ │ └── pages │ │ │ │ ├── [...404].vue │ │ │ │ ├── admin │ │ │ │ ├── [id].vue │ │ │ │ ├── [id] │ │ │ │ │ ├── action-[slug].vue │ │ │ │ │ ├── index.vue │ │ │ │ │ ├── profile.vue │ │ │ │ │ └── settings.vue │ │ │ │ └── panel │ │ │ │ │ └── [[blou]].vue │ │ │ │ ├── baguette.vue │ │ │ │ ├── index.vue │ │ │ │ └── user │ │ │ │ ├── [foo]-[[bar]].vue │ │ │ │ ├── [id].vue │ │ │ │ ├── [id] │ │ │ │ ├── [slug].vue │ │ │ │ ├── [slug] │ │ │ │ │ ├── articles.vue │ │ │ │ │ └── index.vue │ │ │ │ ├── index.vue │ │ │ │ └── posts.vue │ │ │ │ ├── [one]-foo-[two].vue │ │ │ │ ├── catch │ │ │ │ └── [...slug].vue │ │ │ │ ├── index.vue │ │ │ │ └── test-[[optional]].vue │ │ ├── nuxt.config.ts │ │ ├── package.json │ │ ├── tests │ │ │ ├── i18n │ │ │ │ ├── NuxtLinkLocale.spec-d.ts │ │ │ │ ├── useLocalePath.spec-d.ts │ │ │ │ └── useLocaleRoute.spec-d.ts │ │ │ ├── misc │ │ │ │ └── definePageMeta.spec-d.ts │ │ │ ├── router │ │ │ │ ├── $typedRouter.spec-d.ts │ │ │ │ ├── NuxtLink.spec-d.ts │ │ │ │ ├── navigateTo.spec-d.ts │ │ │ │ └── useRouter.spec-d.ts │ │ │ └── routes │ │ │ │ └── useRoute.spec-d.ts │ │ └── tsconfig.json │ ├── simple │ │ ├── .gitignore │ │ ├── README.md │ │ ├── app.vue │ │ ├── app │ │ │ └── pages │ │ │ │ ├── admin.vue │ │ │ │ ├── admin │ │ │ │ ├── [id].vue │ │ │ │ ├── [id] │ │ │ │ │ ├── action-[slug].vue │ │ │ │ │ ├── index.vue │ │ │ │ │ ├── profile.vue │ │ │ │ │ └── settings.vue │ │ │ │ └── panel │ │ │ │ │ └── [[blou]].vue │ │ │ │ ├── baguette.vue │ │ │ │ ├── index.vue │ │ │ │ └── user │ │ │ │ ├── [foo]-[[bar]].vue │ │ │ │ ├── [id].vue │ │ │ │ ├── [id] │ │ │ │ ├── [slug].vue │ │ │ │ ├── [slug] │ │ │ │ │ ├── articles.vue │ │ │ │ │ └── index.vue │ │ │ │ ├── index.vue │ │ │ │ └── posts.vue │ │ │ │ ├── [one]-foo-[two].vue │ │ │ │ ├── catch │ │ │ │ └── [...slug].vue │ │ │ │ ├── index.vue │ │ │ │ └── test-[[optional]].vue │ │ ├── nuxt.config.ts │ │ ├── package.json │ │ ├── tests │ │ │ ├── misc │ │ │ │ └── definePageMeta.spec-d.ts │ │ │ ├── router │ │ │ │ ├── NuxtLink.spec-d.ts │ │ │ │ ├── navigateTo.spec-d.ts │ │ │ │ └── useRouter.spec-d.ts │ │ │ └── routes │ │ │ │ ├── useLink.spec-d.ts │ │ │ │ └── useRoute.spec-d.ts │ │ └── tsconfig.json │ └── withOptions │ │ ├── .gitignore │ │ ├── README.md │ │ ├── app.vue │ │ ├── nuxt.config.ts │ │ ├── package.json │ │ ├── pages │ │ ├── index.vue │ │ └── user │ │ │ ├── [...slug].vue │ │ │ ├── [foo]-[[bar]].vue │ │ │ ├── [id].vue │ │ │ ├── [id] │ │ │ ├── [slug].vue │ │ │ ├── [slug] │ │ │ │ ├── articles.vue │ │ │ │ └── index.vue │ │ │ ├── index.vue │ │ │ └── posts.vue │ │ │ ├── [one]-foo-[two].vue │ │ │ ├── index.vue │ │ │ └── test-[[optional]].vue │ │ ├── tests │ │ └── e2e │ │ │ ├── withPartialStrict.spec.ts │ │ │ └── withStrict.spec.ts │ │ └── tsconfig.json ├── tsconfig.json └── utils │ ├── index.ts │ ├── tsd.utils.ts │ └── typecheck.ts ├── tsconfig.json └── vite.config.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@nuxtjs/eslint-config-typescript", "plugin:vue/vue3-recommended", "prettier"], 3 | "parser": "vue-eslint-parser", 4 | "parserOptions": { 5 | "parser": "@typescript-eslint/parser", 6 | "ecmaVersion": 2020, 7 | "sourceType": "module" 8 | }, 9 | "plugins": ["@typescript-eslint", "vue"], 10 | "root": true, 11 | "rules": { 12 | "semi": "off", 13 | "prefer-const": "off", 14 | "no-console": "off", 15 | "import/default": "off", 16 | "import/no-named-as-default-member": "off", 17 | "promise/param-names": "off", 18 | "no-use-before-define": "off", 19 | "vue/multi-word-component-names": "off", 20 | "@typescript-eslint/no-inferrable-types": "off", 21 | "@typescript-eslint/no-unused-vars": ["off"], 22 | "@typescript-eslint/consistent-type-imports": "warn", 23 | "import/named": "off" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 🐞 3 | about: Create a report to help me improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Expected behavior** 14 | A clear and concise description of what you expected to happen. 15 | 16 | **Screenshots** 17 | If applicable, add screenshots to help explain your problem. 18 | 19 | **Environnement infos** 20 | 21 | Run `nuxi infos` 22 | 23 | **Your `pages` folder structure** 24 | 25 | Run `npx tree-node-cli {your page folder path}` 26 | 27 | **Your nuxt.config.ts** 28 | Paste your modules config (and i18n config if you're using it) 29 | -------------------------------------------------------------------------------- /.github/images/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victorgarciaesgi/nuxt-typed-router/65255918ad267f8eacf322f9d1d1c28509d81ece/.github/images/cover.png -------------------------------------------------------------------------------- /.github/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victorgarciaesgi/nuxt-typed-router/65255918ad267f8eacf322f9d1d1c28509d81ece/.github/images/logo.png -------------------------------------------------------------------------------- /.github/images/nuxt-typed-router.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victorgarciaesgi/nuxt-typed-router/65255918ad267f8eacf322f9d1d1c28509d81ece/.github/images/nuxt-typed-router.gif -------------------------------------------------------------------------------- /.github/images/redirectDoc.svg: -------------------------------------------------------------------------------- 1 | 2 | redirectDoc-svg 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.github/workflows/build-test.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [push, pull_request, fork, pull_request_review] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [18.x] 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | - uses: pnpm/action-setup@v2 20 | with: 21 | version: 6.0.2 22 | - run: pnpm i 23 | - run: npx playwright install --with-deps 24 | - run: pnpm run test 25 | env: 26 | CI: true 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | tags: 9 | - 'v*' 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Set node 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: 18 23 | 24 | - run: npx changelogithub 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | lib 22 | ./types 23 | .nuxt 24 | .output 25 | .DS_Store 26 | 27 | # Coverage directory used by tools like istanbul 28 | coverage 29 | *.lcov 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (https://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | jspm_packages/ 49 | 50 | # TypeScript v1 declaration files 51 | typings/ 52 | 53 | # TypeScript cache 54 | *.tsbuildinfo 55 | 56 | # Optional npm cache directory 57 | .npm 58 | 59 | # Optional eslint cache 60 | .eslintcache 61 | 62 | # Microbundle cache 63 | .rpt2_cache/ 64 | .rts2_cache_cjs/ 65 | .rts2_cache_es/ 66 | .rts2_cache_umd/ 67 | 68 | # Optional REPL history 69 | .node_repl_history 70 | 71 | # Output of 'npm pack' 72 | *.tgz 73 | 74 | # Yarn Integrity file 75 | .yarn-integrity 76 | 77 | # dotenv environment variables file 78 | .env 79 | .env.test 80 | 81 | # parcel-bundler cache (https://parceljs.org/) 82 | .cache 83 | 84 | # Next.js build output 85 | .next 86 | 87 | # Nuxt.js build / generate output 88 | .nuxt 89 | dist 90 | 91 | # Gatsby files 92 | .cache/ 93 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 94 | # https://nextjs.org/blog/next-9-1#public-directory-support 95 | # public 96 | 97 | # vuepress build output 98 | .vuepress/dist 99 | 100 | # Serverless directories 101 | .serverless/ 102 | 103 | # FuseBox cache 104 | .fusebox/ 105 | 106 | # DynamoDB Local files 107 | .dynamodb/ 108 | 109 | # TernJS port file 110 | .tern-port 111 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "trailingComma": "es5", 5 | "singleQuote": true, 6 | "semi": true, 7 | "bracketSpacing": true, 8 | "htmlWhitespaceSensitivity": "strict" 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "prettier.configPath": ".prettierrc", 4 | "references.preferredLocation": "peek" 5 | } 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on Keep a Changelog, and this project adheres to Semantic Versioning. 5 | 6 | [3.0.#] - 2022-02-23 7 | 8 | - Fixed GlobalComponents declaration #70 9 | 10 | [3.0.0] - 2022-02-22 11 | 12 | # 🎉 New features 13 | 14 | ## Path autocomplete and validity type-check (⚠️ Experimental) 15 | 16 | Can be disabled with the `experimentalPathCheck` option. 17 | 18 | - Autocomplete for path programmatic navigation 19 | - Support `NuxtLink`, `useRouter`, `navigateTo` and `useLocalePath` 20 | - Support query params and hashs 21 | - Throw an error if the path doesn't match any defined routes pattern 22 | 23 | This feature is still experimental and has to be well tested on more apps. 24 | 25 | ## Nuxt devtools support ⚙️ 26 | 27 | - Display nuxt-typed-router docs from devtools 28 | 29 | ## `definePageMeta` support 30 | 31 | Get autocompletion et type check for `redirect`, `validate` and `key` 32 | 33 | ## Bug fixs 🐞 34 | 35 | - Fixed tsconfig augmentation when used with other modules 36 | 37 | ## `@nuxtjs/i18n` 38 | 39 | - Removed routes generated by `@nuxtjs/i18n` from autocomplete 40 | - Support `prefix_and_default` strategy 41 | - `localePath` will only validate and autocomplete default routes 42 | 43 | # Breaking changes ⚠️ 44 | 45 | - Reworked `routeNames` object params to better match `pages` folder structure 46 | 47 | [2.3.1] - 2022-02-03 48 | 49 | - fix: support all i18n module declarations (#48) 50 | 51 | [2.3.0] - 2022-02-01 52 | 53 | # 🎉 New features 54 | 55 | ## i18n support (#48) 56 | 57 | - Autocomplete routes and locales on `useLocalePath` and `useLocaleRoute`. 58 | - Zero config! 59 | - Generate correct routes names for prefixed routes 60 | 61 | ## Strict mode support (#64) 62 | 63 | 64 | - Added `strict` option to prevent passing string paths 65 | - Customizable behaviours for `` and `router` 66 | 67 | 68 | # Misc 69 | 70 | - Small options refacto 71 | - Updated docs 72 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Victor Garcia 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 | 2 | 3 |

4 | nuxt-typed-router cover 5 |

6 | 7 | 8 | [npm-version-src]: https://img.shields.io/npm/v/nuxt-typed-router.svg 9 | [npm-version-href]: https://www.npmjs.com/package/nuxt-typed-router 10 | [npm-downloads-src]: https://img.shields.io/npm/dm/nuxt-typed-router.svg 11 | [npm-total-downloads-src]: https://img.shields.io/npm/dt/nuxt-typed-router.svg 12 | [npm-downloads-href]: https://www.npmjs.com/package/nuxt-typed-router 13 | 14 | [![npm version][npm-version-src]][npm-version-href] 15 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 16 | [![npm downloads][npm-total-downloads-src]][npm-downloads-href] 17 | 18 | 19 | 20 | ## Provide a type safe router to Nuxt 21 | 22 | - Supports all programmatic navigation utils (`NuxtLink`, `useRouter`, `navigateTo`, `useRoute`, `useLocalePath`, etc...) 23 | - Supports optional params and catchAll routes 24 | - Autocompletes routes paths, names and params 25 | - Throw error if route path is invalid 26 | - Out of the box `i18n` support 27 | - Supports routes extended by config and modules 28 | 29 |
30 | 31 |
32 |

33 | 34 |

35 |
36 | 37 | 38 | 39 | 40 | ## Documentation 41 | 42 | [![Documentation](https://github.com/victorgarciaesgi/nuxt-typed-router/blob/master/.github/images/redirectDoc.svg?raw=true)](https://nuxt-typed-router.vercel.app/) 43 | 44 | ## Play with it 45 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/edit/github-7e4xvw?file=store/testRouter.ts) 46 | 47 | Demo repo 🧪 : [nuxt-typed-router-demo](https://github.com/victorgarciaesgi/nuxt-typed-router-demo) 48 | 49 | 50 | ## Used by 51 | 52 | Malt logo 53 | 54 | 55 | ## Cool video about it from LearnVue 56 | 57 | [![Watch the video](https://img.youtube.com/vi/jiYoAiFb71Y/default.jpg)](https://www.youtube.com/watch?v=jiYoAiFb71Y&t) 58 | 59 | 60 | ## Compatibility: 61 | 62 | - Nuxt 3 63 | 64 | 65 | ## Install 66 | 67 | ```bash 68 | npx nuxi@latest module add typed-router 69 | ``` 70 | 71 | 72 | ## Configuration 73 | Register the module in the `nuxt.config.ts`, done! 74 | 75 | ```ts 76 | export default defineNuxtConfig({ 77 | modules: ['nuxt-typed-router'], 78 | }); 79 | ``` 80 | 81 | ## Development 82 | 83 | 1. Clone this repository 84 | 2. Install dependencies using `pnpm` 85 | 3. Build project for local tests `pnpm run test` 86 | 4. Start dev playground `pnpm run prepack && pnpm run dev` 87 | 5. Build project for deploy `pnpm prepack` 88 | 89 | ## 📑 License 90 | 91 | [MIT License](./LICENSE) 92 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.iml 3 | .idea 4 | *.log* 5 | .nuxt 6 | .vscode 7 | .DS_Store 8 | coverage 9 | dist 10 | sw.* 11 | .env 12 | .output 13 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Docus Starter 2 | 3 | Starter template for [Docus](https://docus.dev). 4 | 5 | ## Clone 6 | 7 | Clone the repository (using `nuxi`): 8 | 9 | ```bash 10 | npx nuxi init -t themes/docus 11 | ``` 12 | 13 | ## Setup 14 | 15 | Install dependencies: 16 | 17 | ```bash 18 | pnpm install 19 | ``` 20 | 21 | ## Development 22 | 23 | ```bash 24 | pnpm run dev 25 | ``` 26 | 27 | ## Edge Side Rendering 28 | 29 | Can be deployed to Vercel Functions, Netlify Functions, AWS, and most Node-compatible environments. 30 | 31 | Look at all the available presets [here](https://v3.nuxtjs.org/guide/deploy/presets). 32 | 33 | ```bash 34 | pnpm run build 35 | ``` 36 | 37 | ## Static Generation 38 | 39 | Use the `generate` command to build your application. 40 | 41 | The HTML files will be generated in the .output/public directory and ready to be deployed to any static compatible hosting. 42 | 43 | ```bash 44 | pnpm run generate 45 | ``` 46 | 47 | ## Preview build 48 | 49 | You might want to preview the result of your build locally, to do so, run the following command: 50 | 51 | ```bash 52 | pnpm run preview 53 | ``` 54 | 55 | --- 56 | 57 | For a detailed explanation of how things work, check out [Docus](https://docus.dev). 58 | -------------------------------------------------------------------------------- /docs/app.config.ts: -------------------------------------------------------------------------------- 1 | export default defineAppConfig({ 2 | docus: { 3 | url: 'https://nuxt-typed-router.vercel.app/', 4 | title: 'Nuxt Typed Router', 5 | description: 6 | 'Provide a type safe router to Nuxt with auto-generated typed definitions for route names and autocompletion for route params', 7 | image: 8 | 'https://raw.githubusercontent.com/victorgarciaesgi/nuxt-typed-router/master/.github/images/cover.png', 9 | socials: { 10 | github: 'victorgarciaesgi/nuxt-typed-router', 11 | }, 12 | aside: { 13 | level: 0, 14 | }, 15 | cover: { 16 | src: '/cover.jpg', 17 | alt: 'Type safe router for Nuxt', 18 | }, 19 | header: { 20 | logo: true, 21 | }, 22 | footer: { 23 | iconLinks: [ 24 | { 25 | href: 'https://nuxt.com', 26 | icon: 'IconNuxtLabs', 27 | }, 28 | ], 29 | credits: { 30 | icon: 'IconDocus', 31 | text: 'Powered by Docus', 32 | href: 'https://docus.dev', 33 | }, 34 | }, 35 | github: { 36 | dir: 'docs/content', 37 | edit: true, 38 | contributors: true, 39 | owner: 'victorgarciaesgi', 40 | repo: 'nuxt-typed-router', 41 | branch: 'master', 42 | }, 43 | }, 44 | }); 45 | -------------------------------------------------------------------------------- /docs/assets/theme.css: -------------------------------------------------------------------------------- 1 | br { 2 | margin: 8px 0 !important; 3 | } 4 | -------------------------------------------------------------------------------- /docs/components/Logo.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 29 | -------------------------------------------------------------------------------- /docs/content/0.index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Nuxt typed router 3 | description: Provide a type safe router to Nuxt with auto-generated typed definitions for route paths, names and params 4 | navigation: false 5 | layout: page 6 | --- 7 | 8 | ::block-hero 9 | --- 10 | cta: 11 | - Get Started 12 | - /guide 13 | secondary: 14 | - Open on GitHub → 15 | - https://github.com/victorgarciaesgi/nuxt-typed-router 16 | snippet: npx nuxi@latest module add typed-router 17 | --- 18 | 19 | #title 20 | Nuxt typed router 21 | 22 | #description 23 | Provide a type safe router to Nuxt with auto-generated typed definitions for route paths, names and params 24 | 25 | 26 | #extra 27 | ::list 28 | - Autocompletes and typecheck routes paths, names and params 29 | - Supports all programmatic navigation utils (`NuxtLink`, `useRouter`, `navigateTo`, `useRoute`, `useLocalePath`, etc...) 30 | - Supports optional params and catchAll routes 31 | - Throw error if route path is invalid 32 | - Out of the box `i18n` support 33 | - Supports routes extended by config and modules 34 | :: 35 | 36 | 37 | :: 38 | 39 | 40 | :ellipsis{top=500px} 41 | 42 | 43 |

44 | 45 |

46 | 47 |
48 |
49 | 50 | :button-link[Play on StackBlitz]{icon="IconStackBlitz" href="https://stackblitz.com/edit/github-7e4xvw?file=store/testRouter.ts" blank color="secondary"} 51 | 52 |
53 |
54 | 55 | ::card-grid 56 | #title 57 | Features 58 | #root 59 | :ellipsis 60 | #default 61 | ::card{icon="logos:typescript-icon"} 62 | #title 63 | Type safety 64 | #description 65 | Throws errors when route path/name/params doesn't match any page 66 | :: 67 | 68 | ::card{icon="noto:fire"} 69 | #title 70 | 0 config 71 | #description 72 | Just plug the module and watch the magic! 73 | Supports `autoImport: false` 74 | :: 75 | 76 | ::card{icon="noto:globe-with-meridians"} 77 | #title 78 | I18n support 79 | #description 80 | Out of the box support for prefixed i18n routes 81 | :: 82 | 83 | 84 | ::card{icon="noto:eyes"} 85 | #title 86 | Attentive 87 | #description 88 | Watch changes in your router structure to automatically reload your types 89 | :: 90 | :: -------------------------------------------------------------------------------- /docs/content/1.guide/0.index.md: -------------------------------------------------------------------------------- 1 | 2 | # Installation 3 | ```bash 4 | npx nuxi@latest module add typed-router 5 | ``` 6 | 7 | Add the module to `modules` in your `nuxt.config`: 8 | 9 | ```ts [nuxt.config.ts] 10 | export default defineNuxtConfig({ 11 | modules: [ 12 | 'nuxt-typed-router', 13 | ] 14 | }) 15 | ``` 16 | 17 | You now need to either launch `dev`, `prepare`, `generate` or `build` to generate the typings. 18 | 19 | ::alert{type="success"} 20 | Now every time that you change your `pages` folder, it will re-generate type definitions 21 | :: 22 | -------------------------------------------------------------------------------- /docs/content/1.guide/1.configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | Add an `nuxtTypedRouter` section in your `nuxt.config`: 4 | 5 | ```ts [nuxt.config.ts] 6 | export default defineNuxtConfig({ 7 | modules: [ 8 | ['nuxt-typed-router', { 9 | // options 10 | }] 11 | ], 12 | }) 13 | ``` 14 | 15 | Available options: 16 | 17 | ```ts 18 | export interface ModuleOptions { 19 | /** 20 | * 21 | * Enables path autocomplete and path validity for programmatic validation 22 | * 23 | * @default true 24 | */ 25 | pathCheck?: boolean; 26 | /** 27 | * Set to false if you don't want a plugin generated 28 | * @default false 29 | */ 30 | plugin?: boolean; 31 | /** 32 | * Customise Route location arguments strictness for `NuxtLink` or `router` 33 | * All strict options are disabled by default. 34 | * You can tweak options to add strict router navigation options. 35 | * 36 | * By passing `true` you can enable all of them 37 | * 38 | * @default false 39 | */ 40 | strict?: boolean | StrictOptions; 41 | /** 42 | * Remove Nuxt definitions to avoid conflicts 43 | * @default true 44 | */ 45 | removeNuxtDefs?: boolean; 46 | /** 47 | * ⚠️ Experimental 48 | * 49 | * Exclude certain routes from being included into the generated types 50 | * Ex: 404 routes or catchAll routes 51 | */ 52 | ignoreRoutes?: string[]; 53 | /** 54 | * Disable prettier formatter 55 | * @default false 56 | */ 57 | disablePrettier?: boolean; 58 | } 59 | 60 | ``` 61 | 62 | 63 | See [How it works](../2.usage/0.how-to-use.md) for documentation on how to use the typed router. 64 | 65 | ::alert{type="info"} 66 | If you disabled auto-imports, you can see the [usage without auto-imports](../4.api/3.importAliases.md) 67 | :: -------------------------------------------------------------------------------- /docs/content/1.guide/_dir.yml: -------------------------------------------------------------------------------- 1 | title: Guide 2 | icon: ph:star-duotone -------------------------------------------------------------------------------- /docs/content/2.usage/1.NuxtLink.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 3 | --- 4 | 5 | # NuxtLink 6 | 7 | 8 | You can use it like you used it before. 9 | 10 | ```vue 11 | 20 | ``` 21 | 22 | Your IDE will throw an error if the route `name` does not exists or if the `params` are invalid. 23 | 24 | ::alert{type="info"} 25 | You can do CLI type checking for `` with the [`vue-tsc` package](https://www.npmjs.com/package/vue-tsc). 26 | `external` prop is supported since `v3.1.0` 27 | :: 28 | 29 | ```vue 30 | 34 | ``` 35 | 36 | --- 37 | 38 | ::alert{type="warning"} 39 | `` typings can only be provided if you use the [Volar extension](https://marketplace.visualstudio.com/items?itemName=Vue.volar) 40 | :: 41 | -------------------------------------------------------------------------------- /docs/content/2.usage/2.useRoute.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: useRoute() 3 | --- 4 | 5 | # useRoute 6 | 7 | 8 | **Normal usage (in components)** 9 | 10 | If you have a `[foo].vue` page file, you can safely check the route name with the returned route object. 11 | 12 | 13 | ```vue 14 | 22 | ``` 23 | 24 | But if you have a `[foo]-[[bar]].vue` page file, the `bar` param will be correctly infered as optional in the `route` object 25 | 26 | 27 | ```vue 28 | 37 | ``` 38 | 39 | 40 | **Assertion usage (in pages)** 41 | 42 | You can also invoke `useRoute` directly with a route name to have it type check its params. 43 | For exemple, if you're editing `pages/profile/[id].vue`, you can directly do this and save time. 44 | 45 | ```vue 46 | 52 | 53 | ``` 54 | 55 | 56 | 57 | ## `useLink` 58 | 59 | Typings for `useLink` follow the usage of other composables. 60 | 61 | 62 | 63 | ```vue 64 | 68 | 69 | ``` -------------------------------------------------------------------------------- /docs/content/2.usage/3.useRouter.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: useRouter() 3 | --- 4 | 5 | # useRouter 6 | 7 | You can use `useRouter` the same way as before. 8 | 9 | If you have a `[foo].vue` page file, you can navigate to it like this: 10 | 11 | ```vue 12 | 17 | ``` 18 | 19 | --- 20 | 21 | If you have a `[foo]-[[bar]].vue` page file, the `bar` param will be correctly infered as optional in the `params` option 22 | 23 | ```vue 24 | 30 | ``` 31 | 32 | 33 | 34 | If you have a "catch all" page `[...slug].vue`, the typings will help you providing params 35 | 36 | 37 | ```vue 38 | 44 | ``` 45 | 46 | 47 | -------------------------------------------------------------------------------- /docs/content/2.usage/4.navigateTo.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: navigateTo() 3 | --- 4 | 5 | # navigateTo 6 | 7 | 8 | Like `useRouter` or `useRoute`, `navigateTo` can also be used, both as a global import or from the `@typed-router` alias 9 | 10 | ```vue 11 | 14 | ``` 15 | 16 | Or with alias 17 | 18 | ```vue 19 | 24 | ``` 25 | 26 | The resolved route with also be typed accordingly to the target route. 27 | 28 | 29 | ```vue 30 | 39 | ``` -------------------------------------------------------------------------------- /docs/content/2.usage/5.i18n.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: i18n 3 | --- 4 | 5 | # i18n 6 | 7 | Nuxt typed router has out of the box support for `@nuxtjs/i18n` 8 | 9 | To try it 10 | 11 | ::code-group 12 | ```bash [yarn] 13 | yarn add @nuxtjs/i18n@next 14 | ``` 15 | ```bash [pnpm] 16 | pnpm install @nuxtjs/i18n@next 17 | ``` 18 | ```bash [npm] 19 | npm install @nuxtjs/i18n@next 20 | ``` 21 | :: 22 | 23 | 24 | ```ts [nuxt.config.ts] 25 | export default defineNuxtConfig({ 26 | modules: [ 27 | 'nuxt-typed-router', 28 | '@nuxtjs/i18n' 29 | ], 30 | i18n: // your i18n config 31 | }) 32 | ``` 33 | 34 | You will have autocomplete for 35 | - Only the default routes when using `prefix(*)` strategies. 36 | - The registered locales 37 | 38 | 39 | ## `useLocalePath()` 40 | 41 | Global types for `useLocalePath` will be overwritten, and the function is also exported from `@typed-router`. 42 | 43 | ```ts 44 | const localePath = useLocalePath(); 45 | 46 | navigateTo(localePath({ name: 'user-id'}, 'fr')); // Error ❌ 47 | navigateTo(localePath({ name: 'user-id', params: {id: 1}}, 'fr')); // Good ✅ 48 | ``` 49 | 50 | 51 | ## `useLocaleRoute()` 52 | 53 | Global types for `useLocaleRoute` will be overwritten, and the function is also exported from `@typed-router`. 54 | 55 | ```ts 56 | const localeRoute = useLocaleRoute(); 57 | 58 | const route = localeRoute({ name: 'user-id', }, 'fr'); // Error ❌ 59 | const route = localeRoute({ name: 'user-id', params: {id: 1} } , 'fr'); // Good ✅ 60 | if (route) { 61 | navigateTo(route.fullPath); 62 | } 63 | 64 | ``` 65 | 66 | 67 | ## `NuxtLinkLocale` 68 | 69 | The component provived by `@nuxtjs/i18n` is also supported 70 | 71 | ```vue 72 | 81 | ``` -------------------------------------------------------------------------------- /docs/content/2.usage/6.definePageMeta().md: -------------------------------------------------------------------------------- 1 | --- 2 | title: definePageMeta() 3 | --- 4 | 5 | 6 | # definePageMeta 7 | 8 | You will have autocompletion and typecheck for multiple properties in `definePageMeta`, both as a global import or from the `@typed-router` alias. 9 | 10 | Like `useRoute`, it has a strict mode when you can assert the current page. 11 | 12 | Properties enabled: 13 | 14 | - `validate` 15 | - `redirect` 16 | - `key` 17 | 18 | ::alert{type="warning"} 19 | Do yo Typescript limitations, return type for `redirect` may be buggy or not display anything. 20 | You can use the `helpers` util as a workaround [Helpers doc](./7.helpers.md) 21 | :: 22 | 23 | 24 | Exemple: 25 | 26 | ```ts 27 | definePageMeta({ 28 | validate(route) { // <- Typed 29 | return true 30 | } 31 | }) 32 | 33 | ``` -------------------------------------------------------------------------------- /docs/content/2.usage/7.helpers.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: helpers 3 | --- 4 | 5 | # Helpers 6 | 7 | 8 | Alias `@typed-router` exports a `helpers` util that can be useful in places where there is no autocomplete yet. 9 | It simply returns the route you give as argument. 10 | 11 | 12 | ## `helpers.route` 13 | 14 | It will give you autocomplete and type check for route object 15 | 16 | ```ts 17 | import {helpers} from '@typed-router'; 18 | 19 | const route = helpers.route({name: 'admin-id'}) // Error ❌ 20 | const route = helpers.route({name: 'admin-id', params: {id: 1}}) // Good ✅ 21 | ``` 22 | 23 | ## `helpers.path` 24 | 25 | It will give you autocomplete and type check for string path 26 | 27 | ```ts 28 | import {helpers} from '@typed-router'; 29 | 30 | const route = helpers.path('/admin') // Error ❌ 31 | const route = helpers.path('/admin/1') // Good ✅ 32 | ``` -------------------------------------------------------------------------------- /docs/content/2.usage/8.typing-components.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Typing component props 3 | --- 4 | 5 | # NuxtLink 6 | 7 | 8 | Sometimes you don't want to use `NuxtLink` directly and make a wrapper component, there is a way to do it with `nuxt-typed-router`. It implies using component generics if you're on a Vue SFC 9 | 10 | ```vue 11 | 14 | 15 | 22 | ``` 23 | 24 | It's also easy to add support for `external` prop 25 | 26 | ```vue 27 | 30 | 31 | 39 | ``` 40 | 41 | ## Same behaviour for pure Typescript functions 42 | 43 | 44 | ```ts 45 | import type { RoutesNamesList, NuxtRoute } from '@typed-router'; 46 | 47 | export function myCustomNavigateTool(to: NuxtRoute) { 48 | // 49 | } 50 | 51 | ``` 52 | 53 | 54 | ::alert{type="info"} 55 | There is also the same type variant if you use `@nuxtjs/i18n` : `NuxtLocaleRoute` 56 | :: -------------------------------------------------------------------------------- /docs/content/2.usage/_dir.yml: -------------------------------------------------------------------------------- 1 | title: Usage 2 | icon: ph-puzzle-piece-duotone -------------------------------------------------------------------------------- /docs/content/3.options/0.plugin.md: -------------------------------------------------------------------------------- 1 | # plugin 2 | 3 | `plugin` option let you enable [global utils](../4.api/7.plugin.md) 4 | 5 | ## Type 6 | `type: boolean` 7 | `default: false` 8 | 9 | It allows you to use typed router in template as a global util. 10 | 11 | ```vue 12 | 15 | ``` 16 | 17 | ```ts 18 | const { $typedRouter, $typedRoute, $routesNames } = useNuxtApp(); 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/content/3.options/1.strict.md: -------------------------------------------------------------------------------- 1 | # strict 2 | 3 | Customise Route location arguments strictness for `NuxtLink` or `router`. 4 | 5 | - All strict options are disabled by default. 6 | - You can tweak options to add strict router navigation options. 7 | 8 | 9 | ## Type 10 | 11 | ```ts 12 | /** 13 | * @default false 14 | */ 15 | strict?: boolean | StrictOptions; 16 | ``` 17 | 18 | 19 | ## Exemple config 20 | 21 | ```ts [nuxt.config.ts] 22 | export default defineNuxtConfig({ 23 | ..., 24 | nuxtTypedRouter: { 25 | strict: true, 26 | // or 27 | strict: { 28 | router: { 29 | strictToArgument: true 30 | } 31 | } 32 | } 33 | }) 34 | 35 | ``` 36 | 37 | 38 | ### `StrictOptions` properties 39 | 40 | ```ts 41 | export interface StrictOptions { 42 | NuxtLink?: StrictParamsOptions; 43 | router?: StrictParamsOptions; 44 | } 45 | ``` 46 | 47 | ```ts 48 | export interface StrictParamsOptions { 49 | /** 50 | * @default false 51 | */ 52 | strictToArgument?: boolean; 53 | /** 54 | * @default false 55 | */ 56 | strictRouteLocation?: boolean; 57 | } 58 | ``` 59 | 60 | ### `strictToArgument` 61 | 62 | Prevent passing string path to the RouteLocation argument. 63 | 64 | Ex: 65 | ```vue 66 | 69 | ``` 70 | Or 71 | ```ts 72 | router.push('/login'); // Error ❌ 73 | navigateTo('/login'); // Error ❌ 74 | ``` 75 | 76 | 77 | ### `strictRouteLocation` 78 | 79 | Prevent passing a `params` property in the RouteLocation argument. 80 | 81 | Ex: 82 | ```vue 83 | 86 | ``` 87 | Or 88 | ```ts 89 | router.push({path: "/login"}); // Error ❌ 90 | navigateTo({path: "/login"}); // Error ❌ 91 | ``` -------------------------------------------------------------------------------- /docs/content/3.options/2.pathCheck.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: pathCheck 3 | --- 4 | 5 | # experimentalPathCheck 6 | 7 | It allows you to disable path autocomplete and typecheck 8 | ## Type 9 | `type: boolean` 10 | `default: true` 11 | 12 | -------------------------------------------------------------------------------- /docs/content/3.options/3.removeNuxtDefs.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: removeNuxtDefs 3 | --- 4 | 5 | # removeNuxtDefs 6 | 7 | Remove Nuxt global definitions for `useRouter` etc.. to avoid conflicts 8 | ## Type 9 | `type: boolean` 10 | `default: true` 11 | 12 | -------------------------------------------------------------------------------- /docs/content/3.options/4.ignoreRoutes.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ignoreRoutes 3 | --- 4 | 5 | # ignoreRoutes 6 | 7 | Allow to ignore selected files to be typed, for exemple 404 routes or catch-all routes. 8 | You can pass an array of file paths (taking base of your `pagesDir`). 9 | 10 | Usage: 11 | 12 | ```ts 13 | ignoreRoutes: ["[...404].vue", "admin/[...slug].vue"]; 14 | ``` 15 | 16 | ## Type 17 | `type: string[]` 18 | `default: []` 19 | 20 | -------------------------------------------------------------------------------- /docs/content/3.options/_dir.yml: -------------------------------------------------------------------------------- 1 | title: Options 2 | icon: ph-sliders-duotone -------------------------------------------------------------------------------- /docs/content/4.api/0.types.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Exported types 3 | --- 4 | 5 | 6 | # Exported Types 7 | 8 | You will be able to use the module generated types for custom purposes (like typing props) 9 | They are all importable from `@typed-router` alias 10 | 11 | --- 12 | ## Routes definitions types 13 | 14 | ## `RoutesNamesList` 15 | 16 | `RoutesNamesList` is an union type regrouping all the registered route names of your app. 17 | 18 | ```vue 19 | 26 | ``` 27 | 28 | ## `RoutesParamsRecord` 29 | 30 | `RoutesParamsRecord` is a dictionnary containing the route names as key and their params as values 31 | 32 | Exemple output 33 | 34 | ```ts 35 | export type RoutesParamsRecord = { 36 | index: never; 37 | 'user-foo-bar': { 38 | foo: string | number; 39 | bar?: string | number; 40 | }; 41 | 'user-id-slug-articles': { 42 | id?: string | number; 43 | slug?: string | number; 44 | }; 45 | user: never; 46 | 'user-posts-slug': { 47 | slug: (string | number)[]; 48 | }; 49 | 'user-test-optional': { 50 | optional?: string | number; 51 | }; 52 | }; 53 | ``` 54 | 55 | ## `RoutesNamedLocations` 56 | 57 | `RoutesNamedLocations` is Discriminated union that will allow to infer params based on route name 58 | _It's used for programmatic navigation like `router.push` or _ 59 | 60 | Exemple output 61 | 62 | ```ts 63 | export type RoutesNamedLocations = 64 | | { name: 'index' } 65 | | { 66 | name: 'user-foo-bar'; 67 | params: { 68 | foo: string | number; 69 | bar?: string | number; 70 | }; 71 | } 72 | | { 73 | name: 'user-id-slug-articles'; 74 | params?: { 75 | id?: string | number; 76 | slug?: string | number; 77 | }; 78 | } 79 | ``` 80 | 81 | ## `RoutesNamedLocationsResolved` 82 | 83 | `RoutesNamedLocationsResolved` is a partial discrimanated union type that regroups each route name and their params. 84 | This type is meant to be used for resolved routes. 85 | 86 | Exemple output 87 | ```ts 88 | export type RoutesNamedLocationsResolved = { 89 | name: TypedRouteList; 90 | params: unknown; 91 | } & ( 92 | | { name: 'index' } 93 | | { 94 | name: 'user-foo-bar'; 95 | params: { 96 | foo: string; 97 | bar?: string; 98 | }; 99 | } 100 | | { 101 | name: 'user-id-slug-articles'; 102 | params: { 103 | id: string; 104 | slug: string; 105 | }; 106 | } 107 | | { 108 | name: 'user-id-slug'; 109 | params: { 110 | id: string; 111 | slug: string; 112 | }; 113 | }; 114 | ``` 115 | 116 | ::alert{type="info"} 117 | In this type, we have a default intersection. It's useful because it means if we don't make a type guard for the route name, the `params` property will still be accessible 118 | :: 119 | 120 | --- 121 | 122 | ## Router related types 123 | 124 | ## `TypedRouter` 125 | 126 | `TypedRouter` is the type returned by `useRouter`. 127 | It's an extension of `vue-router.Router` with type abilities. 128 | 129 | ```ts 130 | import {TypedRouter} from '@typed-router'; 131 | 132 | declare const customRouter: TypedRouter 133 | ``` 134 | 135 | 136 | ## `TypedRouteLocationRaw` 137 | 138 | Clone of `vue-router.RouteLocationRaw` with discrimanated name and params properties 139 | 140 | 141 | ## `TypedRouteLocationRawFromName` 142 | 143 | Same as TypedRouteLocationRaw but with a generic param indicating route name 144 | 145 | --- 146 | 147 | ## Resolved routes related types 148 | 149 | ## `TypedRoute` 150 | 151 | `TypedRoute` is the default type returned by `useRoute`. 152 | It's an extension of `vue-router.Route` with type abilities. 153 | 154 | It includes a discrimated union for name/params type-check, meaning it can type check itself when doing a type guard check on its name. 155 | 156 | ```vue 157 | 168 | ``` 169 | 170 | ## `TypedNamedRoute` 171 | 172 | `TypedNamedRoute` is the secondary type returned by `useRoute`. 173 | It's a genereric type and it's first type argument is the route name. 174 | 175 | It's used when calling `useRoute` with a route name assertion. 176 | You can use it to infer the correct route params given a route name; 177 | 178 | ```ts 179 | import {TypedNamedRoute, TypedRouteList} from '@typed-router'; 180 | 181 | function getMyRoute(name: T): TypedNamedRoute { 182 | // 183 | } 184 | 185 | ``` 186 | 187 | 188 | 189 | 190 | 191 | -------------------------------------------------------------------------------- /docs/content/4.api/2.importAliases.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Without autoImport 3 | --- 4 | 5 | 6 | # Usage without autoImport 7 | 8 | If you have configured your Nuxt app like this: 9 | 10 | ```ts 11 | export default defineNuxtConfig({ 12 | imports: { 13 | autoImport: false 14 | } 15 | }) 16 | ``` 17 | 18 | It's still possible to use the typed `useRouter` and `useRoute`, but Typescripts limit the module override capability for `#app`. 19 | 20 | But Nuxt Typed Router will create an alias `@typed-router` where you can import router composables 21 | 22 | 23 | ```ts 24 | import {useRouter, useRoute, navigateTo} from '@typed-router'; 25 | 26 | const router = useRouter(); 27 | 28 | router.push({name: 'foo', params: { foo: 'bar' }}); 29 | 30 | navigateTo({name: 'foo', params: { foo: 'bar' }}); 31 | 32 | const route = useRoute('profile-id'); 33 | ``` 34 | 35 | 36 | -------------------------------------------------------------------------------- /docs/content/4.api/3.outside-vue.md: -------------------------------------------------------------------------------- 1 | # Usage outside Vue component 2 | 3 | You can import the `useTypedRouter` composable from where it's generated. 4 | Exemple with `pinia` store here 5 | 6 | ```ts 7 | import pinia from 'pinia'; 8 | import { useRouter } from '@typed-router'; 9 | 10 | export const useFooStore = defineStore('foo', () => { 11 | function bar() { 12 | const router = useRouter(); 13 | router.push({ name: 'profile-user', params: { user: 2 } }); 14 | } 15 | 16 | return { 17 | bar 18 | } 19 | }); 20 | ``` -------------------------------------------------------------------------------- /docs/content/4.api/4.plugin.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Plugin usage 3 | --- 4 | 5 | # Plugin 6 | 7 | 8 | ::alert{type="info"} 9 | To activate plugin, you need to add `plugin: true` to your config options 10 | :: 11 | 12 | A plugin will be automaticaly generated, providing global access to `$typedRouter` , `$typedRoutes` and `$routesNames` 13 | 14 | 15 | ```ts 16 | const { $typedRouter, $typedRoute, $routesNames } = useNuxtApp(); 17 | ``` -------------------------------------------------------------------------------- /docs/content/4.api/5.route-names.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Route names dictionnary 3 | --- 4 | 5 | # Route names dictionnary 6 | 7 | When your app is too big, you can start having 50+ routes, navigating the suggestions can not be as intuitive. 8 | 9 | A other way of providing the route name is available with `nuxt-typed-router` 10 | 11 | A object tree containing a representation of your `pages` folder is accessible everywhere. 12 | 13 | ```ts 14 | 15 | import {routesNames, useRouter} from '@typed-router'; 16 | 17 | const router = useRouter(); 18 | 19 | // Instead of doing 20 | router.push({name: 'profile-id-slug'}) 21 | 22 | 23 | // You can do 24 | 25 | router.push({name: routesNames.profile.id.slug}) 26 | ``` 27 | 28 | 29 | ::alert{type='info'} 30 | If you activated the `plugin` option, `$routesNames` will also be injected into your global properties 31 | :: -------------------------------------------------------------------------------- /docs/content/4.api/_dir.yml: -------------------------------------------------------------------------------- 1 | title: Api 2 | icon: ph-code-duotone -------------------------------------------------------------------------------- /docs/content/5.playground/0.index.md: -------------------------------------------------------------------------------- 1 | # Live example 2 | 3 | Unfortunetely, StackBlitz and CodeSandbox still can't support `Volar` IDE extension, so any `.vue` file autocompletion will not work online. 4 | 5 | [The example repo is available here](https://github.com/victorgarciaesgi/nuxt-typed-router-demo) 6 | 7 | The only online playground we can show will be on a Typescript file, but it wil be enough to see the magic. 8 | 9 | To test `` autocomplete, you will have to open it in your local VSCode 10 | 11 | - To test autocomplete on the online IDE, go he `store/testRouter.ts` 12 | - In local env (VSCode), you can edit `pages/index.vue` or any `.vue` file. 13 | - Add/delete pages files to see the magic 14 | 15 | 16 | :sandbox{src="https://stackblitz.com/edit/github-7e4xvw?embed=1&file=store/testRouter.ts&theme=dark&view=editor"} 17 | -------------------------------------------------------------------------------- /docs/content/5.playground/_dir.yml: -------------------------------------------------------------------------------- 1 | title: Playground 2 | icon: ph-codesandbox-logo-duotone -------------------------------------------------------------------------------- /docs/layouts/Default.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /docs/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtConfig({ 2 | extends: '@nuxt-themes/docus', 3 | css: ['@/assets/theme.css'], 4 | app: { 5 | head: { 6 | meta: [ 7 | { 8 | name: 'google-site-verification', 9 | content: 'g3klMOEJCzOJ5ATgQHqYvjYEcdGnpWVC9NpMxrvcqNA', 10 | }, 11 | ], 12 | }, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docus-starter", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "nuxi dev", 7 | "build": "nuxi build", 8 | "generate": "nuxi generate", 9 | "preview": "nuxi preview" 10 | }, 11 | "devDependencies": { 12 | "@nuxt-themes/docus": "1.15.1", 13 | "nuxt": "3.17.5" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victorgarciaesgi/nuxt-typed-router/65255918ad267f8eacf322f9d1d1c28509d81ece/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@nuxtjs" 4 | ], 5 | "lockFileMaintenance": { 6 | "enabled": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /docs/tokens.config.ts: -------------------------------------------------------------------------------- 1 | import { defineTheme } from 'pinceau'; 2 | 3 | export default defineTheme({ 4 | color: { 5 | primary: { 6 | 900: '#021b2c', 7 | 800: '#043353', 8 | 700: '#064b7a', 9 | 600: '#0763a1', 10 | 500: '#0a87db', 11 | 400: '#2092de', 12 | 300: '#4ba7e5', 13 | 200: '#77bceb', 14 | 100: '#a2d2f1', 15 | }, 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-typed-router", 3 | "version": "4.0.1", 4 | "description": "Provide autocompletion for routes paths, names and params in Nuxt apps", 5 | "type": "module", 6 | "main": "./dist/module.mjs", 7 | "types": "./dist/types.d.mts", 8 | "exports": { 9 | ".": { 10 | "types": "./dist/types.d.mts", 11 | "import": "./dist/module.mjs" 12 | } 13 | }, 14 | "files": [ 15 | "dist" 16 | ], 17 | "scripts": { 18 | "prepack": "nuxt-module-build build", 19 | "dev": "nuxi dev playground", 20 | "dev:build": "nuxi build playground", 21 | "prepare:playground": "nuxi prepare playground", 22 | "dev:prepare": "nuxt-module-build build --stub && nuxi prepare playground && pnpm run test:prepare-fixtures", 23 | "build:test": "cross-env NUXT_BUILD_TYPE=stub pnpm run prepack && pnpm run dev:build", 24 | "test:prepare-fixtures": "nuxi prepare test/fixtures/simple && nuxi prepare test/fixtures/withOptions && nuxi prepare test/fixtures/complex", 25 | "test:fixtures": "vitest run --dir test", 26 | "test:types": "pnpm run typecheck && pnpm run test:vue", 27 | "test:vue": "vue-tsc -p test/fixtures/simple/tsconfig.json --noEmit && vue-tsc -p test/fixtures/complex/tsconfig.json --noEmit", 28 | "test": "pnpm run dev:prepare && pnpm run test:types && pnpm run test:fixtures", 29 | "lint": "eslint --ext .ts --ext .vue .", 30 | "docs:dev": "cd docs && pnpm run dev", 31 | "docs:prepare": "nuxt-module-build --stub && nuxi prepare playground", 32 | "docs:build": "npm run docs:prepare && cd docs && nuxi generate", 33 | "typecheck": "tsc --noEmit", 34 | "release": "bumpp && npm publish && git push --follow-tags" 35 | }, 36 | "publishConfig": { 37 | "access": "public" 38 | }, 39 | "keywords": [ 40 | "nuxt typed router", 41 | "nuxt router", 42 | "nuxt typed", 43 | "nuxt safe router", 44 | "nuxt typed routes", 45 | "nuxt generate route ts", 46 | "nuxt 3", 47 | "nuxt 3 router" 48 | ], 49 | "homepage": "https://nuxt-typed-router.vercel.app/", 50 | "repository": { 51 | "type": "git", 52 | "url": "git+https://github.com/victorgarciaesgi/nuxt-typed-router.git" 53 | }, 54 | "author": { 55 | "name": "Victor Garcia", 56 | "url": "https://github.com/victorgarciaesgi" 57 | }, 58 | "license": "MIT", 59 | "bugs": { 60 | "url": "https://github.com/victorgarciaesgi/nuxt-typed-router/issues" 61 | }, 62 | "peerDependencies": { 63 | "prettier": "^2.5.x || 3.x" 64 | }, 65 | "dependencies": { 66 | "@nuxt/kit": "3.17.5", 67 | "chalk": "5.4.1", 68 | "defu": "6.1.4", 69 | "lodash-es": "4.17.21", 70 | "log-symbols": "7.0.1", 71 | "mkdirp": "3.0.1", 72 | "nanoid": "5.1.5", 73 | "pathe": "2.0.3", 74 | "prettier": "3.5.3" 75 | }, 76 | "devDependencies": { 77 | "@intlify/core-base": "11.1.5", 78 | "@intlify/message-compiler": "11.1.5", 79 | "@intlify/shared": "11.1.5", 80 | "@intlify/vue-i18n-bridge": "1.1.0", 81 | "@intlify/vue-router-bridge": "1.1.0", 82 | "@nuxt/content": "2.13.4", 83 | "@nuxt/devtools": "2.5.0", 84 | "@nuxt/module-builder": "1.0.1", 85 | "@nuxt/schema": "3.17.5", 86 | "@nuxt/test-utils": "3.19.1", 87 | "@nuxt/types": "2.18.1", 88 | "@nuxtjs/eslint-config-typescript": "12.1.0", 89 | "@nuxtjs/i18n": "9.5.5", 90 | "@nuxtjs/web-vitals": "0.2.7", 91 | "@playwright/test": "1.52.0", 92 | "@types/lodash-es": "4.17.12", 93 | "@types/node": "22.15.29", 94 | "@typescript-eslint/eslint-plugin": "8.33.1", 95 | "@typescript-eslint/parser": "8.33.1", 96 | "@vue/test-utils": "2.4.6", 97 | "bumpp": "10.1.1", 98 | "changelogithub": "13.15.0", 99 | "cross-env": "7.0.3", 100 | "eslint": "9.28.0", 101 | "eslint-config-prettier": "10.1.5", 102 | "eslint-plugin-vue": "10.1.0", 103 | "nuxt": "3.17.5", 104 | "nuxt-seo-kit": "1.3.13", 105 | "playwright": "1.52.0", 106 | "tsd": "0.32.0", 107 | "typescript": "5.8.3", 108 | "vitest": "3.2.1", 109 | "vue": "3.5.16", 110 | "vue-eslint-parser": "10.1.3", 111 | "vue-i18n": "11.1.5", 112 | "vue-router": "4.5.1", 113 | "vue-tsc": "2.2.10" 114 | } 115 | } -------------------------------------------------------------------------------- /playground/app/components/TestLink.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /playground/app/modules/test-module.ts: -------------------------------------------------------------------------------- 1 | import { createResolver, defineNuxtModule, extendPages } from '@nuxt/kit'; 2 | 3 | export default defineNuxtModule({ 4 | setup() { 5 | extendPages((routes) => { 6 | routes.push({ 7 | path: '/testModule/:foo', 8 | name: 'test-module', 9 | }); 10 | }); 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /playground/app/pages/[...404].vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /playground/app/pages/[lang]/post/[slug].vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /playground/app/pages/admin/[444]/[...languages].vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /playground/app/pages/admin/[444]/index.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /playground/app/pages/admin/profil.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /playground/app/pages/catch-[...all].vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /playground/app/pages/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 80 | -------------------------------------------------------------------------------- /playground/app/pages/index/index.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /playground/app/pages/test/[foo].vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /playground/app/pages/user/[id].vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /playground/app/pages/user/[id]/[slug].vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /playground/app/pages/user/[id]/[slug]/[foo]-test-[[bar]].vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /playground/app/pages/user/[id]/[slug]/articles.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /playground/app/pages/user/[id]/[slug]/index.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /playground/app/pages/user/[id]/index.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /playground/app/pages/user/[id]/posts.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /playground/app/pages/user/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /playground/app/pages/user/test-[[optional]].vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /playground/app/store/index.ts: -------------------------------------------------------------------------------- 1 | import { useRouter, navigateTo } from '@typed-router'; 2 | 3 | export async function callOutsideComponent() { 4 | // const { $typedRouter, $routesList, $router } = useNuxtApp(); 5 | // console.log($typedRouter, $routesList, $router); 6 | const router = useRouter(); 7 | router.push({ name: 'user' }); 8 | 9 | const route = await navigateTo({ name: 'user-id-slug', params: { slug: 'foo' } }); 10 | if (route instanceof Error) { 11 | // 12 | } else if (route) { 13 | console.log(route.name); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /playground/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import TestModuleRoute from './app/modules/test-module'; 2 | 3 | export default defineNuxtConfig({ 4 | modules: ['nuxt-typed-router', '@nuxtjs/i18n', TestModuleRoute, '@nuxt/content'], 5 | future: { 6 | compatibilityVersion: 4, 7 | }, 8 | devtools: { 9 | enabled: true, 10 | }, 11 | nuxtTypedRouter: { 12 | plugin: true, 13 | pathCheck: true, 14 | disablePrettier: false, 15 | removeNuxtDefs: true, 16 | ignoreRoutes: ['[...404].vue'], 17 | }, 18 | content: { 19 | documentDriven: false, 20 | }, 21 | hooks: { 22 | 'pages:extend': (pages) => { 23 | pages.push({ 24 | name: 'foo', 25 | path: '/foo', 26 | }); 27 | }, 28 | }, 29 | i18n: { 30 | defaultLocale: 'de', 31 | // dynamicRouteParams: true, 32 | locales: [ 33 | { 34 | code: 'en', 35 | iso: 'en-US', 36 | }, 37 | { 38 | code: 'de', 39 | iso: 'de-DE', 40 | }, 41 | { 42 | code: 'zh', 43 | iso: 'zh-CN', 44 | }, 45 | ], 46 | }, 47 | compatibilityDate: '2025-01-07', 48 | }); 49 | -------------------------------------------------------------------------------- /playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - test/fixtures/simple 3 | - test/fixtures/complex 4 | - test/fixtures/withOptions 5 | - docs -------------------------------------------------------------------------------- /src/core/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './moduleOptions'; 2 | -------------------------------------------------------------------------------- /src/core/config/moduleOptions.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { defu } from 'defu'; 3 | import type { NuxtI18nOptions } from '@nuxtjs/i18n'; 4 | import type { ModuleOptions, StrictOptions } from '../../types'; 5 | import logSymbols from 'log-symbols'; 6 | interface CustomNuxtConfigOptions { 7 | autoImport?: boolean; 8 | rootDir?: string; 9 | buildDir?: string; 10 | srcDir?: string; 11 | i18n?: boolean; 12 | i18nOptions?: NuxtI18nOptions | null; 13 | isDocumentDriven?: boolean; 14 | } 15 | 16 | class ModuleOptionsStore { 17 | plugin: boolean = false; 18 | strict: boolean | StrictOptions = false; 19 | pathCheck: boolean = true; 20 | disablePrettier: boolean = false; 21 | autoImport: boolean = false; 22 | rootDir: string = ''; 23 | buildDir: string = ''; 24 | srcDir: string = ''; 25 | pagesDir: string = ''; 26 | i18n: boolean = false; 27 | i18nOptions: NuxtI18nOptions | null = null; 28 | i18nLocales: string[] = []; 29 | ignoreRoutes: string[] = []; 30 | 31 | updateOptions(options: ModuleOptions & CustomNuxtConfigOptions) { 32 | if (options.plugin != null) this.plugin = options.plugin; 33 | if (options.strict != null) this.strict = options.strict; 34 | if (options.autoImport != null) this.autoImport = options.autoImport; 35 | if (options.rootDir != null) this.rootDir = options.rootDir; 36 | if (options.srcDir != null) this.srcDir = options.srcDir; 37 | if (options.buildDir != null) this.buildDir = options.buildDir; 38 | this.pagesDir = path.join(this.srcDir, 'pages'); 39 | if (options.i18n != null) this.i18n = options.i18n; 40 | if (options.i18nOptions != null) { 41 | this.i18nOptions = defu(options.i18nOptions, { 42 | strategy: 'prefix_except_default', 43 | } satisfies Partial); 44 | if ( 45 | this.i18nOptions.strategy === 'prefix_except_default' && 46 | !options.i18nOptions.defaultLocale 47 | ) { 48 | console.error( 49 | logSymbols.error, 50 | "You have not set 'i18n.defaultLocale', it's required when using 'prefix_except_default' mode (default one)" 51 | ); 52 | } 53 | if (options.i18nOptions.locales) { 54 | this.i18nLocales = options.i18nOptions.locales.map((l) => { 55 | if (typeof l === 'string') { 56 | return l; 57 | } else { 58 | return l.code; 59 | } 60 | }); 61 | } 62 | } 63 | if (options.pathCheck != null) { 64 | this.pathCheck = options.pathCheck; 65 | } 66 | if (options.ignoreRoutes) { 67 | this.ignoreRoutes = options.ignoreRoutes; 68 | } 69 | 70 | if (options.isDocumentDriven) { 71 | this.ignoreRoutes.push('[...slug].vue'); 72 | } 73 | } 74 | 75 | get resolvedIgnoredRoutes(): string[] { 76 | return this.ignoreRoutes.map((file) => path.join(this.pagesDir, file)); 77 | } 78 | 79 | getResolvedStrictOptions(): Required { 80 | let resolved: Required; 81 | if (typeof this.strict === 'boolean') { 82 | if (this.strict) { 83 | resolved = { 84 | NuxtLink: { 85 | strictRouteLocation: true, 86 | strictToArgument: true, 87 | }, 88 | router: { 89 | strictRouteLocation: true, 90 | strictToArgument: true, 91 | }, 92 | }; 93 | } else { 94 | resolved = { 95 | NuxtLink: { 96 | strictRouteLocation: false, 97 | strictToArgument: false, 98 | }, 99 | router: { 100 | strictRouteLocation: false, 101 | strictToArgument: false, 102 | }, 103 | }; 104 | } 105 | } else { 106 | resolved = defu(this.strict, { 107 | NuxtLink: { 108 | strictRouteLocation: false, 109 | strictToArgument: false, 110 | }, 111 | router: { 112 | strictRouteLocation: false, 113 | strictToArgument: false, 114 | }, 115 | } satisfies Required); 116 | } 117 | 118 | return resolved; 119 | } 120 | } 121 | 122 | export const moduleOptionStore = new ModuleOptionsStore(); 123 | -------------------------------------------------------------------------------- /src/core/fs/index.ts: -------------------------------------------------------------------------------- 1 | export * from './prettierFormat'; 2 | export * from './writeFile'; 3 | -------------------------------------------------------------------------------- /src/core/fs/prettierFormat.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import logSymbols from 'log-symbols'; 3 | import prettier from 'prettier'; 4 | 5 | const defaultPrettierOptions = { 6 | printWidth: 100, 7 | tabWidth: 2, 8 | trailingComma: 'es5', 9 | singleQuote: true, 10 | semi: true, 11 | bracketSpacing: true, 12 | htmlWhitespaceSensitivity: 'strict', 13 | } as const; 14 | 15 | export async function formatOutputWithPrettier(template: string): Promise { 16 | try { 17 | let prettierFoundOptions = await prettier.resolveConfig(process.cwd()); 18 | 19 | if (!prettierFoundOptions) { 20 | prettierFoundOptions = defaultPrettierOptions; 21 | } 22 | // const formatedTemplate = template; 23 | 24 | const formatedTemplate = prettier.format(template, { 25 | ...prettierFoundOptions, 26 | parser: 'typescript', 27 | }); 28 | 29 | return formatedTemplate; 30 | } catch (e) { 31 | console.error(logSymbols.error, chalk.red('Error while formatting the output'), '\n' + e); 32 | return Promise.reject(e); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/core/fs/writeFile.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { fileURLToPath } from 'url'; 3 | import { resolve, dirname } from 'pathe'; 4 | import logSymbols from 'log-symbols'; 5 | import chalk from 'chalk'; 6 | import { mkdirp } from 'mkdirp'; 7 | import { moduleOptionStore } from '../config'; 8 | import { formatOutputWithPrettier } from './prettierFormat'; 9 | 10 | export const __dirname = dirname(fileURLToPath(import.meta.url)); 11 | 12 | type ProcessPathAndWriteFileArgs = { 13 | fileName: string; 14 | content: string; 15 | outDir?: string; 16 | }; 17 | 18 | export async function processPathAndWriteFile({ 19 | content, 20 | fileName, 21 | outDir, 22 | }: ProcessPathAndWriteFileArgs): Promise { 23 | try { 24 | const { rootDir, disablePrettier } = moduleOptionStore; 25 | 26 | const finalOutDir = outDir ?? `.nuxt/typed-router`; 27 | const processedOutDir = resolve(rootDir, finalOutDir); 28 | const outputFile = resolve(process.cwd(), `${processedOutDir}/${fileName}`); 29 | const formatedContent = disablePrettier ? content : await formatOutputWithPrettier(content); 30 | 31 | if (fs.existsSync(outputFile)) { 32 | await writeFile(outputFile, formatedContent); 33 | } else { 34 | let dirList = outputFile.split('/'); 35 | dirList.pop(); 36 | const dirPath = dirList.join('/'); 37 | await mkdirp(dirPath); 38 | await writeFile(outputFile, formatedContent); 39 | } 40 | } catch (e) { 41 | return Promise.reject(e); 42 | } 43 | } 44 | 45 | async function writeFile(path: string, content: string): Promise { 46 | try { 47 | await fs.writeFileSync(path, content); 48 | } catch (e) { 49 | console.log(logSymbols.error, chalk.red(`Error while saving file at ${path}`, e)); 50 | return Promise.reject(e); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/core/index.ts: -------------------------------------------------------------------------------- 1 | import { extendPages } from '@nuxt/kit'; 2 | import type { Nuxt, NuxtPage } from '@nuxt/schema'; 3 | import chalk from 'chalk'; 4 | import logSymbols from 'log-symbols'; 5 | import { moduleOptionStore } from './config'; 6 | import { handleAddPlugin, saveGeneratedFiles } from './output'; 7 | import { constructRouteMap } from './parser'; 8 | 9 | type CreateTypedRouterArgs = { 10 | nuxt: Nuxt; 11 | routesConfig?: NuxtPage[]; 12 | isHookCall?: boolean; 13 | }; 14 | 15 | let hasLoggedNoPages = false; 16 | let hasRoutesDefined = false; 17 | 18 | export async function createTypedRouter({ 19 | nuxt, 20 | routesConfig, 21 | isHookCall = false, 22 | }: CreateTypedRouterArgs): Promise { 23 | try { 24 | const rootDir = nuxt.options.rootDir; 25 | const srcDir = nuxt.options.srcDir; 26 | const autoImport = nuxt.options.imports.autoImport ?? true; 27 | moduleOptionStore.updateOptions({ rootDir, autoImport, srcDir }); 28 | 29 | if (!isHookCall) { 30 | // Allow to collect custom routes added by config or module before generating it 31 | if (routesConfig) { 32 | await nuxt.callHook('pages:extend', routesConfig); 33 | return; 34 | } 35 | nuxt.hook('pages:extend', (routesConfig) => { 36 | createTypedRouter({ nuxt, routesConfig, isHookCall: true }); 37 | }); 38 | nuxt.hook('modules:done', () => { 39 | createTypedRouter({ nuxt, isHookCall: true }); 40 | }); 41 | if (moduleOptionStore.plugin) { 42 | await handleAddPlugin(); 43 | } 44 | return; 45 | } 46 | 47 | // We use extendPages here to access the NuxtPage, not accessible in the `pages:extend` hook 48 | extendPages(async (routes: NuxtPage[]) => { 49 | // console.log(JSON.stringify(routes)); 50 | hasRoutesDefined = true; 51 | const outputData = constructRouteMap(routes); 52 | 53 | await saveGeneratedFiles({ 54 | outputData, 55 | }); 56 | }); 57 | setTimeout(() => { 58 | if (!hasRoutesDefined && !hasLoggedNoPages) { 59 | hasLoggedNoPages = true; 60 | console.log( 61 | logSymbols.warning, 62 | chalk.yellow( 63 | `🚦 No routes defined. Check if your ${chalk.underline( 64 | chalk.bold('pages') 65 | )} folder exists` 66 | ) 67 | ); 68 | } 69 | }, 3000); 70 | } catch (e) { 71 | console.error(chalk.red('Error while generating routes definitions model'), '\n' + e); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/core/output/fileSave/definitions.save.ts: -------------------------------------------------------------------------------- 1 | import logSymbols from 'log-symbols'; 2 | import type { GeneratorOutput } from '../../../types'; 3 | import { moduleOptionStore } from '../../config'; 4 | import { processPathAndWriteFile } from '../../fs'; 5 | import { 6 | createDefinePageMetaFile, 7 | createHelpersFile, 8 | createIndexFile, 9 | createNavigateToFile, 10 | createNuxtLinkLocaleDefinitionFile, 11 | createPathsFiles, 12 | createRoutesTypesFile, 13 | createTypeUtilsRuntimeFile, 14 | createTypedRouterDefinitionFile, 15 | createTypedRouterFile, 16 | createUseTypedLinkFile, 17 | createUseTypedRouteFile, 18 | createUseTypedRouterFile, 19 | createi18nRouterFile, 20 | } from '../generators/files'; 21 | 22 | import { watermarkTemplate } from '../static'; 23 | 24 | let previousGeneratedRoutes = ''; 25 | let firstRun = false; 26 | 27 | type SaveGeneratedFiles = { 28 | outputData: GeneratorOutput; 29 | }; 30 | 31 | export async function saveGeneratedFiles({ outputData }: SaveGeneratedFiles): Promise { 32 | const { i18n } = moduleOptionStore; 33 | const filesMap: Array<{ fileName: string; content: string }> = [ 34 | { 35 | fileName: '__useTypedRouter.ts', 36 | content: createUseTypedRouterFile(), 37 | }, 38 | { 39 | fileName: '__useTypedRoute.ts', 40 | content: createUseTypedRouteFile(), 41 | }, 42 | { 43 | fileName: '__useTypedLink.ts', 44 | content: createUseTypedLinkFile(), 45 | }, 46 | { 47 | fileName: '__paths.d.ts', 48 | content: createPathsFiles(outputData), 49 | }, 50 | { 51 | fileName: `__routes.ts`, 52 | content: createRoutesTypesFile(outputData), 53 | }, 54 | { 55 | fileName: `__helpers.ts`, 56 | content: createHelpersFile(), 57 | }, 58 | { 59 | fileName: '__navigateTo.ts', 60 | content: createNavigateToFile(), 61 | }, 62 | { 63 | fileName: '__definePageMeta.ts', 64 | content: createDefinePageMetaFile(), 65 | }, 66 | { 67 | fileName: `__router.d.ts`, 68 | content: createTypedRouterFile(), 69 | }, 70 | { 71 | fileName: `__types_utils.d.ts`, 72 | content: createTypeUtilsRuntimeFile(), 73 | }, 74 | { 75 | fileName: `typed-router.d.ts`, 76 | content: createTypedRouterDefinitionFile(), 77 | }, 78 | { 79 | fileName: 'index.ts', 80 | content: createIndexFile(), 81 | }, 82 | ]; 83 | 84 | if (i18n) { 85 | filesMap.push({ 86 | fileName: '__i18n-router.ts', 87 | content: createi18nRouterFile(), 88 | }); 89 | filesMap.push({ 90 | fileName: '__NuxtLinkLocale.ts', 91 | content: createNuxtLinkLocaleDefinitionFile(), 92 | }); 93 | } 94 | 95 | await Promise.all( 96 | filesMap.map(({ content, fileName }) => { 97 | const waterMakeredContent = ` 98 | ${watermarkTemplate} 99 | 100 | ${content} 101 | `; 102 | return processPathAndWriteFile({ content: waterMakeredContent, fileName }); 103 | }) 104 | ); 105 | if (previousGeneratedRoutes !== outputData.routesList.join(',')) { 106 | previousGeneratedRoutes = outputData.routesList.join(','); 107 | console.log(logSymbols.success, `Router autocompletions generated 🚦`); 108 | if (!firstRun) { 109 | firstRun = true; 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/core/output/fileSave/index.ts: -------------------------------------------------------------------------------- 1 | export * from './plugin.save'; 2 | export * from './definitions.save'; 3 | -------------------------------------------------------------------------------- /src/core/output/fileSave/plugin.save.ts: -------------------------------------------------------------------------------- 1 | import { addPluginTemplate } from '@nuxt/kit'; 2 | import { createPluginFile } from '../generators'; 3 | 4 | export async function handleAddPlugin() { 5 | const pluginName = '__typed-router.plugin.ts'; 6 | 7 | addPluginTemplate({ 8 | filename: pluginName, 9 | getContents: createPluginFile, 10 | mode: 'all', 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /src/core/output/generators/blocks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './routes'; 2 | -------------------------------------------------------------------------------- /src/core/output/generators/blocks/routes/index.ts: -------------------------------------------------------------------------------- 1 | export * from './routes-names-list.block'; 2 | export * from './routes-params-record.block'; 3 | export * from './routes-named-locations.block'; 4 | export * from './routes-named-locations-resolved.block'; 5 | export * from './routes-params-record-resolved.block'; 6 | export * from './routes-paths.block'; 7 | -------------------------------------------------------------------------------- /src/core/output/generators/blocks/routes/routes-named-locations-resolved.block.ts: -------------------------------------------------------------------------------- 1 | import type { RouteParamsDecl } from '../../../../../types'; 2 | 3 | export function createRoutesNamedLocationsResolvedExport(routesParams: RouteParamsDecl[]): string { 4 | return ` 5 | /** 6 | * Type returned by a resolved Route that will allow to type guard the route name. 7 | * By default the params are unknown 8 | * */ 9 | export type RoutesNamedLocationsResolved = 10 | { 11 | name: RoutesNamesList; 12 | params: RoutesParamsRecord[keyof RoutesParamsRecord]; 13 | } ${ 14 | routesParams.length 15 | ? `& ( 16 | ${routesParams 17 | .map( 18 | ({ name, params }) => 19 | `{name: "${name}" ${ 20 | params.length 21 | ? `, params: { 22 | ${params 23 | .map( 24 | ({ key, notRequiredOnPage, catchAll }) => 25 | `"${key}"${notRequiredOnPage ? '?' : ''}: string${catchAll ? '[]' : ''}` 26 | ) 27 | .join(',\n')} 28 | }` 29 | : '' 30 | }}` 31 | ) 32 | .join('|\n')} 33 | )` 34 | : '' 35 | } 36 | `; 37 | } 38 | -------------------------------------------------------------------------------- /src/core/output/generators/blocks/routes/routes-named-locations.block.ts: -------------------------------------------------------------------------------- 1 | import type { RouteParamsDecl } from '../../../../../types'; 2 | 3 | export function createRoutesNamedLocationsExport(routesParams: RouteParamsDecl[]): string { 4 | return ` 5 | /** 6 | * Discriminated union that will allow to infer params based on route name 7 | * It's used for programmatic navigation like router.push or 8 | * */ 9 | export type RoutesNamedLocations = 10 | ${ 11 | routesParams.length 12 | ? routesParams 13 | .map( 14 | ({ name, params }) => 15 | `{name: "${name}" ${ 16 | params.length 17 | ? `, params${params.some((s) => s.required) ? '' : '?'}: { 18 | ${params 19 | .map( 20 | ({ key, required, catchAll }) => 21 | `"${key}"${required ? '' : '?'}: (string | number)${catchAll ? '[]' : ''}` 22 | ) 23 | .join(',\n')} 24 | }` 25 | : '' 26 | }}` 27 | ) 28 | .join('|\n') 29 | : "''" 30 | } 31 | `; 32 | } 33 | -------------------------------------------------------------------------------- /src/core/output/generators/blocks/routes/routes-names-list.block.ts: -------------------------------------------------------------------------------- 1 | export function createRoutesNamesListExport(routesList: string[]): string { 2 | return ` 3 | /** 4 | * Exhaustive list of all the available route names in the app 5 | * */ 6 | export type RoutesNamesList = ${ 7 | routesList.length ? routesList.map((m) => `'${m}'`).join('|\n') : '""' 8 | }`; 9 | } 10 | -------------------------------------------------------------------------------- /src/core/output/generators/blocks/routes/routes-params-record-resolved.block.ts: -------------------------------------------------------------------------------- 1 | import type { RouteParamsDecl } from '../../../../../types'; 2 | 3 | export function createRoutesParamsRecordResolvedExport(routesParams: RouteParamsDecl[]): string { 4 | return ` 5 | /** 6 | * Record resolved used for resolved routes 7 | * 8 | * */ 9 | export type RoutesParamsRecordResolved = { 10 | ${routesParams 11 | .map( 12 | ({ name, params }) => 13 | `"${name}": ${ 14 | params.length 15 | ? `{ 16 | ${params 17 | .map( 18 | ({ key, notRequiredOnPage, catchAll }) => 19 | `"${key}"${notRequiredOnPage ? '?' : ''}: string${catchAll ? '[]' : ''}` 20 | ) 21 | .join(',\n')} 22 | }` 23 | : 'never' 24 | }` 25 | ) 26 | .join(',\n')} 27 | }`; 28 | } 29 | -------------------------------------------------------------------------------- /src/core/output/generators/blocks/routes/routes-params-record.block.ts: -------------------------------------------------------------------------------- 1 | import type { RouteParamsDecl } from '../../../../../types'; 2 | 3 | export function createRoutesParamsRecordExport(routesParams: RouteParamsDecl[]): string { 4 | return ` 5 | /** 6 | * Routes params are only required for the exact targeted route name, 7 | * vue-router behaviour allow to navigate between children routes without the need to provide all the params every time. 8 | * So we can't enforce params when navigating between routes, only a \`[xxx].vue\` page will have required params in the type definition 9 | * 10 | * */ 11 | export type RoutesParamsRecord = { 12 | ${routesParams 13 | .map( 14 | ({ name, params }) => 15 | `"${name}": ${ 16 | params.length 17 | ? `{ 18 | ${params 19 | .map( 20 | ({ key, required, catchAll }) => 21 | `"${key}"${required ? '' : '?'}: (string | number)${catchAll ? '[]' : ''}` 22 | ) 23 | .join(',\n')} 24 | }` 25 | : 'never' 26 | }` 27 | ) 28 | .join(',\n')} 29 | }`; 30 | } 31 | -------------------------------------------------------------------------------- /src/core/output/generators/files/__NuxtLinkLocale.file.ts: -------------------------------------------------------------------------------- 1 | import { moduleOptionStore } from '../../../config'; 2 | 3 | export function createNuxtLinkLocaleDefinitionFile(): string { 4 | const strictOptions = moduleOptionStore.getResolvedStrictOptions(); 5 | 6 | return /* typescript */ ` 7 | 8 | import type { NuxtLinkProps, PageMeta } from 'nuxt/app'; 9 | import NuxtLink from 'nuxt/dist/app/components/nuxt-link'; 10 | import type { RoutesNamedLocations, RoutesNamesListRecord, RoutesNamesList } from './__routes'; 11 | import type {TypedRouter, TypedRoute, TypedRouteLocationRawFromName, TypedLocationAsRelativeRaw} from './__router'; 12 | import type {NuxtLocaleRoute, I18nLocales} from './__i18n-router'; 13 | 14 | 15 | type TypedNuxtLinkLocaleProps< 16 | T extends RoutesNamesList, 17 | P extends string, 18 | E extends boolean = false> = Omit & 19 | { 20 | to: NuxtLocaleRoute, 21 | external?: E, 22 | locale?: E extends true ? never : I18nLocales 23 | } 24 | 25 | export type TypedNuxtLinkLocale = new (props: TypedNuxtLinkLocaleProps) => Omit< 26 | typeof NuxtLink, 27 | '$props' 28 | > & { 29 | $props: TypedNuxtLinkLocaleProps; 30 | }; 31 | 32 | `; 33 | } 34 | -------------------------------------------------------------------------------- /src/core/output/generators/files/__definePageMeta.file.ts: -------------------------------------------------------------------------------- 1 | import { returnIfTrue } from '../../../../utils'; 2 | import { moduleOptionStore } from '../../../config'; 3 | 4 | export function createDefinePageMetaFile(): string { 5 | const strictOptions = moduleOptionStore.getResolvedStrictOptions(); 6 | const { pathCheck } = moduleOptionStore; 7 | 8 | return /* typescript */ ` 9 | 10 | import { definePageMeta as defaultDefinePageMeta } from '#imports'; 11 | import type {PageMeta, NuxtError} from 'nuxt/app' 12 | import type {TypedRouteFromName, TypedRoute, TypedRouteLocationRawFromName, TypedRouteLocationRaw} from './__router'; 13 | import type {RoutesNamesList} from './__routes'; 14 | ${returnIfTrue(pathCheck, `import type {TypedPathParameter} from './__paths';`)} 15 | 16 | type FilteredPageMeta = { 17 | [T in keyof PageMeta as [unknown] extends [PageMeta[T]] ? never : T]: PageMeta[T]; 18 | } 19 | 20 | export type TypedPageMeta = Omit & { 21 | /** 22 | * Validate whether a given route can validly be rendered with this page. 23 | * 24 | * Return true if it is valid, or false if not. If another match can't be found, 25 | * this will mean a 404. You can also directly return an object with 26 | * statusCode/statusMessage to respond immediately with an error (other matches 27 | * will not be checked). 28 | */ 29 | validate?: (route: TypedRoute) => boolean | Promise | Partial | Promise>; 30 | key?: false | string | ((route: TypedRoute) => string); 31 | /** Allow types augmented by other modules */ 32 | [key: string]: any; 33 | } 34 | 35 | 36 | /** 37 | * Typed clone of \`definePageMeta\` 38 | * 39 | * ⚠️ Types for the redirect function may be buggy or not display autocomplete 40 | * Use \`helpers.route\` or \`helpers.path\` to provide autocomplete. 41 | * 42 | * \`\`\`ts 43 | * import {helpers} from '@typed-router'; 44 | * definePageMeta({ 45 | * redirect(route) { 46 | * return helpers.path('/foo') 47 | * } 48 | * }); 49 | * \`\`\` 50 | * @exemple 51 | * 52 | * \`\`\`ts 53 | * definePageMeta({ 54 | * validate(route) { 55 | * }); 56 | * \`\`\` 57 | */ 58 | export function definePageMeta

( 59 | meta: TypedPageMeta & { redirect: TypedRouteLocationRawFromName } 60 | ): void; 61 | ${returnIfTrue( 62 | pathCheck && !strictOptions.router.strictToArgument, 63 | `export function definePageMeta

( 64 | meta: TypedPageMeta & { redirect: TypedPathParameter

} 65 | ): void;` 66 | )} 67 | export function definePageMeta

( 68 | meta: TypedPageMeta & { 69 | redirect?: (to: TypedRoute) => TypedRouteLocationRaw

${returnIfTrue( 70 | pathCheck && !strictOptions.router.strictToArgument, 71 | ` | TypedPathParameter

` 72 | )}; 73 | } 74 | ): void; 75 | export function definePageMeta

( 76 | meta: TypedPageMeta & { 77 | redirect?: () => TypedRouteLocationRaw

${returnIfTrue( 78 | pathCheck && !strictOptions.router.strictToArgument, 79 | ` | TypedPathParameter

` 80 | )}; 81 | } 82 | ): void; 83 | export function definePageMeta(meta?: TypedPageMeta): void { 84 | return defaultDefinePageMeta(meta); 85 | } 86 | 87 | `; 88 | } 89 | -------------------------------------------------------------------------------- /src/core/output/generators/files/__helpers.file.ts: -------------------------------------------------------------------------------- 1 | import { returnIfFalse, returnIfTrue } from '../../../../utils'; 2 | import { moduleOptionStore } from '../../../config'; 3 | 4 | export function createHelpersFile() { 5 | const { pathCheck } = moduleOptionStore; 6 | 7 | return /* typescript */ ` 8 | 9 | 10 | 11 | import type { RouteLocationRaw } from 'vue-router'; 12 | import type { TypedRouteLocationRawFromName, TypedLocationAsRelativeRaw } from './__router'; 13 | import type { RoutesNamesList } from './__routes'; 14 | ${returnIfTrue( 15 | pathCheck, 16 | `import type {TypedPathParameter, RouteNameFromPath} from './__paths';` 17 | )} 18 | 19 | export const helpers = { 20 | route( 21 | to: TypedRouteLocationRawFromName 22 | ): [T] extends [never] 23 | ? string 24 | : Required< 25 | Omit, 'name' | 'params' | 'path'> & TypedLocationAsRelativeRaw 26 | > { 27 | return to as any; 28 | }, 29 | path( 30 | to: TypedPathParameter 31 | ): [T] extends [never] 32 | ? string 33 | : Required, T>> { 34 | return to as any; 35 | }, 36 | }; 37 | 38 | 39 | `; 40 | } 41 | -------------------------------------------------------------------------------- /src/core/output/generators/files/__i18n-router.file.ts: -------------------------------------------------------------------------------- 1 | import { returnIfTrue } from '../../../../utils'; 2 | import { moduleOptionStore } from '../../../config'; 3 | 4 | export function createi18nRouterFile() { 5 | const { router, NuxtLink } = moduleOptionStore.getResolvedStrictOptions(); 6 | const { i18nOptions, pathCheck, i18nLocales } = moduleOptionStore; 7 | 8 | const LocalePathType = 9 | i18nOptions?.strategy === 'no_prefix' ? 'TypedPathParameter' : 'TypedLocalePathParameter'; 10 | 11 | return /* typescript */ ` 12 | import type { RouteLocationRaw } from 'vue-router'; 13 | import { useLocalePath as _useLocalePath, useLocaleRoute as _useLocaleRoute} from '#imports'; 14 | import type {TypedRouteLocationRawFromName, TypedLocationAsRelativeRaw, TypedRouteFromName} from './__router'; 15 | import type {RoutesNamesList} from './__routes'; 16 | ${returnIfTrue( 17 | pathCheck, 18 | `import type {TypedLocalePathParameter, TypedPathParameter, RouteNameFromLocalePath} from './__paths';` 19 | )} 20 | 21 | export type I18nLocales = ${ 22 | i18nLocales?.length ? i18nLocales.map((loc) => `"${loc}"`).join('|') : 'string' 23 | }; 24 | 25 | 26 | export type NuxtLocaleRoute = 27 | | TypedRouteLocationRawFromName 28 | ${returnIfTrue(!pathCheck && !NuxtLink.strictToArgument, ` | string`)} 29 | ${returnIfTrue(pathCheck && NuxtLink.strictToArgument, ` | (E extends true ? string : never)`)} 30 | ${returnIfTrue( 31 | pathCheck && !NuxtLink.strictToArgument, 32 | ` | (E extends true ? string : ${LocalePathType}

)` 33 | )} 34 | 35 | export interface TypedToLocalePath { 36 | ( 37 | to: TypedRouteLocationRawFromName, 38 | locale?: I18nLocales | undefined 39 | ) : [T] extends [never] ? string : Required< 40 | (Omit, 'name' | 'params' | 'path'> & TypedLocationAsRelativeRaw) 41 | > 42 | ${returnIfTrue( 43 | pathCheck && !router.strictToArgument, 44 | `( 45 | to: ${LocalePathType}, 46 | locale?: I18nLocales | undefined 47 | ) : [T] extends [never] ? string : Required, T>>;` 48 | )} 49 | } 50 | 51 | export function useLocalePath(): TypedToLocalePath { 52 | return _useLocalePath() as any; 53 | } 54 | 55 | export interface TypedLocaleRoute { 56 | (to: TypedRouteLocationRawFromName, locale?: I18nLocales | undefined) : TypedRouteFromName 57 | ${returnIfTrue( 58 | pathCheck && !router.strictToArgument, 59 | ` (to: ${LocalePathType}, locale?: I18nLocales | undefined) : TypedRouteFromName>;` 60 | )} 61 | } 62 | 63 | 64 | export function useLocaleRoute(): TypedLocaleRoute { 65 | return _useLocaleRoute() as any; 66 | } 67 | 68 | `; 69 | } 70 | -------------------------------------------------------------------------------- /src/core/output/generators/files/__navigateTo.file.ts: -------------------------------------------------------------------------------- 1 | import { returnIfTrue } from '../../../../utils'; 2 | import { moduleOptionStore } from '../../../config'; 3 | 4 | export function createNavigateToFile() { 5 | const { router } = moduleOptionStore.getResolvedStrictOptions(); 6 | const { pathCheck } = moduleOptionStore; 7 | return /* typescript */ ` 8 | import { navigateTo as defaultNavigateTo } from '#imports'; 9 | import type { NavigateToOptions } from 'nuxt/dist/app/composables/router'; 10 | import type { NavigationFailure } from 'vue-router'; 11 | import type { TypedRouteLocationRawFromName, TypedRouteFromName, TypedRoute } from './__router'; 12 | import type { RoutesNamesList } from './__routes'; 13 | ${returnIfTrue( 14 | pathCheck, 15 | `import type {TypedPathParameter, RouteNameFromPath} from './__paths';` 16 | )} 17 | 18 | type TypedNavigateToOptions = Omit & { 19 | external?: E 20 | } 21 | 22 | /** 23 | * Typed clone of \`navigateTo\` 24 | * 25 | * @exemple 26 | * 27 | * \`\`\`ts 28 | * const resolved = navigateTo({name: 'foo', params: {foo: 'bar'}}); 29 | * \`\`\` 30 | */ 31 | 32 | 33 | interface NavigateToFunction { 34 | ( 35 | to: TypedRouteLocationRawFromName, 36 | options?: TypedNavigateToOptions 37 | ) : Promise> 38 | ${returnIfTrue( 39 | pathCheck && !router.strictToArgument, 40 | `( 41 | to: (E extends true ? string : TypedPathParameter), 42 | options?: TypedNavigateToOptions 43 | ) : Promise>>` 44 | )} 45 | } 46 | 47 | export const navigateTo: NavigateToFunction = defaultNavigateTo as any; 48 | 49 | `; 50 | } 51 | -------------------------------------------------------------------------------- /src/core/output/generators/files/__paths.file.ts: -------------------------------------------------------------------------------- 1 | import { returnIfTrue } from '../../../../../src/utils'; 2 | import type { GeneratorOutput } from '../../../../types'; 3 | import { moduleOptionStore } from '../../../config'; 4 | import { destructurePath } from '../../../parser/params'; 5 | import { 6 | createLocaleRoutePathSchema, 7 | createRoutePathSchema, 8 | createValidatePathTypes, 9 | } from '../blocks'; 10 | 11 | export function createPathsFiles({ routesPaths, routesList }: GeneratorOutput) { 12 | const { i18n, i18nOptions } = moduleOptionStore; 13 | const hasPrefixStrategy = i18n && i18nOptions?.strategy !== 'no_prefix'; 14 | 15 | const filteredRoutesPaths = routesPaths 16 | .filter((route) => !routesPaths.find((r) => `${route.path}/` === r.path)) 17 | .map((route) => ({ 18 | ...route, 19 | path: route.path.replace(/\(\)/g, ''), 20 | })) 21 | .sort((a, b) => { 22 | const pathCountA = a.path.split('/'); 23 | const pathCountB = b.path.split('/'); 24 | pathCountA.splice(0, 1); 25 | pathCountB.splice(0, 1); 26 | const maxIndex = Math.max(pathCountA.length, pathCountB.length) - 1; 27 | 28 | let order = 0; 29 | let index = 0; 30 | let reason: string = ''; 31 | 32 | let alphabetOrder: number; 33 | let hasElement: number; 34 | let hasParam: number; 35 | let indexOfParam: number; 36 | do { 37 | alphabetOrder = pathCountA[index]?.localeCompare(pathCountB[index] ?? '') ?? 0; 38 | hasElement = (pathCountA[index] != null ? 1 : 0) - (pathCountB[index] != null ? 1 : 0); 39 | hasParam = 40 | (pathCountA[index]?.includes(':') ? 1 : 0) - (pathCountB[index]?.includes(':') ? 1 : 0); 41 | indexOfParam = 42 | (pathCountB[index]?.indexOf(':') ?? 0) - (pathCountA[index]?.indexOf(':') ?? 0); 43 | 44 | if (alphabetOrder !== 0 && index === 0) { 45 | order = alphabetOrder; 46 | reason = 'Alphabet-0'; 47 | break; 48 | } else { 49 | if (hasElement !== 0) { 50 | order = hasElement; 51 | reason = 'No element'; 52 | break; 53 | } else if (hasParam !== 0) { 54 | order = hasParam; 55 | reason = 'No param'; 56 | break; 57 | } else if (hasParam === 0 && indexOfParam !== 0) { 58 | order = indexOfParam; 59 | reason = 'Param index'; 60 | break; 61 | } else if (alphabetOrder !== 0) { 62 | order = alphabetOrder; 63 | reason = 'Alphabet'; 64 | break; 65 | } 66 | } 67 | index = index + 1; 68 | } while (index < maxIndex); 69 | // console.log(a.path, b.path, order, reason); 70 | return order; 71 | }); 72 | 73 | const pathElements = filteredRoutesPaths 74 | .filter((f) => f.path && f.path !== '/') 75 | .map((route) => { 76 | return route.path 77 | .split('/') 78 | .filter((f) => f.length) 79 | .map((m) => destructurePath(m, route)); 80 | }) 81 | .filter((f) => f.length); 82 | 83 | const validatePathTypes = createValidatePathTypes(pathElements, routesList); 84 | const validateLocalePathTypes = createValidatePathTypes(pathElements, routesList, true); 85 | 86 | return /* typescript */ ` 87 | 88 | ${createRoutePathSchema(filteredRoutesPaths)}; 89 | 90 | ${returnIfTrue(hasPrefixStrategy, createLocaleRoutePathSchema(filteredRoutesPaths))} 91 | 92 | type ValidStringPath = T extends \`\${string} \${string}\` ? false : T extends '' ? false : true; 93 | 94 | type ValidParam = T extends \`\${infer A}/\${infer B}\` 95 | ? A extends \`\${string} \${string}\` 96 | ? false 97 | : A extends \`?\${string}\` 98 | ? false 99 | : A extends \`\${string} \${string}\` 100 | ? false 101 | : A extends '' 102 | ? B extends '' 103 | ? true 104 | : false 105 | : B extends \`?\${string}\` 106 | ? false 107 | : B extends \`#\${string}\` 108 | ? true 109 | : B extends '' 110 | ? true 111 | : false 112 | : R extends true 113 | ? T extends '' 114 | ? false 115 | : ValidParam 116 | : T extends \`?\${string}\` 117 | ? false 118 | : T extends \`\${string} \${string}\` 119 | ? false 120 | : true; 121 | 122 | type ValidEndOfPath = T extends \`/\` 123 | ? true 124 | : T extends '' 125 | ? true 126 | : T extends \`\${string} \${string}\` 127 | ? false 128 | : T extends \`?\${string}\` 129 | ? true 130 | : T extends \`#\${string}\` 131 | ? true 132 | : false; 133 | 134 | ${validatePathTypes} 135 | ${returnIfTrue(hasPrefixStrategy, validateLocalePathTypes)} 136 | 137 | 138 | export type TypedPathParameter = ValidatePath | RoutePathSchema; 139 | ${returnIfTrue( 140 | hasPrefixStrategy, 141 | `export type TypedLocalePathParameter = ValidateLocalePath | LocaleRoutePathSchema;` 142 | )} 143 | 144 | `; 145 | } 146 | -------------------------------------------------------------------------------- /src/core/output/generators/files/__routes.file.ts: -------------------------------------------------------------------------------- 1 | import type { GeneratorOutput } from '../../../../types'; 2 | import { 3 | createRoutesNamedLocationsExport, 4 | createRoutesNamedLocationsResolvedExport, 5 | createRoutesNamesListExport, 6 | createRoutesParamsRecordExport, 7 | createRoutesParamsRecordResolvedExport, 8 | } from '../blocks'; 9 | 10 | export function createRoutesTypesFile({ 11 | routesList, 12 | routesObjectTemplate, 13 | routesDeclTemplate, 14 | routesParams, 15 | routesPaths, 16 | }: GeneratorOutput): string { 17 | return /* typescript */ ` 18 | ${createRoutesNamesListExport(routesList)} 19 | 20 | ${createRoutesParamsRecordExport(routesParams)} 21 | 22 | ${createRoutesParamsRecordResolvedExport(routesParams)} 23 | 24 | ${createRoutesNamedLocationsExport(routesParams)} 25 | 26 | ${createRoutesNamedLocationsResolvedExport(routesParams)} 27 | 28 | export type RoutesNamesListRecord = ${routesDeclTemplate}; 29 | 30 | export const routesNames = ${routesObjectTemplate}; 31 | `; 32 | } 33 | -------------------------------------------------------------------------------- /src/core/output/generators/files/__typed-router.d.file.ts: -------------------------------------------------------------------------------- 1 | import { returnIfTrue } from '../../../../utils'; 2 | import { moduleOptionStore } from '../../../config'; 3 | 4 | export function createTypedRouterDefinitionFile(): string { 5 | const strictOptions = moduleOptionStore.getResolvedStrictOptions(); 6 | const { plugin, autoImport, i18n, pathCheck } = moduleOptionStore; 7 | 8 | return /* typescript */ ` 9 | 10 | import type { NuxtLinkProps, PageMeta, NuxtApp } from 'nuxt/app'; 11 | import NuxtLink from 'nuxt/dist/app/components/nuxt-link'; 12 | import type { RouteLocationRaw, RouteLocationPathRaw, RouteLocation, RouterLinkProps, UseLinkReturn } from 'vue-router'; 13 | import type { RoutesNamedLocations, RoutesNamesListRecord, RoutesNamesList } from './__routes'; 14 | import type {TypedRouter, TypedRoute, TypedRouteLocationRawFromName, TypedLocationAsRelativeRaw, NuxtRoute} from './__router'; 15 | import { useRoute as _useRoute } from './__useTypedRoute'; 16 | import { useRouter as _useRouter } from './__useTypedRouter'; 17 | import { useLink as _useLink } from './__useTypedLink'; 18 | import { navigateTo as _navigateTo } from './__navigateTo'; 19 | import type { DefineSetupFnComponent, SlotsType, UnwrapRef, VNode } from 'vue'; 20 | 21 | ${returnIfTrue( 22 | i18n, 23 | `import { useLocalePath as _useLocalePath, useLocaleRoute as _useLocaleRoute} from './__i18n-router'; 24 | import type {TypedNuxtLinkLocale} from './__NuxtLinkLocale'` 25 | )} 26 | 27 | import {definePageMeta as _definePageMeta} from './__definePageMeta'; 28 | 29 | ${returnIfTrue(pathCheck, `import type {TypedPathParameter} from './__paths';`)} 30 | 31 | 32 | declare global { 33 | 34 | ${returnIfTrue( 35 | autoImport, 36 | /* typescript */ ` 37 | const useRoute: typeof _useRoute; 38 | const useRouter: typeof _useRouter; 39 | const useLink: typeof _useLink; 40 | const navigateTo: typeof _navigateTo; 41 | const definePageMeta: typeof _definePageMeta; 42 | 43 | ${returnIfTrue( 44 | i18n, 45 | /* typescript */ ` 46 | const useLocalePath: typeof _useLocalePath; 47 | const useLocaleRoute: typeof _useLocaleRoute; 48 | ` 49 | )} 50 | ` 51 | )} 52 | } 53 | 54 | type TypedNuxtLinkProps< 55 | T extends RoutesNamesList, 56 | P extends string, 57 | E extends boolean = false, 58 | CustomProp extends boolean = false> = Omit, 'to' | 'external'> & 59 | { 60 | to: NuxtRoute, 61 | external?: E 62 | } 63 | 64 | type NuxtLinkDefaultSlotProps = CustomProp extends true ? { 65 | href: string; 66 | navigate: (e?: MouseEvent) => Promise; 67 | prefetch: (nuxtApp?: NuxtApp) => Promise; 68 | route: (RouteLocation & { 69 | href: string; 70 | }) | undefined; 71 | rel: string | null; 72 | target: '_blank' | '_parent' | '_self' | '_top' | (string & {}) | null; 73 | isExternal: boolean; 74 | isActive: false; 75 | isExactActive: false; 76 | } : UnwrapRef; 77 | 78 | type NuxtLinkSlots = { 79 | default?: (props: NuxtLinkDefaultSlotProps) => VNode[]; 80 | }; 81 | 82 | 83 | 84 | export type TypedNuxtLink = (new (props: TypedNuxtLinkProps) => InstanceType, [], SlotsType>>>) & Record 85 | 86 | declare module 'vue' { 87 | interface GlobalComponents { 88 | NuxtLink: TypedNuxtLink; 89 | ${returnIfTrue(i18n, ` NuxtLinkLocale: TypedNuxtLinkLocale;`)} 90 | } 91 | } 92 | 93 | ${returnIfTrue( 94 | plugin, 95 | /* typescript */ ` 96 | interface CustomPluginProperties { 97 | $typedRouter: TypedRouter, 98 | $typedRoute: TypedRoute, 99 | $routesNames: RoutesNamesListRecord 100 | } 101 | declare module '#app' { 102 | interface NuxtApp extends CustomPluginProperties {} 103 | } 104 | declare module 'vue' { 105 | interface ComponentCustomProperties extends CustomPluginProperties {} 106 | } 107 | ` 108 | )} 109 | `; 110 | } 111 | -------------------------------------------------------------------------------- /src/core/output/generators/files/__types-utils.d.file.ts: -------------------------------------------------------------------------------- 1 | export function createTypeUtilsRuntimeFile() { 2 | return /* typescript */ ` 3 | 4 | import { RoutesNamesList, RoutesParamsRecord } from './__routes'; 5 | 6 | // - Type utils 7 | export type ExtractRequiredParameters> = Pick< 8 | T, 9 | { [K in keyof T]: undefined extends T[K] ? never : K }[keyof T] 10 | >; 11 | 12 | export type HasOneRequiredParameter = [RoutesParamsRecord[T]] extends [ 13 | never 14 | ] 15 | ? false 16 | : [keyof ExtractRequiredParameters] extends [undefined] 17 | ? false 18 | : true; 19 | `; 20 | } 21 | -------------------------------------------------------------------------------- /src/core/output/generators/files/__useTypedLink.file.ts: -------------------------------------------------------------------------------- 1 | import { returnIfTrue } from '../../../../utils'; 2 | import { moduleOptionStore } from '../../../config'; 3 | 4 | export function createUseTypedLinkFile(): string { 5 | const strictOptions = moduleOptionStore.getResolvedStrictOptions(); 6 | const { pathCheck } = moduleOptionStore; 7 | 8 | return /* typescript */ ` 9 | 10 | import { useLink as defaultLink } from '#imports'; 11 | import type {MaybeRef, Ref} from 'vue'; 12 | import type { NavigateToOptions } from 'nuxt/dist/app/composables/router'; 13 | import type { NavigationFailure } from 'vue-router'; 14 | import type { TypedRouteLocationRawFromName, TypedRouteFromName, TypedRoute } from './__router'; 15 | import type { RoutesNamesList } from './__routes'; 16 | ${returnIfTrue( 17 | pathCheck, 18 | `import type {TypedPathParameter, RouteNameFromPath} from './__paths';` 19 | )} 20 | 21 | 22 | type LinkedRoute = { 23 | route: ComputedRef & { 24 | href: string; 25 | }>; 26 | href: ComputedRef; 27 | isActive: ComputedRef; 28 | isExactActive: ComputedRef; 29 | navigate: (e?: MouseEvent) => Promise; 30 | }; 31 | 32 | 33 | interface UseLinkFunction { 34 | (props: { 35 | to: TypedRouteLocationRawFromName; 36 | replace?: MaybeRef; 37 | }): LinkedRoute; 38 | (props: { 39 | to: Ref>; 40 | replace?: MaybeRef; 41 | }): LinkedRoute; 42 | 43 | ${returnIfTrue( 44 | pathCheck && !strictOptions.router.strictToArgument, 45 | `

( 46 | props: { 47 | to: TypedPathParameter

, 48 | replace?: MaybeRef 49 | } 50 | ) : LinkedRoute> 51 |

(props: { 52 | to: Ref>; 53 | replace?: MaybeRef; 54 | }): LinkedRoute>; 55 | 56 | ` 57 | )} 58 | } 59 | 60 | /** 61 | * Typed clone of \`useLink\` 62 | * 63 | * @exemple 64 | * 65 | * \`\`\`ts 66 | * const router = useLink(props); 67 | * \`\`\` 68 | */ 69 | export const useLink: UseLinkFunction = defaultLink as any; 70 | 71 | `; 72 | } 73 | -------------------------------------------------------------------------------- /src/core/output/generators/files/__useTypedRoute.file.ts: -------------------------------------------------------------------------------- 1 | export function createUseTypedRouteFile(): string { 2 | return /* typescript */ ` 3 | import { useRoute as defaultRoute } from '#imports'; 4 | import type { RoutesNamesList } from './__routes'; 5 | import type {TypedRoute, TypedRouteFromName} from './__router' 6 | 7 | /** 8 | * Typed clone of \`useRoute\` 9 | * 10 | * @exemple 11 | * 12 | * \`\`\`ts 13 | * const route = useRoute(); 14 | * \`\`\` 15 | * 16 | * \`\`\`ts 17 | * const route = useRoute('my-route-with-param-id'); 18 | * route.params.id // autocompletes! 19 | * \`\`\` 20 | * 21 | * \`\`\`ts 22 | * const route = useRoute(); 23 | * if (route.name === 'my-route-with-param-id') { 24 | * route.params.id // autocompletes! 25 | * } 26 | * \`\`\` 27 | */ 28 | export function useRoute( 29 | name?: T 30 | ): [T] extends [never] ? TypedRoute : TypedRouteFromName { 31 | const route = defaultRoute(); 32 | 33 | return route as any; 34 | } 35 | `; 36 | } 37 | -------------------------------------------------------------------------------- /src/core/output/generators/files/__useTypedRouter.file.ts: -------------------------------------------------------------------------------- 1 | export function createUseTypedRouterFile(): string { 2 | return /* typescript */ ` 3 | 4 | import { useRouter as defaultRouter } from '#imports'; 5 | import type { TypedRouter } from './__router'; 6 | 7 | /** 8 | * Typed clone of \`useRouter\` 9 | * 10 | * @exemple 11 | * 12 | * \`\`\`ts 13 | * const router = useRouter(); 14 | * \`\`\` 15 | */ 16 | export function useRouter(): TypedRouter { 17 | const router = defaultRouter(); 18 | 19 | return router; 20 | }; 21 | 22 | `; 23 | } 24 | -------------------------------------------------------------------------------- /src/core/output/generators/files/index.file.ts: -------------------------------------------------------------------------------- 1 | import { returnIfTrue } from '../../../../utils'; 2 | import { moduleOptionStore } from '../../../config'; 3 | 4 | export function createIndexFile(): string { 5 | const { i18n, i18nOptions, pathCheck } = moduleOptionStore; 6 | const hasPrefixStrategy = i18n && i18nOptions?.strategy !== 'no_prefix'; 7 | 8 | return /* typescript */ ` 9 | 10 | export type { 11 | TypedLocationAsRelativeRaw, 12 | TypedResolvedMatcherLocation, 13 | TypedRoute, 14 | TypedRouteFromName, 15 | TypedRouteLocation, 16 | TypedRouteLocationFromName, 17 | TypedRouteLocationRaw, 18 | TypedRouteLocationRawFromName, 19 | TypedRouter, 20 | NuxtRoute 21 | } from './__router'; 22 | export { routesNames } from './__routes'; 23 | export type { 24 | RoutesNamedLocations, 25 | RoutesNamedLocationsResolved, 26 | RoutesNamesList, 27 | RoutesNamesListRecord, 28 | RoutesParamsRecord, 29 | } from './__routes'; 30 | export { useRoute } from './__useTypedRoute'; 31 | export { useRouter } from './__useTypedRouter'; 32 | export { useLink } from './__useTypedLink'; 33 | export { navigateTo } from './__navigateTo'; 34 | export { definePageMeta } from './__definePageMeta'; 35 | export { helpers } from './__helpers'; 36 | 37 | ${returnIfTrue( 38 | pathCheck, 39 | `export type { ValidatePath, RoutePathSchema, TypedPathParameter, RouteNameFromPath, ${returnIfTrue( 40 | hasPrefixStrategy, 41 | `TypedLocalePathParameter` 42 | )} } from './__paths';` 43 | )} 44 | ${returnIfTrue( 45 | i18n, 46 | `export {useLocalePath, useLocaleRoute} from './__i18n-router'; 47 | export type {TypedToLocalePath, TypedLocaleRoute, I18nLocales} from './__i18n-router';` 48 | )} 49 | 50 | 51 | `; 52 | } 53 | -------------------------------------------------------------------------------- /src/core/output/generators/files/index.ts: -------------------------------------------------------------------------------- 1 | export * from './__routes.file'; 2 | export * from './__router.d.file'; 3 | export * from './__typed-router.d.file'; 4 | export * from './index.file'; 5 | export * from './plugin.file'; 6 | export * from './__useTypedRoute.file'; 7 | export * from './__useTypedRouter.file'; 8 | export * from './__useTypedLink.file'; 9 | export * from './__navigateTo.file'; 10 | export * from './__types-utils.d.file'; 11 | export * from './__i18n-router.file'; 12 | export * from './__paths.file'; 13 | export * from './__definePageMeta.file'; 14 | export * from './__helpers.file'; 15 | export * from './__NuxtLinkLocale.file'; 16 | -------------------------------------------------------------------------------- /src/core/output/generators/files/plugin.file.ts: -------------------------------------------------------------------------------- 1 | export function createPluginFile(): string { 2 | return /* typescript */ ` 3 | 4 | import { defineNuxtPlugin, useRouter, useRoute } from '#imports'; 5 | import {TypedRouter, TypedRoute, routesNames} from '@typed-router'; 6 | 7 | export default defineNuxtPlugin(() => { 8 | const router = useRouter(); 9 | const route = useRoute(); 10 | 11 | return { 12 | provide: { 13 | typedRouter: router as TypedRouter, 14 | typedRoute: route as TypedRoute, 15 | routesNames, 16 | }, 17 | }; 18 | }); 19 | `; 20 | } 21 | -------------------------------------------------------------------------------- /src/core/output/generators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './files'; 2 | export * from './blocks'; 3 | -------------------------------------------------------------------------------- /src/core/output/index.ts: -------------------------------------------------------------------------------- 1 | export * from './fileSave'; 2 | -------------------------------------------------------------------------------- /src/core/output/static/index.ts: -------------------------------------------------------------------------------- 1 | export * from './watermark.template'; 2 | -------------------------------------------------------------------------------- /src/core/output/static/watermark.template.ts: -------------------------------------------------------------------------------- 1 | export const watermarkTemplate = ` 2 | // @ts-nocheck 3 | // eslint-disable 4 | // --------------------------------------------------- 5 | // 🚗🚦 Generated by nuxt-typed-router. Do not modify ! 6 | // --------------------------------------------------- 7 | 8 | 9 | 10 | `; 11 | -------------------------------------------------------------------------------- /src/core/parser/base.ts: -------------------------------------------------------------------------------- 1 | import type { NuxtPage } from '@nuxt/schema'; 2 | import type { GeneratorOutput, RouteParamsDecl, RoutePathsDecl } from '../../types'; 3 | import { isItemLast } from '../../utils'; 4 | import { walkThoughRoutes } from './walkRoutes'; 5 | 6 | export function constructRouteMap(routesConfig: NuxtPage[]): GeneratorOutput { 7 | try { 8 | let routesObjectTemplate = '{'; 9 | let routesDeclTemplate = '{'; 10 | let routesList: string[] = []; 11 | let routesParams: RouteParamsDecl[] = []; 12 | let routesPaths: RoutePathsDecl[] = []; 13 | 14 | const output = { 15 | routesObjectTemplate, 16 | routesDeclTemplate, 17 | routesList, 18 | routesParams, 19 | routesPaths, 20 | }; 21 | 22 | startGenerator({ 23 | output, 24 | routesConfig, 25 | }); 26 | 27 | return output; 28 | } catch (e) { 29 | throw new Error(`Generation failed: ${e}`); 30 | } 31 | } 32 | 33 | type StartGeneratorParams = { 34 | output: GeneratorOutput; 35 | routesConfig: NuxtPage[]; 36 | }; 37 | 38 | export function startGenerator({ output, routesConfig }: StartGeneratorParams): void { 39 | routesConfig.forEach((route, index) => { 40 | const rootSiblingsRoutes = routesConfig.filter((rt) => rt.path !== route.path); 41 | walkThoughRoutes({ 42 | route, 43 | level: 0, 44 | output, 45 | siblings: rootSiblingsRoutes, 46 | isLast: isItemLast(routesConfig, index), 47 | isLocale: false, 48 | }); 49 | }); 50 | output.routesObjectTemplate += '}'; 51 | output.routesDeclTemplate += '}'; 52 | } 53 | -------------------------------------------------------------------------------- /src/core/parser/extractChunks.ts: -------------------------------------------------------------------------------- 1 | import type { NuxtPage } from '@nuxt/schema'; 2 | 3 | export function extractUnMatchingSiblings( 4 | mainRoute: NuxtPage, 5 | siblingRoutes?: NuxtPage[] 6 | ): NuxtPage[] | undefined { 7 | return siblingRoutes?.filter((s) => { 8 | return s.name !== mainRoute.name; 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /src/core/parser/i18n.modifiers.ts: -------------------------------------------------------------------------------- 1 | import type { NuxtPage } from '@nuxt/schema'; 2 | import { moduleOptionStore } from '../config'; 3 | import type { RoutePathsDecl } from '../../types'; 4 | import logSymbols from 'log-symbols'; 5 | 6 | const specialCharacterRegxp = /([^a-zA-Z0-9_])/gm; 7 | 8 | /** Will check if the is a route generated by @nuxtjs/i18n */ 9 | export function is18Sibling(source: RoutePathsDecl[], route: NuxtPage) { 10 | const { i18n, i18nOptions, i18nLocales } = moduleOptionStore; 11 | if (i18n && i18nOptions && i18nOptions?.strategy !== 'no_prefix') { 12 | const i18LocalesRecognizer = i18nLocales 13 | ?.map((m) => m.replace(specialCharacterRegxp, '\\$&')) 14 | .join('|'); 15 | 16 | return !!route.path?.match(new RegExp(`^/?(${i18LocalesRecognizer})(/.*)?$`, 'g')); 17 | } 18 | return false; 19 | } 20 | 21 | export function modifyRoutePrefixDefaultIfI18n(route: NuxtPage) { 22 | const { i18n, i18nOptions, i18nLocales } = moduleOptionStore; 23 | if (i18n && i18nOptions && route.name) { 24 | const separator = i18nOptions?.routesNameSeparator ?? '___'; 25 | const i18LocalesRecognizer = i18nLocales 26 | ?.map((m) => m.replace(specialCharacterRegxp, '\\$&')) 27 | .join('|'); 28 | if (i18nOptions?.strategy === 'prefix_and_default') { 29 | const routeDefaultRegXp = new RegExp( 30 | `([a-zA-Z0-9-]+)${separator}(${i18LocalesRecognizer})${separator}default`, 31 | 'g' 32 | ); 33 | const match = routeDefaultRegXp.exec(route.name); 34 | if (match) { 35 | const [_, routeName] = match; 36 | route.name = routeName; 37 | return { 38 | ...route, 39 | name: routeName, 40 | }; 41 | } 42 | } else if (i18nOptions?.strategy === 'prefix_except_default') { 43 | let defaultLocale = i18nLocales.find((f) => f === i18nOptions.defaultLocale) 44 | ? i18nOptions.defaultLocale?.replace(specialCharacterRegxp, '\\$&') 45 | : undefined; 46 | 47 | const routeDefaultNameRegXp = new RegExp(`^([a-zA-Z0-9-]+)${separator}${defaultLocale}`, 'g'); 48 | const match = routeDefaultNameRegXp.exec(route.name); 49 | if (match) { 50 | const [_, routeName] = match; 51 | return { 52 | ...route, 53 | name: routeName, 54 | }; 55 | } 56 | } 57 | } 58 | return route; 59 | } 60 | -------------------------------------------------------------------------------- /src/core/parser/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base'; 2 | -------------------------------------------------------------------------------- /src/core/parser/params/destructurePath.ts: -------------------------------------------------------------------------------- 1 | import { nanoid } from 'nanoid/non-secure'; 2 | import type { RoutePathsDecl } from '../../../../src/types'; 3 | 4 | const ExtractRegex = /(^(\/)?([^:/]+)?(:(\w+)(\((.*)\)[*+]?)?(\?)?)*([^:/]+)?)+/g; 5 | export type DestructuredPath = { 6 | type: 'name' | 'param' | 'optionalParam' | 'catchAll'; 7 | content: string; 8 | fullPath?: string; 9 | id: string; 10 | routeName: string; 11 | isLocale: boolean; 12 | }; 13 | 14 | export function destructurePath(path: string, route: RoutePathsDecl): DestructuredPath[] { 15 | let allPathElements: DestructuredPath[] = []; 16 | let _path = `${path}`; 17 | do { 18 | const { pathElements, strippedPath } = extractPathElements(_path, route); 19 | allPathElements = allPathElements.concat(pathElements); 20 | _path = _path.replace(strippedPath, ''); 21 | } while (_path.length); 22 | 23 | return allPathElements; 24 | } 25 | 26 | function extractPathElements(partOfPath: string, route: RoutePathsDecl) { 27 | let pathElements: DestructuredPath[] = []; 28 | let strippedPath = ''; 29 | let matches: RegExpExecArray | null; 30 | matches = ExtractRegex.exec(partOfPath); 31 | if (matches) { 32 | const [_, mtch, slash, path1, paramDef, key, catchAll, parentheseContent, optional, path2] = 33 | matches; 34 | if (mtch) { 35 | strippedPath = mtch; 36 | 37 | const sharedProperties = { 38 | fullPath: route.path, 39 | routeName: route.name!, 40 | isLocale: route.isLocale, 41 | }; 42 | if (path1) { 43 | pathElements.push({ 44 | type: 'name', 45 | content: path1, 46 | id: nanoid(6), 47 | ...sharedProperties, 48 | }); 49 | } 50 | if (key) { 51 | pathElements.push({ 52 | type: catchAll && parentheseContent ? 'catchAll' : optional ? 'optionalParam' : 'param', 53 | content: key, 54 | id: nanoid(6), 55 | ...sharedProperties, 56 | }); 57 | } 58 | if (path2) { 59 | pathElements.push({ 60 | type: 'name', 61 | content: path2, 62 | id: nanoid(6), 63 | ...sharedProperties, 64 | }); 65 | } 66 | } 67 | } 68 | 69 | return { pathElements, strippedPath }; 70 | } 71 | -------------------------------------------------------------------------------- /src/core/parser/params/extractParams.ts: -------------------------------------------------------------------------------- 1 | import type { ParamDecl } from '../../../types'; 2 | import { extractParamsFromPathDecl } from './replaceParams'; 3 | 4 | export function extractRouteParamsFromPath( 5 | path: string, 6 | isIndexFileForRouting: boolean, 7 | previousParams?: ParamDecl[] 8 | ): ParamDecl[] { 9 | const params = extractParamsFromPathDecl(path); 10 | 11 | let allMergedParams = params.map( 12 | ({ name, optional, catchAll }): ParamDecl => ({ 13 | key: name, 14 | required: !optional, 15 | notRequiredOnPage: optional, 16 | catchAll, 17 | }) 18 | ); 19 | if (previousParams?.length) { 20 | allMergedParams = previousParams 21 | .map((m) => ({ ...m, required: false })) 22 | .concat(allMergedParams); 23 | } 24 | if (!params.length && isIndexFileForRouting) { 25 | const lastItem = allMergedParams[allMergedParams.length - 1]; 26 | if (lastItem) { 27 | lastItem.required = true; 28 | } 29 | } 30 | return allMergedParams; 31 | } 32 | -------------------------------------------------------------------------------- /src/core/parser/params/index.ts: -------------------------------------------------------------------------------- 1 | export * from './extractParams'; 2 | export * from './replaceParams'; 3 | export * from './destructurePath'; 4 | -------------------------------------------------------------------------------- /src/core/parser/params/replaceParams.ts: -------------------------------------------------------------------------------- 1 | const routeParamExtractRegxp = /(:(\w+)(\(\.[^(]\)[*+]?)?(\?)?)+/g; 2 | 3 | type ExtractedParam = { name: string; optional: boolean; catchAll: boolean }; 4 | 5 | export function extractParamsFromPathDecl(path: string): ExtractedParam[] { 6 | let params: ExtractedParam[] = []; 7 | let matches: RegExpExecArray | null; 8 | do { 9 | matches = routeParamExtractRegxp.exec(path); 10 | if (matches) { 11 | const [_, mtch, key, catchAll, optional] = matches; 12 | if (mtch && key) { 13 | const _param = { 14 | name: key, 15 | optional: !!optional, 16 | catchAll: !!catchAll, 17 | } satisfies ExtractedParam; 18 | params.push(_param); 19 | } 20 | } 21 | } while (matches); 22 | 23 | return params; 24 | } 25 | -------------------------------------------------------------------------------- /src/core/parser/removeNuxtDefs.ts: -------------------------------------------------------------------------------- 1 | import { readFile } from 'fs/promises'; 2 | import { existsSync } from 'fs'; 3 | import { createResolver } from '@nuxt/kit'; 4 | import { processPathAndWriteFile } from '../fs'; 5 | 6 | type RemoveNuxtDefinitionsOptions = { 7 | buildDir: string; 8 | autoImport: boolean; 9 | }; 10 | 11 | export async function removeNuxtDefinitions({ 12 | buildDir, 13 | autoImport, 14 | }: RemoveNuxtDefinitionsOptions): Promise { 15 | const { resolve } = createResolver(import.meta.url); 16 | 17 | // Remove NuxtLink from .nuxt/components.d.ts 18 | const componentFilePath = resolve(buildDir, 'components.d.ts'); 19 | if (existsSync(componentFilePath)) { 20 | const componentDefinitions = await readFile(componentFilePath, { 21 | encoding: 'utf8', 22 | }); 23 | const replacedNuxtLink = componentDefinitions.replace( 24 | /'NuxtLink': typeof import\(".*"\)\['default'\]|'NuxtLinkLocale': typeof import\(".*"\)\['default'\]/gm, 25 | '' 26 | ); 27 | 28 | processPathAndWriteFile({ 29 | content: replacedNuxtLink, 30 | fileName: 'components.d.ts', 31 | outDir: '.nuxt', 32 | }); 33 | } 34 | 35 | // Remove global imports from .nuxt/types/imports.d.ts 36 | 37 | if (autoImport) { 38 | const importsFilePath = resolve(buildDir, 'types/imports.d.ts'); 39 | if (existsSync(importsFilePath)) { 40 | let globalDefinitions = await readFile(importsFilePath, { 41 | encoding: 'utf8', 42 | }); 43 | 44 | const importsToRemove = [ 45 | 'useRouter', 46 | 'useRoute', 47 | 'useLocalePath', 48 | 'useLocaleRoute', 49 | 'definePageMeta', 50 | 'navigateTo', 51 | ].map((m) => new RegExp(`const ${m}: typeof import\\('.*'\\)\\['${m}'\\]`, 'gm')); 52 | 53 | importsToRemove.forEach((imp) => { 54 | globalDefinitions = globalDefinitions.replace(imp, ''); 55 | }); 56 | 57 | processPathAndWriteFile({ 58 | content: globalDefinitions, 59 | fileName: 'types/imports.d.ts', 60 | outDir: '.nuxt', 61 | }); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/core/parser/walkRoutes.ts: -------------------------------------------------------------------------------- 1 | import type { NuxtPage } from '@nuxt/schema'; 2 | import { camelCase } from 'lodash-es'; 3 | import type { GeneratorOutput, ParamDecl } from '../../types'; 4 | import { isItemLast } from '../../utils'; 5 | import { moduleOptionStore } from '../config'; 6 | import { extractUnMatchingSiblings } from './extractChunks'; 7 | import { is18Sibling, modifyRoutePrefixDefaultIfI18n } from './i18n.modifiers'; 8 | import { extractRouteParamsFromPath } from './params'; 9 | 10 | type WalkThoughRoutesParams = { 11 | route: NuxtPage; 12 | level: number; 13 | siblings?: NuxtPage[]; 14 | parent?: NuxtPage; 15 | previousParams?: ParamDecl[]; 16 | output: GeneratorOutput; 17 | isLast: boolean; 18 | isLocale: boolean; 19 | }; 20 | 21 | function createKeyedName(route: NuxtPage, parent?: NuxtPage): string { 22 | const splittedPaths = route.path.split('/'); 23 | const parentPath = splittedPaths[splittedPaths.length - 1]; 24 | if (parent) { 25 | return camelCase(parentPath || 'index'); 26 | } else { 27 | return camelCase(route.path.split('/').join('-')) || 'index'; 28 | } 29 | } 30 | 31 | function createNameKeyFromFullName(route: NuxtPage, level: number, parentName?: string): string { 32 | let splitted: string[] = []; 33 | splitted = route.name?.split('-') ?? []; 34 | splitted = splitted.slice(level, splitted.length); 35 | if (splitted[0] === parentName) { 36 | splitted.splice(0, 1); 37 | } 38 | 39 | const keyName = route.path === '' ? 'index' : camelCase(splitted.join('-')) || 'index'; 40 | 41 | return keyName; 42 | } 43 | 44 | /** Mutates the output object with generated routes */ 45 | export function walkThoughRoutes({ 46 | route: _route, 47 | level, 48 | siblings, 49 | parent, 50 | previousParams, 51 | output, 52 | isLast, 53 | isLocale, 54 | }: WalkThoughRoutesParams) { 55 | const route = modifyRoutePrefixDefaultIfI18n(_route); 56 | const isLocaleRoute = isLocale || is18Sibling(output.routesPaths, route); 57 | 58 | if (route.file && moduleOptionStore.resolvedIgnoredRoutes.includes(route.file)) { 59 | return; 60 | } 61 | 62 | const newPath = `${parent?.path ?? ''}${ 63 | route.path.startsWith('/') || parent?.path === '/' ? route.path : `/${route.path}` 64 | }`; 65 | 66 | if (parent?.path !== '/' || newPath !== parent?.path) { 67 | output.routesPaths.push({ 68 | name: route.name, 69 | path: newPath, 70 | isLocale: isLocaleRoute, 71 | }); 72 | } 73 | 74 | // Filter routes added by i18n module 75 | if (route.children?.length) { 76 | // - Route with children 77 | 78 | let childrenChunks = route.children; 79 | let nameKey = createKeyedName(route, parent); 80 | const allRouteParams = extractRouteParamsFromPath(route.path, false, previousParams); 81 | 82 | const newRoute = { ...route, name: nameKey, path: newPath } satisfies NuxtPage; 83 | 84 | if (!isLocaleRoute) { 85 | // Output 86 | output.routesObjectTemplate += `${nameKey}:{`; 87 | output.routesDeclTemplate += `"${nameKey}":{`; 88 | } 89 | 90 | // Recursive walk though children 91 | childrenChunks?.map((routeConfig, index) => 92 | walkThoughRoutes({ 93 | route: routeConfig, 94 | level: level + 1, 95 | siblings: extractUnMatchingSiblings(route, siblings), 96 | parent: newRoute, 97 | previousParams: allRouteParams, 98 | output, 99 | isLast: isItemLast(childrenChunks, index), 100 | isLocale: isLocaleRoute, 101 | }) 102 | ); 103 | if (!isLocaleRoute) { 104 | output.routesObjectTemplate += '},'; 105 | output.routesDeclTemplate += `}${isLast ? '' : ','}`; 106 | } 107 | 108 | // Output 109 | } else if (route.name && !isLocaleRoute) { 110 | // - Single route 111 | 112 | let keyName = createNameKeyFromFullName(route, level, parent?.name); 113 | 114 | output.routesObjectTemplate += `'${keyName}': '${route.name}' as const,`; 115 | output.routesDeclTemplate += `"${keyName}": "${route.name}"${isLast ? '' : ','}`; 116 | output.routesList.push(route.name); 117 | 118 | // Params 119 | const isIndexFileForRouting = route.path === ''; 120 | const allRouteParams = extractRouteParamsFromPath( 121 | route.path, 122 | isIndexFileForRouting, 123 | previousParams 124 | ); 125 | output.routesParams.push({ 126 | name: route.name, 127 | params: allRouteParams, 128 | }); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/module.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtModule, createResolver, addTemplate } from '@nuxt/kit'; 2 | import type { Nuxt } from '@nuxt/schema'; 3 | import type { NuxtI18nOptions } from '@nuxtjs/i18n'; 4 | import { createTypedRouter } from './core'; 5 | import { moduleOptionStore } from './core/config'; 6 | import type { ModuleOptions } from './types'; 7 | import { removeNuxtDefinitions } from './core/parser/removeNuxtDefs'; 8 | export type { ModuleOptions } from './types'; 9 | 10 | export default defineNuxtModule({ 11 | meta: { 12 | name: 'nuxt-typed-router', 13 | configKey: 'nuxtTypedRouter', 14 | compatibility: { nuxt: '>=3.0.0', bridge: false }, 15 | }, 16 | defaults: { 17 | plugin: false, 18 | strict: false, 19 | pathCheck: true, 20 | disablePrettier: false, 21 | removeNuxtDefs: true, 22 | ignoreRoutes: [], 23 | }, 24 | setup(moduleOptions, nuxt: Nuxt) { 25 | const { resolve } = createResolver(import.meta.url); 26 | 27 | const rootDir = nuxt.options.rootDir; 28 | let i18nOptions: NuxtI18nOptions | null = null; 29 | 30 | const hasi18nModuleRegistered = nuxt.options.modules.some((mod) => { 31 | if (Array.isArray(mod)) { 32 | const [moduleName, options] = mod; 33 | const isRegistered = moduleName === '@nuxtjs/i18n'; 34 | if (isRegistered) { 35 | i18nOptions = options; 36 | } 37 | return isRegistered; 38 | } else { 39 | const isRegistered = mod === '@nuxtjs/i18n'; 40 | if (isRegistered) { 41 | i18nOptions = (nuxt.options as any).i18n; 42 | } 43 | return isRegistered; 44 | } 45 | }); 46 | 47 | const isDocumentDriven = 48 | !!nuxt.options.modules.find((mod) => { 49 | if (Array.isArray(mod)) { 50 | return mod[0] === '@nuxt/content'; 51 | } else { 52 | return mod === '@nuxt/content'; 53 | } 54 | }) && 55 | 'content' in nuxt.options && 56 | 'documentDriven' in (nuxt.options.content as any); 57 | 58 | moduleOptionStore.updateOptions({ 59 | ...moduleOptions, 60 | i18n: hasi18nModuleRegistered, 61 | i18nOptions, 62 | isDocumentDriven, 63 | }); 64 | 65 | nuxt.options.alias = { 66 | ...nuxt.options.alias, 67 | '@typed-router': resolve(`${rootDir}/.nuxt/typed-router`), 68 | }; 69 | 70 | // Force register of type declaration 71 | nuxt.hook('prepare:types', (options) => { 72 | options.tsConfig.include?.unshift('./typed-router/typed-router.d.ts'); 73 | if (moduleOptions.removeNuxtDefs) { 74 | removeNuxtDefinitions({ 75 | autoImport: nuxt.options.imports.autoImport ?? true, 76 | buildDir: nuxt.options.buildDir, 77 | }); 78 | } 79 | }); 80 | 81 | nuxt.hook('build:done', () => { 82 | if (moduleOptions.removeNuxtDefs) { 83 | removeNuxtDefinitions({ 84 | autoImport: nuxt.options.imports.autoImport ?? true, 85 | buildDir: nuxt.options.buildDir, 86 | }); 87 | } 88 | }); 89 | 90 | if (nuxt.options.dev) { 91 | nuxt.hook('devtools:customTabs' as any, (tabs: any[]) => { 92 | tabs.push({ 93 | name: 'nuxt-typed-router', 94 | title: 'Nuxt Typed Router', 95 | icon: 'https://github.com/victorgarciaesgi/nuxt-typed-router/blob/master/.github/images/logo.png?raw=true', 96 | view: { 97 | type: 'iframe', 98 | src: 'https://nuxt-typed-router.vercel.app/', 99 | }, 100 | }); 101 | }); 102 | } 103 | 104 | createTypedRouter({ nuxt }); 105 | }, 106 | }); 107 | -------------------------------------------------------------------------------- /src/types/config.types.ts: -------------------------------------------------------------------------------- 1 | export interface ModuleOptions { 2 | /** 3 | * 4 | * Enables path autocomplete and path validity for programmatic validation 5 | * 6 | * @default true 7 | */ 8 | pathCheck?: boolean; 9 | /** 10 | * Set to false if you don't want a plugin generated 11 | * @default false 12 | */ 13 | plugin?: boolean; 14 | /** 15 | * Customise Route location arguments strictness for `NuxtLink` or `router` 16 | * All strict options are disabled by default. 17 | * You can tweak options to add strict router navigation options. 18 | * 19 | * By passing `true` you can enable all of them 20 | * 21 | * @default false 22 | */ 23 | strict?: boolean | StrictOptions; 24 | /** 25 | * Remove Nuxt definitions to avoid conflicts 26 | * @default true 27 | */ 28 | removeNuxtDefs?: boolean; 29 | /** 30 | * ⚠️ Experimental 31 | * 32 | * Exclude certain routes from being included into the generated types 33 | * Ex: 404 routes or catchAll routes 34 | */ 35 | ignoreRoutes?: string[]; 36 | /** 37 | * Disable prettier formatter 38 | * @default false 39 | */ 40 | disablePrettier?: boolean; 41 | } 42 | 43 | export interface StrictOptions { 44 | NuxtLink?: StrictParamsOptions; 45 | router?: StrictParamsOptions; 46 | } 47 | 48 | export interface StrictParamsOptions { 49 | /** 50 | * Prevent passing string path to the RouteLocation argument. 51 | * 52 | * Ex: 53 | * ```vue 54 | * 57 | * ``` 58 | * Or 59 | * ```ts 60 | * router.push('/login'); // Error ❌ 61 | * navigateTo('/login'); // Error ❌ 62 | * ``` 63 | * 64 | * @default false 65 | */ 66 | strictToArgument?: boolean; 67 | /** 68 | * Prevent passing a `params` property in the RouteLocation argument. 69 | * 70 | * Ex: 71 | * ```vue 72 | * 75 | * ``` 76 | * Or 77 | * ```ts 78 | * router.push({path: "/login"}); // Error ❌ 79 | * navigateTo({path: "/login"}); // Error ❌ 80 | * ``` 81 | * 82 | * @default false 83 | */ 84 | strictRouteLocation?: boolean; 85 | } 86 | -------------------------------------------------------------------------------- /src/types/generator.types.ts: -------------------------------------------------------------------------------- 1 | export interface ParamDecl { 2 | key: string; 3 | required: boolean; 4 | notRequiredOnPage: boolean; 5 | catchAll: boolean; 6 | } 7 | 8 | export interface RouteParamsDecl { 9 | name: string; 10 | params: ParamDecl[]; 11 | } 12 | 13 | export interface RoutePathsDecl { 14 | path: string; 15 | name?: string; 16 | isLocale: boolean; 17 | } 18 | 19 | export interface GeneratorOutput { 20 | /** String template of the exported route object of `__routes.ts` file (contains `as const`) */ 21 | routesObjectTemplate: string; 22 | /** String template of the injected $routeList in Nuxt plugin */ 23 | routesDeclTemplate: string; 24 | /** String array of the all the routes for the Union type */ 25 | routesList: string[]; 26 | /** Array of RouteParams mapping with routeList */ 27 | routesParams: RouteParamsDecl[]; 28 | routesPaths: RoutePathsDecl[]; 29 | } 30 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config.types'; 2 | export * from './generator.types'; 3 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './misc.utils'; 2 | -------------------------------------------------------------------------------- /src/utils/misc.utils.ts: -------------------------------------------------------------------------------- 1 | export function isItemLast(array: any[] | undefined, index: number): boolean { 2 | return array ? index === array.length - 1 : false; 3 | } 4 | 5 | export function returnIfTrue(condition: boolean | undefined, template: string, otherwise?: string) { 6 | if (condition) { 7 | return template; 8 | } 9 | return otherwise ?? ''; 10 | } 11 | 12 | export function returnIfFalse( 13 | condition: boolean | undefined, 14 | template: string, 15 | otherwise?: string 16 | ) { 17 | if (!condition) { 18 | return template; 19 | } 20 | return otherwise ?? ''; 21 | } 22 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Tests 2 | 3 | Tests are divided in multiple ways 4 | 5 | - e2e 6 | - fixtures unit test for types with `vue-tsc` 7 | - Unit test with `vitest typecheck` 8 | 9 | 10 | Files to update for testing: 11 | 12 | - e2e/base.spec.ts 13 | - fixtures/simple/components/*.vue 14 | - fixtures/simple/tests/*.ts 15 | - fixtures/complex/components/*.vue 16 | - fixtures/complex/tests/*.ts 17 | 18 | 19 | Cannot use pnpm workspaces because of a Nuxt bug with workspaces 20 | 21 | `simple` fixture repo is for a vanilla config project. 22 | 23 | `complex` fixture is for a heavily modified config project (like plugin, srcDir modified etc..) -------------------------------------------------------------------------------- /test/e2e/complex/complex.spec.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url'; 2 | import { describe, it, expect } from 'vitest'; 3 | import { setup, $fetch, createPage } from '@nuxt/test-utils/e2e'; 4 | import { expectNoClientErrors } from '../utils'; 5 | import { timeout } from '$$/utils'; 6 | 7 | const TIME = 2000; 8 | 9 | describe('Complex config behaviour', async () => { 10 | await setup({ 11 | rootDir: fileURLToPath(new URL('../../fixtures/complex', import.meta.url)), 12 | setupTimeout: 120000, 13 | dev: true, 14 | }); 15 | 16 | it('should display the root page without error', async () => { 17 | const html = await $fetch('/'); 18 | 19 | expect(html).toContain('Navigate button'); 20 | expect(html).toContain('Navigate link'); 21 | expect(html).toContain('NavigateTo button'); 22 | expect(html).toContain('Navigate plugin'); 23 | 24 | await expectNoClientErrors('/'); 25 | }); 26 | 27 | // // Commented for now because of a Nuxt bug still happening to me 28 | 29 | // it('should navigate correctly with useRouter', async () => { 30 | // const page = await createPage('/'); 31 | // await page.click('#useRouter'); 32 | // const html = await page.innerHTML('body'); 33 | // await timeout(TIME); 34 | 35 | // await expectNoClientErrors('/'); 36 | // }); 37 | 38 | // it('should navigate correctly with nuxtLink', async () => { 39 | // const page = await createPage('/'); 40 | // await page.click('#nuxtLink'); 41 | 42 | // await timeout(TIME); 43 | // const html = await page.innerHTML('body'); 44 | 45 | // await expectNoClientErrors('/'); 46 | // }); 47 | 48 | // it('should navigate correctly with navigateTo', async () => { 49 | // const page = await createPage('/'); 50 | // await page.click('#navigateTo'); 51 | // const html = await page.innerHTML('body'); 52 | // await timeout(TIME); 53 | 54 | // await expectNoClientErrors('/'); 55 | // }); 56 | }); 57 | -------------------------------------------------------------------------------- /test/e2e/simple/simple.spec.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url'; 2 | import { describe, it, expect, assertType, expectTypeOf } from 'vitest'; 3 | import { setup, $fetch, createPage } from '@nuxt/test-utils/e2e'; 4 | import { expectNoClientErrors } from '../utils'; 5 | import { timeout } from '$$/utils'; 6 | 7 | const TIME = 2000; 8 | 9 | describe('Simple config behaviour', async () => { 10 | await setup({ 11 | rootDir: fileURLToPath(new URL('../../fixtures/simple', import.meta.url)), 12 | setupTimeout: 120000, 13 | dev: true, 14 | }); 15 | 16 | it('should display the root page without error', async () => { 17 | const html = await $fetch('/'); 18 | 19 | expect(html).toContain('Navigate button'); 20 | expect(html).toContain('Navigate link'); 21 | expect(html).toContain('NavigateTo button'); 22 | 23 | await expectNoClientErrors('/'); 24 | }); 25 | 26 | it('should navigate correctly with useRouter', async () => { 27 | const page = await createPage('/'); 28 | await page.click('#useRouter'); 29 | const html = await page.innerHTML('body'); 30 | await timeout(TIME); 31 | 32 | await expectNoClientErrors('/'); 33 | }); 34 | 35 | it('should navigate correctly with nuxtLink', async () => { 36 | const page = await createPage('/'); 37 | await page.click('#nuxtLink'); 38 | await timeout(TIME); 39 | const html = await page.innerHTML('body'); 40 | 41 | await expectNoClientErrors('/'); 42 | }); 43 | 44 | it('should navigate correctly with navigateTo', async () => { 45 | const page = await createPage('/'); 46 | await page.click('#navigateTo'); 47 | const html = await page.innerHTML('body'); 48 | await timeout(TIME); 49 | 50 | await expectNoClientErrors('/'); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/e2e/utils.ts: -------------------------------------------------------------------------------- 1 | import { getBrowser, url, useTestContext } from '@nuxt/test-utils'; 2 | 3 | // Taken from nuxt/framework repo 4 | export async function renderPage(path = '/') { 5 | const ctx = useTestContext(); 6 | if (!ctx.options.browser) { 7 | throw new Error('`renderPage` require `options.browser` to be set'); 8 | } 9 | 10 | const browser = await getBrowser(); 11 | const page = await browser.newPage({}); 12 | const pageErrors: Error[] = []; 13 | const consoleLogs: { type: string; text: string }[] = []; 14 | 15 | page.on('console', (message: any) => { 16 | consoleLogs.push({ 17 | type: message.type(), 18 | text: message.text(), 19 | }); 20 | }); 21 | page.on('pageerror', (err: any) => { 22 | pageErrors.push(err); 23 | }); 24 | 25 | if (path) { 26 | await page.goto(url(path), { waitUntil: 'networkidle' }); 27 | } 28 | 29 | return { 30 | page, 31 | pageErrors, 32 | consoleLogs, 33 | }; 34 | } 35 | 36 | // Taken from nuxt/framework repo 37 | export async function expectNoClientErrors(path: string) { 38 | const ctx = useTestContext(); 39 | if (!ctx.options.browser) { 40 | return; 41 | } 42 | 43 | const { pageErrors, consoleLogs } = (await renderPage(path))!; 44 | 45 | const consoleLogErrors = consoleLogs.filter((i) => i.type === 'error'); 46 | const consoleLogWarnings = consoleLogs.filter((i) => i.type === 'warning'); 47 | 48 | expect(pageErrors).toEqual([]); 49 | expect(consoleLogErrors).toEqual([]); 50 | expect(consoleLogWarnings).toEqual([]); 51 | } 52 | -------------------------------------------------------------------------------- /test/fixtures/complex/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log* 3 | .nuxt 4 | .nitro 5 | .cache 6 | .output 7 | .env 8 | dist 9 | 10 | src/plugins -------------------------------------------------------------------------------- /test/fixtures/complex/README.md: -------------------------------------------------------------------------------- 1 | # Nuxt 3 Minimal Starter 2 | 3 | Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. 4 | 5 | ## Setup 6 | 7 | Make sure to install the dependencies: 8 | 9 | ```bash 10 | # yarn 11 | yarn install 12 | 13 | # npm 14 | npm install 15 | 16 | # pnpm 17 | pnpm install 18 | ``` 19 | 20 | ## Development Server 21 | 22 | Start the development server on http://localhost:3000 23 | 24 | ```bash 25 | npm run dev 26 | ``` 27 | 28 | ## Production 29 | 30 | Build the application for production: 31 | 32 | ```bash 33 | npm run build 34 | ``` 35 | 36 | Locally preview production build: 37 | 38 | ```bash 39 | npm run preview 40 | ``` 41 | 42 | Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. 43 | -------------------------------------------------------------------------------- /test/fixtures/complex/app.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /test/fixtures/complex/app/components/testModule.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/complex/app/modules/testAddRoute.ts: -------------------------------------------------------------------------------- 1 | import { createResolver, defineNuxtModule, extendPages } from '@nuxt/kit'; 2 | 3 | export default defineNuxtModule({ 4 | setup() { 5 | const { resolve } = createResolver(import.meta.url); 6 | extendPages((routes) => { 7 | routes.push({ 8 | file: resolve('../components/testModule.vue'), 9 | path: '/testModule/:foo', 10 | name: 'test-module', 11 | }); 12 | }); 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /test/fixtures/complex/app/pages/[...404].vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /test/fixtures/complex/app/pages/admin/[id].vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/complex/app/pages/admin/[id]/action-[slug].vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/complex/app/pages/admin/[id]/index.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/complex/app/pages/admin/[id]/profile.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/complex/app/pages/admin/[id]/settings.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/complex/app/pages/admin/panel/[[blou]].vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/complex/app/pages/baguette.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/complex/app/pages/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 38 | -------------------------------------------------------------------------------- /test/fixtures/complex/app/pages/user/[foo]-[[bar]].vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/complex/app/pages/user/[id].vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/complex/app/pages/user/[id]/[slug].vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/complex/app/pages/user/[id]/[slug]/articles.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/complex/app/pages/user/[id]/[slug]/index.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /test/fixtures/complex/app/pages/user/[id]/index.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/complex/app/pages/user/[id]/posts.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/complex/app/pages/user/[one]-foo-[two].vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/complex/app/pages/user/catch/[...slug].vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/complex/app/pages/user/index.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/complex/app/pages/user/test-[[optional]].vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/complex/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import TestModuleRoute from './app/modules/testAddRoute'; 2 | 3 | export default defineNuxtConfig({ 4 | modules: ['nuxt-typed-router', TestModuleRoute, '@nuxtjs/i18n'], 5 | future: { 6 | compatibilityVersion: 4, 7 | }, 8 | nuxtTypedRouter: { 9 | plugin: true, 10 | ignoreRoutes: ['[...404].vue'], 11 | }, 12 | i18n: { 13 | locales: ['en', 'fr'], 14 | defaultLocale: 'en', 15 | }, 16 | imports: { 17 | autoImport: false, 18 | }, 19 | vite: { 20 | resolve: { 21 | dedupe: ['vue-router'], 22 | }, 23 | }, 24 | compatibilityDate: '2025-01-07', 25 | }); 26 | -------------------------------------------------------------------------------- /test/fixtures/complex/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "complex", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "build": "nuxt build", 7 | "dev": "nuxt dev", 8 | "generate": "nuxt generate", 9 | "preview": "nuxt preview" 10 | }, 11 | "devDependencies": { 12 | "@intlify/core-base": "~11.1.5", 13 | "@intlify/message-compiler": "~11.1.5", 14 | "@intlify/shared": "~11.1.5", 15 | "@intlify/vue-i18n-bridge": "1.1.0", 16 | "@intlify/vue-router-bridge": "1.1.0", 17 | "@nuxtjs/i18n": "9.5.5", 18 | "nuxt": "3.17.5", 19 | "nuxt-typed-router": "workspace:*", 20 | "vue": "3.5.16", 21 | "vue-i18n": "~11.1.5" 22 | } 23 | } -------------------------------------------------------------------------------- /test/fixtures/complex/tests/i18n/useLocaleRoute.spec-d.ts: -------------------------------------------------------------------------------- 1 | import { assertType, test } from 'vitest'; 2 | import type { RouteLocationMatched } from 'vue-router'; 3 | import type { TypedRouteFromName } from '@typed-router'; 4 | import { useLocaleRoute } from '@typed-router'; 5 | 6 | // @ts-expect-error Ensure global imports are disabled 7 | declare const globalDecl: (typeof globalThis)['useLocaleRoute']; 8 | 9 | const localeRoute = useLocaleRoute(); 10 | 11 | // ! ------ Should Error ❌ 12 | 13 | // @ts-expect-error 14 | assertType(localeRoute({ name: 'index' }, 'DE')); 15 | // * index.vue 16 | // @ts-expect-error 17 | assertType(localeRoute({ name: 'index', params: { id: 1 } }, 'es')); 18 | // @ts-expect-error 19 | assertType(localeRoute({ name: 'index', params: { id: 1 } })); 20 | // @ts-expect-error 21 | assertType(localeRoute({ name: 'blabla-baguette' })); 22 | 23 | // * --- [id].vue 24 | // @ts-expect-error 25 | assertType(localeRoute({ name: 'user-id' })); 26 | // @ts-expect-error 27 | assertType(localeRoute({ name: 'user-id', params: { foo: 'bar' } })); 28 | 29 | // * --- [foo]-[[bar]].vue 30 | // @ts-expect-error 31 | assertType(localeRoute({ name: 'user-foo-bar' })); 32 | // @ts-expect-error 33 | assertType(localeRoute({ name: 'user-foo-bar', params: { bar: 1 } })); 34 | 35 | // * --- [...slug].vue 36 | // @ts-expect-error 37 | assertType(localeRoute({ name: 'user-slug' })); 38 | // @ts-expect-error 39 | assertType(localeRoute({ name: 'user-slug', params: { slug: 1 } })); 40 | 41 | // * --- [one]-foo-[two].vue 42 | // @ts-expect-error 43 | assertType(localeRoute({ name: 'user-one-foo-two' })); 44 | // @ts-expect-error 45 | assertType(localeRoute({ name: 'user-one-foo-two', params: { one: 1 } })); 46 | 47 | // * --- [id]/[slug].vue 48 | // @ts-expect-error 49 | assertType(localeRoute({ name: 'user-id-slug' })); 50 | // @ts-expect-error 51 | assertType(localeRoute({ name: 'user-id-slug', params: { id: 1 } })); 52 | 53 | // * --- Routes added by modules 54 | // @ts-expect-error 55 | assertType(localeRoute({ name: 'test-module' })); 56 | 57 | // * --- Path navigation 58 | // @ts-expect-error 59 | assertType(localeRoute('/fooooo')); 60 | // @ts-expect-error 61 | assertType(localeRoute({ path: '/foooo' })); 62 | 63 | // * Basic types 64 | 65 | test('Basic', () => { 66 | const resolved = localeRoute({ name: 'user-foo-bar', params: { foo: 1 } }, 'fr'); 67 | 68 | assertType>(resolved); 69 | assertType<'user-foo-bar'>(resolved.name); 70 | assertType<{ 71 | foo: string; 72 | bar?: string | undefined; 73 | }>(resolved.params); 74 | assertType(resolved.fullPath); 75 | assertType(resolved.hash); 76 | assertType(resolved.path); 77 | assertType(resolved.matched); 78 | }); 79 | 80 | // * index.vue 81 | 82 | test('index', () => { 83 | const resolved = localeRoute({ name: 'index' }, 'fr'); 84 | 85 | assertType>(resolved); 86 | assertType<'index'>(resolved.name); 87 | // @ts-expect-error 88 | assertType(resolved.params); 89 | }); 90 | 91 | // * --- [id].vue 92 | 93 | test('[id]', () => { 94 | const resolved = localeRoute({ name: 'user-id', params: { id: 1 } }, 'fr'); 95 | 96 | assertType>(resolved); 97 | assertType<'user-id'>(resolved.name); 98 | assertType<{ id: string }>(resolved.params); 99 | }); 100 | 101 | // * --- [foo]-[[bar]].vue 102 | test('[foo]-[[bar]]', () => { 103 | const resolved = localeRoute({ name: 'user-foo-bar', params: { foo: 1, bar: 1 } }, 'fr'); 104 | 105 | assertType>(resolved); 106 | assertType<'user-foo-bar'>(resolved.name); 107 | assertType<{ 108 | foo: string; 109 | bar?: string | undefined; 110 | }>(resolved.params); 111 | }); 112 | 113 | // * --- [...slug].vue 114 | test('[...slug]', () => { 115 | const resolved = localeRoute({ name: 'user-catch-slug', params: { slug: [1, 2] } }, 'fr'); 116 | 117 | assertType>(resolved); 118 | assertType<'user-catch-slug'>(resolved.name); 119 | assertType<{ 120 | slug: string[]; 121 | }>(resolved.params); 122 | }); 123 | 124 | // * --- [one]-foo-[two].vue 125 | test('[one]-foo-[two]', () => { 126 | const resolved = localeRoute({ name: 'user-one-foo-two', params: { one: 1, two: 2 } }, 'fr'); 127 | 128 | assertType>(resolved); 129 | assertType<'user-one-foo-two'>(resolved.name); 130 | assertType<{ 131 | one: string; 132 | two: string; 133 | }>(resolved.params); 134 | }); 135 | 136 | // * --- [id]/[slug].vue 137 | 138 | test('[id]/[slug]', () => { 139 | const resolved = localeRoute({ name: 'user-id-slug', params: { slug: 1, id: '1' } }, 'fr'); 140 | 141 | assertType>(resolved); 142 | assertType<'user-id-slug'>(resolved.name); 143 | assertType<{ 144 | id: string; 145 | slug: string; 146 | }>(resolved.params); 147 | }); 148 | 149 | // * --- Routes added by modules 150 | 151 | test('Routes added by modules', () => { 152 | const resolved = localeRoute({ name: 'test-module', params: { foo: 1 } }, 'fr'); 153 | 154 | assertType>(resolved); 155 | assertType<'test-module'>(resolved.name); 156 | assertType<{ 157 | foo: string; 158 | }>(resolved.params); 159 | }); 160 | -------------------------------------------------------------------------------- /test/fixtures/complex/tests/misc/definePageMeta.spec-d.ts: -------------------------------------------------------------------------------- 1 | import { assertType, expectTypeOf } from 'vitest'; 2 | import { definePageMeta } from '@typed-router'; 3 | 4 | // Given 5 | 6 | // - Usage of useRouter with useRouter 7 | 8 | // ! ------ Should Error ❌ 9 | 10 | // * index.vue 11 | definePageMeta({ redirect: { name: 'index' } }); 12 | // @ts-expect-error 13 | definePageMeta({ redirect: { name: 'index', params: { id: 1 } } }); 14 | // @ts-expect-error 15 | definePageMeta({ redirect: { name: 'blabla-baguette' } }); 16 | 17 | // * --- [id].vue 18 | // @ts-expect-error 19 | definePageMeta({ redirect: { name: 'user-id' } }); 20 | // @ts-expect-error 21 | definePageMeta({ redirect: { name: 'user-id', params: { foo: 'bar' } } }); 22 | 23 | // * --- [foo]-[[bar]].vue 24 | // @ts-expect-error 25 | definePageMeta({ redirect: { name: 'user-foo-bar' } }); 26 | // @ts-expect-error 27 | definePageMeta({ redirect: { name: 'user-foo-bar', params: { bar: 1 } } }); 28 | 29 | // * --- [...slug].vue 30 | // @ts-expect-error 31 | definePageMeta({ redirect: { name: 'user-slug' } }); 32 | // @ts-expect-error 33 | definePageMeta({ redirect: { name: 'user-slug', params: { slug: 1 } } }); 34 | 35 | // * --- [one]-foo-[two].vue 36 | // @ts-expect-error 37 | definePageMeta({ redirect: { name: 'user-one-foo-two' } }); 38 | // @ts-expect-error 39 | definePageMeta({ redirect: { name: 'user-one-foo-two', params: { one: 1 } } }); 40 | 41 | // * --- [id]/[slug].vue 42 | // @ts-expect-error 43 | definePageMeta({ redirect: { name: 'user-id-slug' } }); 44 | // @ts-expect-error 45 | definePageMeta({ redirect: { name: 'user-id-slug', params: { id: 1 } } }); 46 | 47 | // * --- Routes added by modules 48 | // @ts-expect-error 49 | definePageMeta({ redirect: { name: 'test-module' } }); 50 | 51 | // * --- Path navigation 52 | // @ts-expect-error 53 | definePageMeta({ redirect: '/fooooooooooo' }); 54 | // @ts-expect-error 55 | definePageMeta({ redirect: { path: '/foo' } }); 56 | 57 | // $ ----- Should be valid ✅ 58 | 59 | definePageMeta({ redirect: { name: 'index' } }); 60 | definePageMeta({ redirect: { name: 'user-id', params: { id: 1 }, hash: 'baz' } }); 61 | definePageMeta({ redirect: { name: 'user-foo-bar', params: { foo: 'bar' }, force: true } }); 62 | definePageMeta({ redirect: { name: 'user-foo-bar', params: { foo: 'bar', bar: 'baz' } } }); 63 | definePageMeta({ redirect: { name: 'user-catch-slug', params: { slug: ['foo'] } } }); 64 | definePageMeta({ redirect: { name: 'user-catch-slug', params: { slug: [1, 2, 3] } } }); 65 | definePageMeta({ redirect: { name: 'user-one-foo-two', params: { one: 1, two: '2' } } }); 66 | definePageMeta({ 67 | redirect: { name: 'user-id-slug', params: { slug: '2' }, query: { foo: 'bar' } }, 68 | }); 69 | 70 | // --- Path navigation 71 | 72 | // ! ------ Should Error ❌ 73 | 74 | // @ts-expect-error 75 | assertType(definePageMeta({ redirect: '' })); 76 | // @ts-expect-error 77 | assertType(definePageMeta({ redirect: '/admin ' })); 78 | // @ts-expect-error 79 | assertType(definePageMeta({ redirect: '/admin/ /' })); 80 | // @ts-expect-error 81 | assertType(definePageMeta({ redirect: `/ / // / / eefzr` })); 82 | // @ts-expect-error 83 | assertType(definePageMeta({ redirect: '/elzhlzehflzhef' })); 84 | // @ts-expect-error 85 | assertType(definePageMeta({ redirect: '/admin/foo/bar' })); 86 | // @ts-expect-error 87 | assertType(definePageMeta({ redirect: '/admin/foo/bar/baz' })); 88 | // @ts-expect-error 89 | assertType(definePageMeta({ redirect: `/admin/${id}/action-bar/taz?query` })); 90 | // @ts-expect-error 91 | assertType(definePageMeta({ redirect: '/admin/panel/3O9393/bar' })); 92 | // @ts-expect-error 93 | assertType(definePageMeta({ redirect: '/admin/foo/ profile/ezfje' })); 94 | // @ts-expect-error 95 | assertType(definePageMeta({ redirect: '/admin/3U93U/settings/baz' })); 96 | // @ts-expect-error 97 | assertType(definePageMeta({ redirect: '/admin/panel/?fjzk' })); 98 | 99 | // $ ----- Should be valid ✅ 100 | 101 | const id = '38789803'; 102 | assertType(definePageMeta({ redirect: '/' })); 103 | assertType(definePageMeta({ redirect: '/baguette' })); 104 | assertType(definePageMeta({ redirect: '/admin/foo' })); 105 | assertType(definePageMeta({ redirect: '/admin/foo/' })); 106 | assertType(definePageMeta({ redirect: `/admin/${id}/action-bar#hash` })); 107 | assertType(definePageMeta({ redirect: `/admin/${id}/action-bar?query=bar` })); 108 | assertType(definePageMeta({ redirect: '/admin/foo/profile/' })); 109 | assertType(definePageMeta({ redirect: `/admin/${id}/settings` })); 110 | assertType(definePageMeta({ redirect: '/admin/panel/' })); 111 | assertType(definePageMeta({ redirect: '/admin/panel/938783/' })); 112 | assertType(definePageMeta({ redirect: '/user/38873-' })); 113 | assertType(definePageMeta({ redirect: '/user/38673/bar/#hash' })); 114 | assertType(definePageMeta({ redirect: '/user/ç9737/foo/articles?baz=foo' })); 115 | assertType(definePageMeta({ redirect: '/user/catch/1/2' })); 116 | assertType(definePageMeta({ redirect: '/user/test-' })); 117 | assertType(definePageMeta({ redirect: '/user' })); 118 | -------------------------------------------------------------------------------- /test/fixtures/complex/tests/router/$typedRouter.spec-d.ts: -------------------------------------------------------------------------------- 1 | import { useNuxtApp } from '#imports'; 2 | import { assertType } from 'vitest'; 3 | import type { TypedRouter } from '@typed-router'; 4 | 5 | // Given 6 | const { $typedRouter } = useNuxtApp(); 7 | 8 | assertType($typedRouter); 9 | 10 | // - Usage of localePath with useRouter 11 | 12 | // ! ------ Should Error ❌ 13 | 14 | // * index.vue 15 | // @ts-expect-error 16 | $typedRouter.push({ name: 'index', params: { id: 1 } }); 17 | // @ts-expect-error 18 | $typedRouter.push({ name: 'index', params: { id: 1 } }); 19 | // @ts-expect-error 20 | $typedRouter.push({ name: 'blabla-baguette' }); 21 | 22 | // * --- [id].vue 23 | // @ts-expect-error 24 | $typedRouter.push({ name: 'user-id' }); 25 | // @ts-expect-error 26 | $typedRouter.push({ name: 'user-id', params: { foo: 'bar' } }); 27 | 28 | // * --- [foo]-[[bar]].vue 29 | // @ts-expect-error 30 | $typedRouter.push({ name: 'user-foo-bar' }); 31 | // @ts-expect-error 32 | $typedRouter.push({ name: 'user-foo-bar', params: { bar: 1 } }); 33 | 34 | // * --- [...slug].vue 35 | // @ts-expect-error 36 | $typedRouter.push({ name: 'user-slug' }); 37 | // @ts-expect-error 38 | $typedRouter.push({ name: 'user-slug', params: { slug: 1 } }); 39 | 40 | // * --- [one]-foo-[two].vue 41 | // @ts-expect-error 42 | $typedRouter.push({ name: 'user-one-foo-two' }); 43 | // @ts-expect-error 44 | $typedRouter.push({ name: 'user-one-foo-two', params: { one: 1 } }); 45 | 46 | // * --- [id]/[slug].vue 47 | // @ts-expect-error 48 | $typedRouter.push({ name: 'user-id-slug' }); 49 | // @ts-expect-error 50 | $typedRouter.push({ name: 'user-id-slug', params: { id: 1 } }); 51 | 52 | // * --- Routes added by modules 53 | // @ts-expect-error 54 | $typedRouter.push({ name: 'test-module' }); 55 | 56 | // * --- Path navigation 57 | // @ts-expect-error 58 | $typedRouter.push('/admin/:id/foo'); 59 | 60 | // $ ----- Should be valid ✅ 61 | 62 | $typedRouter.push({ name: 'index' }); 63 | $typedRouter.push({ name: 'user-id', params: { id: 1 }, hash: 'baz' }); 64 | $typedRouter.push({ name: 'user-foo-bar', params: { foo: 'bar' }, force: true }); 65 | $typedRouter.push({ name: 'user-foo-bar', params: { foo: 'bar', bar: 'baz' } }); 66 | $typedRouter.push({ name: 'user-catch-slug', params: { slug: ['foo'] } }); 67 | $typedRouter.push({ name: 'user-catch-slug', params: { slug: [1, 2, 3] } }); 68 | $typedRouter.push({ name: 'user-one-foo-two', params: { one: 1, two: '2' } }); 69 | $typedRouter.push({ name: 'user-id-slug', params: { slug: '2' }, query: { foo: 'bar' } }); 70 | $typedRouter.push({ name: 'test-module', params: { foo: 1 }, query: { foo: 'bar' } }); 71 | 72 | $typedRouter.replace({ name: 'index' }); 73 | $typedRouter.replace({ name: 'user-id', params: { id: 1 }, hash: 'baz' }); 74 | 75 | // * Resolved routes 76 | 77 | const resolved1 = $typedRouter.resolve({ name: 'index' }); 78 | assertType<'index'>(resolved1.name); 79 | // @ts-expect-error 80 | assertType<'index'>(resolved1.params); 81 | 82 | const resolved2 = $typedRouter.resolve({ name: 'user-id', params: { id: 1 }, hash: 'baz' }); 83 | assertType<'user-id'>(resolved2.name); 84 | assertType<{ id: string }>(resolved2.params); 85 | -------------------------------------------------------------------------------- /test/fixtures/complex/tests/router/NuxtLink.spec-d.ts: -------------------------------------------------------------------------------- 1 | import { assertType, vi } from 'vitest'; 2 | import type { GlobalComponents } from 'vue'; 3 | 4 | const NuxtLink: GlobalComponents['NuxtLink'] = vi.fn() as any; 5 | 6 | // ! ------ Should Error ❌ 7 | 8 | // * index.vue 9 | // @ts-expect-error 10 | assertType(new NuxtLink({ to: { name: 'index', params: { id: 1 } } })); 11 | // @ts-expect-error 12 | assertType(new NuxtLink({ to: { name: 'index', params: { id: 1 } } })); 13 | // @ts-expect-error 14 | assertType(new NuxtLink({ to: { name: 'blabla-baguette' } })); 15 | 16 | // * --- [id].vue 17 | // @ts-expect-error 18 | assertType(new NuxtLink({ to: { name: 'user-id' } })); 19 | // @ts-expect-error 20 | assertType(new NuxtLink({ to: { name: 'user-id', params: { foo: 'bar' } } })); 21 | 22 | // * --- [foo]-[[bar]].vue 23 | // @ts-expect-error 24 | assertType(new NuxtLink({ to: { name: 'user-foo-bar' } })); 25 | // @ts-expect-error 26 | assertType(new NuxtLink({ to: { name: 'user-foo-bar', params: { bar: 1 } } })); 27 | 28 | // * --- [...slug].vue 29 | // @ts-expect-error 30 | assertType(new NuxtLink({ to: { name: 'user-slug' } })); 31 | // @ts-expect-error 32 | assertType(new NuxtLink({ to: { name: 'user-slug', params: { slug: 1 } } })); 33 | 34 | // * --- [one]-foo-[two].vue 35 | // @ts-expect-error 36 | assertType(new NuxtLink({ to: { name: 'user-one-foo-two' } })); 37 | // @ts-expect-error 38 | assertType(new NuxtLink({ to: { name: 'user-one-foo-two', params: { one: 1 } } })); 39 | 40 | // * --- [id]/[slug].vue 41 | // @ts-expect-error 42 | assertType(new NuxtLink({ to: { name: 'user-id-slug' } })); 43 | // @ts-expect-error 44 | assertType(new NuxtLink({ to: { name: 'user-id-slug', params: { id: 1 } } })); 45 | 46 | // * --- Routes added by modules 47 | // @ts-expect-error 48 | assertType(new NuxtLink({ to: { name: 'test-module' } })); 49 | 50 | // --- Path navigation 51 | 52 | // ! ------ Should Error ❌ 53 | 54 | // @ts-expect-error 55 | assertType(new NuxtLink({ to: '' })); 56 | // @ts-expect-error 57 | assertType(new NuxtLink({ to: '/admin ' })); 58 | // @ts-expect-error 59 | assertType(new NuxtLink({ to: '/admin/ /' })); 60 | // @ts-expect-error 61 | assertType(new NuxtLink({ to: `/ / // / / eefzr` })); 62 | // @ts-expect-error 63 | assertType(new NuxtLink({ to: '/elzhlzehflzhef' })); 64 | // @ts-expect-error 65 | assertType(new NuxtLink({ to: '/admin/foo/bar' })); 66 | // @ts-expect-error 67 | assertType(new NuxtLink({ to: '/admin/foo/bar/baz' })); 68 | // @ts-expect-error 69 | assertType(new NuxtLink({ to: `/admin/${id}/action-bar/taz?query` })); 70 | // @ts-expect-error 71 | assertType(new NuxtLink({ to: '/admin/panel/3O9393/bar' })); 72 | // @ts-expect-error 73 | assertType(new NuxtLink({ to: '/admin/foo/ profile/ezfje' })); 74 | // @ts-expect-error 75 | assertType(new NuxtLink({ to: '/admin/3U93U/settings/baz' })); 76 | // @ts-expect-error 77 | assertType(new NuxtLink({ to: '/admin/panel/?fjzk' })); 78 | 79 | // $ ----- Should be valid ✅ 80 | 81 | const id = '38789803'; 82 | assertType(new NuxtLink({ to: '/' })); 83 | assertType(new NuxtLink({ to: '/baguette' })); 84 | assertType(new NuxtLink({ to: '/admin/foo' })); 85 | assertType(new NuxtLink({ to: '/admin/foo/' })); 86 | assertType(new NuxtLink({ to: `/admin/${id}/action-bar#hash` })); 87 | assertType(new NuxtLink({ to: `/admin/${id}/action-bar?query=bar` })); 88 | assertType(new NuxtLink({ to: '/admin/foo/profile/' })); 89 | assertType(new NuxtLink({ to: `/admin/${id}/settings` })); 90 | assertType(new NuxtLink({ to: '/admin/panel/' })); 91 | assertType(new NuxtLink({ to: '/admin/panel/938783/' })); 92 | assertType(new NuxtLink({ to: '/user/38873-' })); 93 | assertType(new NuxtLink({ to: '/user/38673/bar/#hash' })); 94 | assertType(new NuxtLink({ to: '/user/ç9737/foo/articles?baz=foo' })); 95 | assertType(new NuxtLink({ to: '/user/catch/1/2' })); 96 | assertType(new NuxtLink({ to: '/user/test-' })); 97 | assertType(new NuxtLink({ to: '/user' })); 98 | 99 | // $ ----- Should be valid ✅ 100 | 101 | assertType(new NuxtLink({ to: { name: 'index' } })); 102 | assertType(new NuxtLink({ to: { name: 'user-id', params: { id: 1 }, hash: 'baz' } })); 103 | assertType(new NuxtLink({ to: { name: 'user-foo-bar', params: { foo: 'bar' }, force: true } })); 104 | assertType( 105 | new NuxtLink({ 106 | to: { name: 'user-foo-bar', params: { foo: 'bar', bar: 'baz' } }, 107 | }) 108 | ); 109 | assertType(new NuxtLink({ to: { name: 'user-catch-slug', params: { slug: ['foo'] } } })); 110 | assertType(new NuxtLink({ to: { name: 'user-catch-slug', params: { slug: [1, 2, 3] } } })); 111 | assertType(new NuxtLink({ to: { name: 'user-one-foo-two', params: { one: 1, two: '2' } } })); 112 | assertType( 113 | new NuxtLink({ 114 | to: { name: 'user-id-slug', params: { slug: '2' }, query: { foo: 'bar' } }, 115 | }) 116 | ); 117 | 118 | assertType( 119 | new NuxtLink({ 120 | to: { name: 'test-module', params: { foo: 1 }, query: { foo: 'bar' } }, 121 | }) 122 | ); 123 | 124 | // - With External prop 125 | 126 | // $ ----- Should be valid ✅ 127 | 128 | assertType(new NuxtLink({ to: '/admin/:id/', external: false })); 129 | assertType(new NuxtLink({ to: 'http://google.com', external: true })); 130 | -------------------------------------------------------------------------------- /test/fixtures/complex/tests/router/useRouter.spec-d.ts: -------------------------------------------------------------------------------- 1 | import { assertType } from 'vitest'; 2 | import type { TypedRouter, } from '@typed-router'; 3 | import { useRouter } from '@typed-router'; 4 | 5 | // @ts-expect-error Ensure global imports are disabled 6 | declare const globalDecl: (typeof globalThis)['useRouter']; 7 | 8 | // Given 9 | const router = useRouter(); 10 | 11 | assertType(router); 12 | 13 | // - Usage of useRouter with useRouter 14 | 15 | // ! ------ Should Error ❌ 16 | 17 | // * index.vue 18 | // @ts-expect-error 19 | router.push({ name: 'index', params: { id: 1 } }); 20 | // @ts-expect-error 21 | router.push({ name: 'index', params: { id: 1 } }); 22 | // @ts-expect-error 23 | router.push({ name: 'blabla-baguette' }); 24 | 25 | // * --- [id].vue 26 | // @ts-expect-error 27 | router.push({ name: 'user-id' }); 28 | // @ts-expect-error 29 | router.push({ name: 'user-id', params: { foo: 'bar' } }); 30 | 31 | // * --- [foo]-[[bar]].vue 32 | // @ts-expect-error 33 | router.push({ name: 'user-foo-bar' }); 34 | // @ts-expect-error 35 | router.push({ name: 'user-foo-bar', params: { bar: 1 } }); 36 | 37 | // * --- [...slug].vue 38 | // @ts-expect-error 39 | router.push({ name: 'user-slug' }); 40 | // @ts-expect-error 41 | router.push({ name: 'user-slug', params: { slug: 1 } }); 42 | 43 | // * --- [one]-foo-[two].vue 44 | // @ts-expect-error 45 | router.push({ name: 'user-one-foo-two' }); 46 | // @ts-expect-error 47 | router.push({ name: 'user-one-foo-two', params: { one: 1 } }); 48 | 49 | // * --- [id]/[slug].vue 50 | // @ts-expect-error 51 | router.push({ name: 'user-id-slug' }); 52 | // @ts-expect-error 53 | router.push({ name: 'user-id-slug', params: { id: 1 } }); 54 | 55 | // * --- Routes added by modules 56 | // @ts-expect-error 57 | router.push({ name: 'test-module' }); 58 | 59 | // $ ----- Should be valid ✅ 60 | 61 | router.push({ name: 'index' }); 62 | router.push({ name: 'user-id', params: { id: 1 }, hash: 'baz' }); 63 | router.push({ name: 'user-foo-bar', params: { foo: 'bar' }, force: true }); 64 | router.push({ name: 'user-foo-bar', params: { foo: 'bar', bar: 'baz' } }); 65 | router.push({ name: 'user-catch-slug', params: { slug: ['foo'] } }); 66 | router.push({ name: 'user-catch-slug', params: { slug: [1, 2, 3] } }); 67 | router.push({ name: 'user-one-foo-two', params: { one: 1, two: '2' } }); 68 | router.push({ name: 'user-id-slug', params: { slug: '2' }, query: { foo: 'bar' } }); 69 | router.push({ name: 'test-module', params: { foo: 1 }, query: { foo: 'bar' } }); 70 | 71 | router.replace({ name: 'index' }); 72 | router.replace({ name: 'user-id', params: { id: 1 }, hash: 'baz' }); 73 | 74 | // --- Path navigation 75 | 76 | // ! ------ Should Error ❌ 77 | 78 | // @ts-expect-error 79 | assertType(router.push('')); 80 | // @ts-expect-error 81 | assertType(router.push('/admin ')); 82 | // @ts-expect-error 83 | assertType(router.push('/admin/ /')); 84 | // @ts-expect-error 85 | assertType(router.push(`/ / // / / eefzr`)); 86 | // @ts-expect-error 87 | assertType(router.push('/elzhlzehflzhef')); 88 | // @ts-expect-error 89 | assertType(router.push('/admin/foo/bar')); 90 | // @ts-expect-error 91 | assertType(router.push('/admin/foo/bar/baz')); 92 | // @ts-expect-error 93 | assertType(router.push(`/admin/${id}/action-bar/taz?query`)); 94 | // @ts-expect-error 95 | assertType(router.push('/admin/panel/3O9393/bar')); 96 | // @ts-expect-error 97 | assertType(router.push('/admin/foo/ profile/ezfje')); 98 | // @ts-expect-error 99 | assertType(router.push('/admin/3U93U/settings/baz')); 100 | // @ts-expect-error 101 | assertType(router.push('/admin/panel/?fjzk')); 102 | 103 | // $ ----- Should be valid ✅ 104 | 105 | const id = '38789803'; 106 | assertType(router.push('/')); 107 | assertType(router.push('/baguette')); 108 | assertType(router.push('/admin/foo')); 109 | assertType(router.push('/admin/foo/')); 110 | assertType(router.push(`/admin/${id}/action-bar#hash`)); 111 | assertType(router.push(`/admin/${id}/action-bar?query=bar`)); 112 | assertType(router.push('/admin/foo/profile/')); 113 | assertType(router.push(`/admin/${id}/settings`)); 114 | assertType(router.push('/admin/panel/')); 115 | assertType(router.push('/admin/panel/938783/')); 116 | assertType(router.push('/user/38873-')); 117 | assertType(router.push('/user/38673/bar/#hash')); 118 | assertType(router.push('/user/ç9737/foo/articles?baz=foo')); 119 | assertType(router.push('/user/catch/1/2')); 120 | assertType(router.push('/user/test-')); 121 | assertType(router.push('/user')); 122 | 123 | // * Resolved routes 124 | 125 | const resolved1 = router.resolve({ name: 'index' }); 126 | assertType<'index'>(resolved1.name); 127 | // @ts-expect-error 128 | assertType<'index'>(resolved1.params); 129 | 130 | const resolved2 = router.resolve({ name: 'user-id', params: { id: 1 }, hash: 'baz' }); 131 | assertType<'user-id'>(resolved2.name); 132 | assertType<{ id: string }>(resolved2.params); 133 | -------------------------------------------------------------------------------- /test/fixtures/complex/tests/routes/useRoute.spec-d.ts: -------------------------------------------------------------------------------- 1 | import { assertType, test } from 'vitest'; 2 | import type { RouteLocationMatched } from 'vue-router'; 3 | import type { TypedRouteFromName } from '@typed-router'; 4 | import {useRoute} from '@typed-router' 5 | 6 | // @ts-expect-error Ensure global imports are disabled 7 | declare const globalDecl: (typeof globalThis)['useRoute']; 8 | 9 | // Given 10 | const route = useRoute(); 11 | 12 | test('Basic', () => { 13 | const namedRoute = useRoute('user-foo-bar'); 14 | 15 | assertType>(namedRoute); 16 | assertType<'user-foo-bar'>(namedRoute.name); 17 | assertType<{ 18 | foo: string; 19 | bar?: string | undefined; 20 | }>(namedRoute.params); 21 | assertType(namedRoute.fullPath); 22 | assertType(namedRoute.hash); 23 | assertType(namedRoute.path); 24 | assertType(namedRoute.matched); 25 | }); 26 | 27 | // * index.vue 28 | 29 | if (route.name === 'index') { 30 | assertType<'index'>(route.name); 31 | assertType(route.params); 32 | } 33 | 34 | // * --- [id].vue 35 | if (route.name === 'user-id') { 36 | assertType>(route); 37 | assertType<'user-id'>(route.name); 38 | assertType<{ id: string }>(route.params); 39 | } 40 | 41 | // * --- [foo]-[[bar]].vue 42 | if (route.name === 'user-foo-bar') { 43 | assertType>(route); 44 | assertType<'user-foo-bar'>(route.name); 45 | assertType<{ 46 | foo: string; 47 | bar?: string | undefined; 48 | }>(route.params); 49 | } 50 | 51 | // * --- [...slug].vue 52 | if (route.name === 'user-catch-slug') { 53 | assertType>(route); 54 | assertType<'user-catch-slug'>(route.name); 55 | assertType<{ 56 | slug: string[]; 57 | }>(route.params); 58 | } 59 | 60 | // * --- [one]-foo-[two].vue 61 | if (route.name === 'user-one-foo-two') { 62 | assertType>(route); 63 | assertType<'user-one-foo-two'>(route.name); 64 | assertType<{ 65 | one: string; 66 | two: string; 67 | }>(route.params); 68 | } 69 | 70 | // * --- [id]/[slug].vue 71 | if (route.name === 'user-id-slug') { 72 | assertType>(route); 73 | assertType<'user-id-slug'>(route.name); 74 | assertType<{ 75 | id: string; 76 | slug: string; 77 | }>(route.params); 78 | } 79 | 80 | // * --- Routes added by modules 81 | if (route.name === 'test-module') { 82 | assertType>(route); 83 | assertType<'test-module'>(route.name); 84 | assertType<{ 85 | foo: string; 86 | }>(route.params); 87 | } 88 | -------------------------------------------------------------------------------- /test/fixtures/complex/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/simple/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log* 3 | .nuxt 4 | .nitro 5 | .cache 6 | .output 7 | .env 8 | dist 9 | -------------------------------------------------------------------------------- /test/fixtures/simple/README.md: -------------------------------------------------------------------------------- 1 | # Nuxt 3 Minimal Starter 2 | 3 | Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. 4 | 5 | ## Setup 6 | 7 | Make sure to install the dependencies: 8 | 9 | ```bash 10 | # yarn 11 | yarn install 12 | 13 | # npm 14 | npm install 15 | 16 | # pnpm 17 | pnpm install 18 | ``` 19 | 20 | ## Development Server 21 | 22 | Start the development server on http://localhost:3000 23 | 24 | ```bash 25 | npm run dev 26 | ``` 27 | 28 | ## Production 29 | 30 | Build the application for production: 31 | 32 | ```bash 33 | npm run build 34 | ``` 35 | 36 | Locally preview production build: 37 | 38 | ```bash 39 | npm run preview 40 | ``` 41 | 42 | Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. 43 | -------------------------------------------------------------------------------- /test/fixtures/simple/app.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /test/fixtures/simple/app/pages/admin.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/simple/app/pages/admin/[id].vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/simple/app/pages/admin/[id]/action-[slug].vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/simple/app/pages/admin/[id]/index.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/simple/app/pages/admin/[id]/profile.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/simple/app/pages/admin/[id]/settings.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/simple/app/pages/admin/panel/[[blou]].vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/simple/app/pages/baguette.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/simple/app/pages/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 25 | -------------------------------------------------------------------------------- /test/fixtures/simple/app/pages/user/[foo]-[[bar]].vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/simple/app/pages/user/[id].vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/simple/app/pages/user/[id]/[slug].vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/simple/app/pages/user/[id]/[slug]/articles.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/simple/app/pages/user/[id]/[slug]/index.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /test/fixtures/simple/app/pages/user/[id]/index.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/simple/app/pages/user/[id]/posts.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/simple/app/pages/user/[one]-foo-[two].vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/simple/app/pages/user/catch/[...slug].vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/simple/app/pages/user/index.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/simple/app/pages/user/test-[[optional]].vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/simple/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtConfig({ 2 | modules: ['nuxt-typed-router'], 3 | future: { 4 | compatibilityVersion: 4, 5 | }, 6 | vite: { 7 | resolve: { 8 | dedupe: ['vue-router'], 9 | }, 10 | }, 11 | compatibilityDate: '2025-01-07', 12 | }); 13 | -------------------------------------------------------------------------------- /test/fixtures/simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "simple", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "build": "nuxt build", 7 | "dev": "nuxt dev", 8 | "generate": "nuxt generate", 9 | "preview": "nuxt preview" 10 | }, 11 | "devDependencies": { 12 | "nuxt": "3.17.5", 13 | "nuxt-typed-router": "workspace:*", 14 | "vue": "3.5.16" 15 | } 16 | } -------------------------------------------------------------------------------- /test/fixtures/simple/tests/misc/definePageMeta.spec-d.ts: -------------------------------------------------------------------------------- 1 | import { assertType } from 'vitest'; 2 | import { definePageMeta } from '@typed-router'; 3 | 4 | // Given 5 | 6 | // - Usage of useRouter with useRouter 7 | 8 | // ! ------ Should Error ❌ 9 | 10 | // * index.vue 11 | definePageMeta({ redirect: { name: 'index' } }); 12 | // @ts-expect-error 13 | definePageMeta({ redirect: { name: 'index', params: { id: 1 } } }); 14 | // @ts-expect-error 15 | definePageMeta({ redirect: { name: 'blabla-baguette' } }); 16 | 17 | // * --- [id].vue 18 | // @ts-expect-error 19 | definePageMeta({ redirect: { name: 'user-id' } }); 20 | // @ts-expect-error 21 | definePageMeta({ redirect: { name: 'user-id', params: { foo: 'bar' } } }); 22 | 23 | // * --- [foo]-[[bar]].vue 24 | // @ts-expect-error 25 | definePageMeta({ redirect: { name: 'user-foo-bar' } }); 26 | // @ts-expect-error 27 | definePageMeta({ redirect: { name: 'user-foo-bar', params: { bar: 1 } } }); 28 | 29 | // * --- [...slug].vue 30 | // @ts-expect-error 31 | definePageMeta({ redirect: { name: 'user-slug' } }); 32 | // @ts-expect-error 33 | definePageMeta({ redirect: { name: 'user-slug', params: { slug: 1 } } }); 34 | 35 | // * --- [one]-foo-[two].vue 36 | // @ts-expect-error 37 | definePageMeta({ redirect: { name: 'user-one-foo-two' } }); 38 | // @ts-expect-error 39 | definePageMeta({ redirect: { name: 'user-one-foo-two', params: { one: 1 } } }); 40 | 41 | // * --- [id]/[slug].vue 42 | // @ts-expect-error 43 | definePageMeta({ redirect: { name: 'user-id-slug' } }); 44 | // @ts-expect-error 45 | definePageMeta({ redirect: { name: 'user-id-slug', params: { id: 1 } } }); 46 | 47 | // * --- Routes added by modules 48 | // @ts-expect-error 49 | definePageMeta({ redirect: { name: 'test-module' } }); 50 | 51 | // * --- Path navigation 52 | // @ts-expect-error 53 | definePageMeta({ redirect: '/fooooooooooo' }); 54 | 55 | // @ts-expect-error 56 | definePageMeta({ redirect: { path: '/foo' } }); 57 | 58 | // $ ----- Should be valid ✅ 59 | 60 | definePageMeta({ redirect: { name: 'index' } }); 61 | definePageMeta({ redirect: { name: 'user-id', params: { id: 1 }, hash: 'baz' } }); 62 | definePageMeta({ redirect: { name: 'user-foo-bar', params: { foo: 'bar' }, force: true } }); 63 | definePageMeta({ redirect: { name: 'user-foo-bar', params: { foo: 'bar', bar: 'baz' } } }); 64 | definePageMeta({ redirect: { name: 'user-catch-slug', params: { slug: ['foo'] } } }); 65 | definePageMeta({ redirect: { name: 'user-catch-slug', params: { slug: [1, 2, 3] } } }); 66 | definePageMeta({ redirect: { name: 'user-one-foo-two', params: { one: 1, two: '2' } } }); 67 | definePageMeta({ 68 | redirect: { name: 'user-id-slug', params: { slug: '2' }, query: { foo: 'bar' } }, 69 | }); 70 | 71 | // --- Path navigation 72 | 73 | // ! ------ Should Error ❌ 74 | 75 | // @ts-expect-error 76 | assertType(definePageMeta({ redirect: '' })); 77 | // @ts-expect-error 78 | assertType(definePageMeta({ redirect: '/admin ' })); 79 | // @ts-expect-error 80 | assertType(definePageMeta({ redirect: '/admin/ /' })); 81 | // @ts-expect-error 82 | assertType(definePageMeta({ redirect: `/ / // / / eefzr` })); 83 | // @ts-expect-error 84 | assertType(definePageMeta({ redirect: '/elzhlzehflzhef' })); 85 | // @ts-expect-error 86 | assertType(definePageMeta({ redirect: '/admin/foo/bar' })); 87 | // @ts-expect-error 88 | assertType(definePageMeta({ redirect: '/admin/foo/bar/baz' })); 89 | // @ts-expect-error 90 | assertType(definePageMeta({ redirect: `/admin/${id}/action-bar/taz?query` })); 91 | // @ts-expect-error 92 | assertType(definePageMeta({ redirect: '/admin/panel/3O9393/bar' })); 93 | // @ts-expect-error 94 | assertType(definePageMeta({ redirect: '/admin/foo/ profile/ezfje' })); 95 | // @ts-expect-error 96 | assertType(definePageMeta({ redirect: '/admin/3U93U/settings/baz' })); 97 | // @ts-expect-error 98 | assertType(definePageMeta({ redirect: '/admin/panel/?fjzk' })); 99 | 100 | // $ ----- Should be valid ✅ 101 | 102 | const id = '38789803'; 103 | assertType(definePageMeta({ redirect: '/' })); 104 | assertType(definePageMeta({ redirect: '/baguette' })); 105 | assertType(definePageMeta({ redirect: '/admin/foo' })); 106 | assertType(definePageMeta({ redirect: '/admin/foo/' })); 107 | assertType(definePageMeta({ redirect: `/admin/${id}/action-bar#hash` })); 108 | assertType(definePageMeta({ redirect: `/admin/${id}/action-bar?query=bar` })); 109 | assertType(definePageMeta({ redirect: '/admin/foo/profile/' })); 110 | assertType(definePageMeta({ redirect: `/admin/${id}/settings` })); 111 | assertType(definePageMeta({ redirect: '/admin/panel/' })); 112 | assertType(definePageMeta({ redirect: '/admin/panel/938783/' })); 113 | assertType(definePageMeta({ redirect: '/user/38873-' })); 114 | assertType(definePageMeta({ redirect: '/user/38673/bar/#hash' })); 115 | assertType(definePageMeta({ redirect: '/user/ç9737/foo/articles?baz=foo' })); 116 | assertType(definePageMeta({ redirect: '/user/catch/1/2' })); 117 | assertType(definePageMeta({ redirect: '/user/test-' })); 118 | assertType(definePageMeta({ redirect: '/user' })); 119 | -------------------------------------------------------------------------------- /test/fixtures/simple/tests/router/NuxtLink.spec-d.ts: -------------------------------------------------------------------------------- 1 | import { assertType, vi } from 'vitest'; 2 | import type { TypedNuxtLink } from '../../.nuxt/typed-router/typed-router'; 3 | 4 | const NuxtLink: TypedNuxtLink = vi.fn() as any; 5 | 6 | // ! ------ Should Error ❌ 7 | 8 | // * index.vue 9 | // @ts-expect-error 10 | assertType(new NuxtLink({ to: { name: 'index', params: { id: 1 } } })); 11 | // @ts-expect-error 12 | assertType(new NuxtLink({ to: { name: 'index', params: { id: 1 } } })); 13 | // @ts-expect-error 14 | assertType(new NuxtLink({ to: { name: 'blabla-baguette' } })); 15 | 16 | // * --- [id].vue 17 | // @ts-expect-error 18 | assertType(new NuxtLink({ to: { name: 'user-id' } })); 19 | // @ts-expect-error 20 | assertType(new NuxtLink({ to: { name: 'user-id', params: { foo: 'bar' } } })); 21 | 22 | // * --- [foo]-[[bar]].vue 23 | // @ts-expect-error 24 | assertType(new NuxtLink({ to: { name: 'user-foo-bar' } })); 25 | // @ts-expect-error 26 | assertType(new NuxtLink({ to: { name: 'user-foo-bar', params: { bar: 1 } } })); 27 | 28 | // * --- [...slug].vue 29 | // @ts-expect-error 30 | assertType(new NuxtLink({ to: { name: 'user-catch-slug' } })); 31 | // @ts-expect-error 32 | assertType(new NuxtLink({ to: { name: 'user-catch-slug', params: { slug: 1 } } })); 33 | 34 | // * --- [one]-foo-[two].vue 35 | // @ts-expect-error 36 | assertType(new NuxtLink({ to: { name: 'user-one-foo-two' } })); 37 | // @ts-expect-error 38 | assertType(new NuxtLink({ to: { name: 'user-one-foo-two', params: { one: 1 } } })); 39 | 40 | // * --- [id]/[slug].vue 41 | // @ts-expect-error 42 | assertType(new NuxtLink({ to: { name: 'user-id-slug' } })); 43 | // @ts-expect-error 44 | assertType(new NuxtLink({ to: { name: 'user-id-slug', params: { id: 1 } } })); 45 | 46 | // * --- Routes added by modules 47 | // @ts-expect-error 48 | assertType(new NuxtLink({ to: { name: 'test-module' } })); 49 | 50 | // $ ----- Should be valid ✅ 51 | 52 | assertType(new NuxtLink({ to: { name: 'index' } })); 53 | assertType(new NuxtLink({ to: { name: 'user-id', params: { id: 1 }, hash: 'baz' } })); 54 | assertType(new NuxtLink({ to: { name: 'user-foo-bar', params: { foo: 'bar' }, force: true } })); 55 | assertType( 56 | new NuxtLink({ 57 | to: { name: 'user-foo-bar', params: { foo: 'bar', bar: 'baz' } }, 58 | }) 59 | ); 60 | assertType(new NuxtLink({ to: { name: 'user-catch-slug', params: { slug: ['foo'] } } })); 61 | assertType(new NuxtLink({ to: { name: 'user-catch-slug', params: { slug: [1, 2, 3] } } })); 62 | assertType(new NuxtLink({ to: { name: 'user-one-foo-two', params: { one: 1, two: '2' } } })); 63 | assertType( 64 | new NuxtLink({ 65 | to: { name: 'user-id-slug', params: { slug: '2' }, query: { foo: 'bar' } }, 66 | }) 67 | ); 68 | 69 | // --- Path navigation 70 | 71 | // ! ------ Should Error ❌ 72 | 73 | // @ts-expect-error 74 | assertType(new NuxtLink({ to: '' })); 75 | // @ts-expect-error 76 | assertType(new NuxtLink({ to: '/admin ' })); 77 | // @ts-expect-error 78 | assertType(new NuxtLink({ to: '/admin/ /' })); 79 | // @ts-expect-error 80 | assertType(new NuxtLink({ to: `/ / // / / eefzr` })); 81 | // @ts-expect-error 82 | assertType(new NuxtLink({ to: '/elzhlzehflzhef' })); 83 | // @ts-expect-error 84 | assertType(new NuxtLink({ to: '/admin/foo/bar' })); 85 | // @ts-expect-error 86 | assertType(new NuxtLink({ to: '/admin/foo/bar/baz' })); 87 | // @ts-expect-error 88 | assertType(new NuxtLink({ to: `/admin/${id}/action-bar/taz?query` })); 89 | // @ts-expect-error 90 | assertType(new NuxtLink({ to: '/admin/panel/3O9393/bar' })); 91 | // @ts-expect-error 92 | assertType(new NuxtLink({ to: '/admin/foo/ profile/ezfje' })); 93 | // @ts-expect-error 94 | assertType(new NuxtLink({ to: '/admin/3U93U/settings/baz' })); 95 | // @ts-expect-error 96 | assertType(new NuxtLink({ to: '/admin/panel/?fjzk' })); 97 | 98 | // $ ----- Should be valid ✅ 99 | 100 | const id = '38789803'; 101 | assertType(new NuxtLink({ to: '/' })); 102 | assertType(new NuxtLink({ to: '/baguette' })); 103 | assertType(new NuxtLink({ to: '/admin/foo' })); 104 | assertType(new NuxtLink({ to: '/admin/foo/' })); 105 | assertType(new NuxtLink({ to: `/admin/${id}/action-bar#hash` })); 106 | assertType(new NuxtLink({ to: `/admin/${id}/action-bar?query=bar` })); 107 | assertType(new NuxtLink({ to: '/admin/foo/profile/' })); 108 | assertType(new NuxtLink({ to: `/admin/${id}/settings` })); 109 | assertType(new NuxtLink({ to: '/admin/panel/' })); 110 | assertType(new NuxtLink({ to: '/admin/panel/938783/' })); 111 | assertType(new NuxtLink({ to: '/user/38873-' })); 112 | assertType(new NuxtLink({ to: '/user/38673/bar/#hash' })); 113 | assertType(new NuxtLink({ to: '/user/ç9737/foo/articles?baz=foo' })); 114 | assertType(new NuxtLink({ to: '/user/catch/1/2' })); 115 | assertType(new NuxtLink({ to: '/user/test-' })); 116 | assertType(new NuxtLink({ to: '/user' })); 117 | 118 | // - With External prop 119 | 120 | // $ ----- Should be valid ✅ 121 | 122 | assertType(new NuxtLink({ to: '/admin/:id/', external: false })); 123 | assertType(new NuxtLink({ to: 'http://google.com', external: true })); 124 | -------------------------------------------------------------------------------- /test/fixtures/simple/tests/router/useRouter.spec-d.ts: -------------------------------------------------------------------------------- 1 | import { assertType, test } from 'vitest'; 2 | import type { TypedRouter } from '@typed-router'; 3 | 4 | // Given 5 | const router = useRouter(); 6 | 7 | assertType(router); 8 | 9 | // - Usage of useRouter with useRouter 10 | 11 | // ! ------ Should Error ❌ 12 | 13 | // * index.vue 14 | // @ts-expect-error 15 | router.push({ name: 'index', params: { id: 1 } }); 16 | // @ts-expect-error 17 | router.push({ name: 'index', params: { id: 1 } }); 18 | // @ts-expect-error 19 | router.push({ name: 'blabla-baguette' }); 20 | 21 | // * --- [id].vue 22 | // @ts-expect-error 23 | router.push({ name: 'user-id' }); 24 | // @ts-expect-error 25 | router.push({ name: 'user-id', params: { foo: 'bar' } }); 26 | 27 | // * --- [foo]-[[bar]].vue 28 | // @ts-expect-error 29 | router.push({ name: 'user-foo-bar' }); 30 | // @ts-expect-error 31 | router.push({ name: 'user-foo-bar', params: { bar: 1 } }); 32 | 33 | // * --- [...slug].vue 34 | // @ts-expect-error 35 | router.push({ name: 'user-slug' }); 36 | // @ts-expect-error 37 | router.push({ name: 'user-slug', params: { slug: 1 } }); 38 | 39 | // * --- [one]-foo-[two].vue 40 | // @ts-expect-error 41 | router.push({ name: 'user-one-foo-two' }); 42 | // @ts-expect-error 43 | router.push({ name: 'user-one-foo-two', params: { one: 1 } }); 44 | 45 | // * --- [id]/[slug].vue 46 | // @ts-expect-error 47 | router.push({ name: 'user-id-slug' }); 48 | // @ts-expect-error 49 | router.push({ name: 'user-id-slug', params: { id: 1 } }); 50 | 51 | // * --- Routes added by modules 52 | // @ts-expect-error 53 | router.push({ name: 'test-module' }); 54 | 55 | // * --- Path navigation 56 | // @ts-expect-error 57 | router.push('/fooooooooooo'); 58 | // @ts-expect-error 59 | router.push({ path: '/foo' }); 60 | 61 | // $ ----- Should be valid ✅ 62 | 63 | router.push({ name: 'index' }); 64 | router.push({ name: 'user-id', params: { id: 1 }, hash: 'baz' }); 65 | router.push({ name: 'user-foo-bar', params: { foo: 'bar' }, force: true }); 66 | router.push({ name: 'user-foo-bar', params: { foo: 'bar', bar: 'baz' } }); 67 | router.push({ name: 'user-catch-slug', params: { slug: ['foo'] } }); 68 | router.push({ name: 'user-catch-slug', params: { slug: [1, 2, 3] } }); 69 | router.push({ name: 'user-one-foo-two', params: { one: 1, two: '2' } }); 70 | router.push({ name: 'user-id-slug', params: { slug: '2' }, query: { foo: 'bar' } }); 71 | 72 | router.replace({ name: 'index' }); 73 | router.replace({ name: 'user-id', params: { id: 1 }, hash: 'baz' }); 74 | router.replace('/admin'); 75 | 76 | // --- Path navigation 77 | 78 | // ! ------ Should Error ❌ 79 | 80 | // @ts-expect-error 81 | assertType(router.push('')); 82 | // @ts-expect-error 83 | assertType(router.push('/admin ')); 84 | // @ts-expect-error 85 | assertType(router.push('/admin/ /')); 86 | // @ts-expect-error 87 | assertType(router.push(`/ / // / / eefzr`)); 88 | // @ts-expect-error 89 | assertType(router.push('/elzhlzehflzhef')); 90 | // @ts-expect-error 91 | assertType(router.push('/admin/foo/bar')); 92 | // @ts-expect-error 93 | assertType(router.push('/admin/foo/bar/baz')); 94 | // @ts-expect-error 95 | assertType(router.push(`/admin/${id}/action-bar/taz?query`)); 96 | // @ts-expect-error 97 | assertType(router.push('/admin/panel/3O9393/bar')); 98 | // @ts-expect-error 99 | assertType(router.push('/admin/foo/ profile/ezfje')); 100 | // @ts-expect-error 101 | assertType(router.push('/admin/3U93U/settings/baz')); 102 | // @ts-expect-error 103 | assertType(router.push('/admin/panel/?fjzk')); 104 | 105 | // $ ----- Should be valid ✅ 106 | 107 | const id = '38789803'; 108 | assertType(router.push('/')); 109 | assertType(router.push('/baguette')); 110 | assertType(router.push('/admin/foo')); 111 | assertType(router.push('/admin/foo/')); 112 | assertType(router.push(`/admin/${id}/action-bar#hash`)); 113 | assertType(router.push(`/admin/${id}/action-bar?query=bar`)); 114 | assertType(router.push('/admin/foo/profile/')); 115 | assertType(router.push(`/admin/${id}/settings`)); 116 | assertType(router.push('/admin/panel/')); 117 | assertType(router.push('/admin/panel/938783/')); 118 | assertType(router.push('/user/38873-')); 119 | assertType(router.push('/user/38673/bar/#hash')); 120 | assertType(router.push('/user/ç9737/foo/articles?baz=foo')); 121 | assertType(router.push('/user/catch/1/2')); 122 | assertType(router.push('/user/test-')); 123 | assertType(router.push('/user')); 124 | 125 | // * Resolved routes 126 | 127 | test('', () => { 128 | const resolved = router.resolve({ name: 'index' }); 129 | assertType<'index'>(resolved.name); 130 | // @ts-expect-error 131 | assertType<'index'>(resolved.params); 132 | }); 133 | 134 | test('', () => { 135 | const resolved = router.resolve({ name: 'user-id', params: { id: 1 }, hash: 'baz' }); 136 | assertType<'user-id'>(resolved.name); 137 | assertType<{ id: string }>(resolved.params); 138 | 139 | // @ts-expect-error 140 | assertType<'user-eojzpejfze'>(resolved.name); 141 | }); 142 | 143 | test('', () => { 144 | const resolved = router.resolve('/admin/foo/'); 145 | assertType<'admin-id'>(resolved.name); 146 | assertType<{ id: string }>(resolved.params); 147 | 148 | // @ts-expect-error 149 | assertType<'jzeifjlfej'>(resolved.name); 150 | // @ts-expect-error 151 | assertType<{ foo: string }>(resolved.params); 152 | }); 153 | -------------------------------------------------------------------------------- /test/fixtures/simple/tests/routes/useRoute.spec-d.ts: -------------------------------------------------------------------------------- 1 | import { assertType, test } from 'vitest'; 2 | import type { RouteLocationMatched } from 'vue-router'; 3 | import type { TypedRouteFromName } from '@typed-router'; 4 | 5 | // Given 6 | const route = useRoute(); 7 | 8 | test('Basic', () => { 9 | const namedRoute = useRoute('user-foo-bar'); 10 | 11 | assertType>(namedRoute); 12 | assertType<'user-foo-bar'>(namedRoute.name); 13 | assertType<{ 14 | foo: string; 15 | bar?: string | undefined; 16 | }>(namedRoute.params); 17 | assertType(namedRoute.fullPath); 18 | assertType(namedRoute.hash); 19 | assertType(namedRoute.path); 20 | assertType(namedRoute.matched); 21 | }); 22 | 23 | // * index.vue 24 | 25 | if (route.name === 'index') { 26 | assertType<'index'>(route.name); 27 | assertType(route.params); 28 | } 29 | 30 | // * --- [id].vue 31 | if (route.name === 'user-id') { 32 | assertType>(route); 33 | assertType<'user-id'>(route.name); 34 | assertType<{ id: string }>(route.params); 35 | } 36 | 37 | // * --- [foo]-[[bar]].vue 38 | if (route.name === 'user-foo-bar') { 39 | assertType>(route); 40 | assertType<'user-foo-bar'>(route.name); 41 | assertType<{ 42 | foo: string; 43 | bar?: string | undefined; 44 | }>(route.params); 45 | } 46 | 47 | // * --- [...slug].vue 48 | if (route.name === 'user-catch-slug') { 49 | assertType>(route); 50 | assertType<'user-catch-slug'>(route.name); 51 | assertType<{ 52 | slug: string[]; 53 | }>(route.params); 54 | } 55 | 56 | // * --- [one]-foo-[two].vue 57 | if (route.name === 'user-one-foo-two') { 58 | assertType>(route); 59 | assertType<'user-one-foo-two'>(route.name); 60 | assertType<{ 61 | one: string; 62 | two: string; 63 | }>(route.params); 64 | } 65 | 66 | // * --- [id]/[slug].vue 67 | if (route.name === 'user-id-slug') { 68 | assertType>(route); 69 | assertType<'user-id-slug'>(route.name); 70 | assertType<{ 71 | id: string; 72 | slug: string; 73 | }>(route.params); 74 | } 75 | -------------------------------------------------------------------------------- /test/fixtures/simple/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json", 4 | "compilerOptions": { 5 | "verbatimModuleSyntax": false 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/withOptions/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log* 3 | .nuxt 4 | .nitro 5 | .cache 6 | .output 7 | .env 8 | dist 9 | -------------------------------------------------------------------------------- /test/fixtures/withOptions/README.md: -------------------------------------------------------------------------------- 1 | # Nuxt 3 Minimal Starter 2 | 3 | Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. 4 | 5 | ## Setup 6 | 7 | Make sure to install the dependencies: 8 | 9 | ```bash 10 | # yarn 11 | yarn install 12 | 13 | # npm 14 | npm install 15 | 16 | # pnpm 17 | pnpm install 18 | ``` 19 | 20 | ## Development Server 21 | 22 | Start the development server on http://localhost:3000 23 | 24 | ```bash 25 | npm run dev 26 | ``` 27 | 28 | ## Production 29 | 30 | Build the application for production: 31 | 32 | ```bash 33 | npm run build 34 | ``` 35 | 36 | Locally preview production build: 37 | 38 | ```bash 39 | npm run preview 40 | ``` 41 | 42 | Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. 43 | -------------------------------------------------------------------------------- /test/fixtures/withOptions/app.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /test/fixtures/withOptions/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import NuxtTypedRouter from '../../..'; 2 | 3 | export default defineNuxtConfig({ 4 | modules: [NuxtTypedRouter], 5 | vite: { 6 | resolve: { 7 | dedupe: ['vue-router'], 8 | }, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /test/fixtures/withOptions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "with-options", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "build": "nuxt build", 7 | "dev": "nuxt dev", 8 | "generate": "nuxt generate", 9 | "preview": "nuxt preview" 10 | }, 11 | "devDependencies": { 12 | "nuxt": "3.17.5", 13 | "nuxt-typed-router": "workspace:*" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/fixtures/withOptions/pages/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /test/fixtures/withOptions/pages/user/[...slug].vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/withOptions/pages/user/[foo]-[[bar]].vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/withOptions/pages/user/[id].vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/withOptions/pages/user/[id]/[slug].vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/withOptions/pages/user/[id]/[slug]/articles.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/withOptions/pages/user/[id]/[slug]/index.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /test/fixtures/withOptions/pages/user/[id]/index.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/withOptions/pages/user/[id]/posts.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/withOptions/pages/user/[one]-foo-[two].vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/withOptions/pages/user/index.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/withOptions/pages/user/test-[[optional]].vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/withOptions/tests/e2e/withPartialStrict.spec.ts: -------------------------------------------------------------------------------- 1 | test('empty') 2 | 3 | // import { fileURLToPath } from 'node:url'; 4 | // import { setup } from '@nuxt/test-utils'; 5 | // import { assertType } from 'vitest'; 6 | // import { useRouter } from '../../.nuxt/typed-router'; 7 | // import type { TypedNuxtLink } from '../../.nuxt/typed-router/typed-router'; 8 | 9 | // test.skip('The strict option should behave correctly with partial strict options', async () => { 10 | // await setup({ 11 | // rootDir: fileURLToPath(new URL('../../fixtures/withOptions', import.meta.url)), 12 | // setupTimeout: 120000, 13 | // nuxtConfig: { 14 | // nuxtTypedRouter: { 15 | // strict: { 16 | // NuxtLink: { 17 | // strictRouteLocation: true, 18 | // }, 19 | // router: { 20 | // strictToArgument: true, 21 | // }, 22 | // }, 23 | // }, 24 | // }, 25 | // } as any); 26 | 27 | // // const diagnostic = await runTypesDiagnostics(__dirname, __filename); 28 | 29 | // // expect(diagnostic.length).toBe(0); 30 | 31 | // const NuxtLink: TypedNuxtLink = vi.fn() as any; 32 | 33 | // const router = { push: vi.fn() } as unknown as ReturnType; 34 | 35 | // assertType(router.push('/user')); 36 | // // @ts-expect-error 37 | // assertType(router.push({ path: '/login' })); 38 | 39 | // // @ts-expect-error 40 | // assertType(new NuxtLink('/user')); 41 | // assertType(new NuxtLink({ to: { path: '/user' } })); 42 | // }); 43 | -------------------------------------------------------------------------------- /test/fixtures/withOptions/tests/e2e/withStrict.spec.ts: -------------------------------------------------------------------------------- 1 | test('empty') 2 | // import { setup } from '@nuxt/test-utils'; 3 | // import { fileURLToPath } from 'node:url'; 4 | // import { assertType } from 'vitest'; 5 | // import { useRouter } from '../../.nuxt/typed-router'; 6 | // import type { TypedNuxtLink } from '../../.nuxt/typed-router/typed-router'; 7 | 8 | // test.skip('The strict option should behave correctly with strict: true', async () => { 9 | // await setup({ 10 | // rootDir: fileURLToPath(new URL('../../fixtures/withOptions', import.meta.url)), 11 | // setupTimeout: 120000, 12 | // nuxtConfig: { 13 | // nuxtTypedRouter: { 14 | // strict: true, 15 | // }, 16 | // }, 17 | // } as any); 18 | 19 | // // const diagnostic = await runTypesDiagnostics(__dirname, __filename); 20 | 21 | // // expect(diagnostic.length).toBe(0); 22 | 23 | // const NuxtLink: TypedNuxtLink = vi.fn() as any; 24 | 25 | // const router = { push: vi.fn() } as unknown as ReturnType; 26 | 27 | // // @ts-expect-error 28 | // assertType(router.push('/foo')); 29 | // // @ts-expect-error 30 | // assertType(router.push({ path: '/login' })); 31 | 32 | // // @ts-expect-error 33 | // assertType(new NuxtLink('/login')); 34 | // // @ts-expect-error 35 | // assertType(new NuxtLink({ path: '/goooo' })); 36 | // }); 37 | 38 | -------------------------------------------------------------------------------- /test/fixtures/withOptions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["vitest/globals"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "preserve", 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "allowJs": true, 10 | "noEmit": true, 11 | "resolveJsonModule": true, 12 | "allowSyntheticDefaultImports": true, 13 | "types": ["node", "vitest/globals"], 14 | "paths": { 15 | "$$/*": ["./*"] 16 | } 17 | }, 18 | "include": ["./**/*.ts", "./fixtures/**/.nuxt/**/*.ts", "./**/*.tsx"] 19 | } 20 | -------------------------------------------------------------------------------- /test/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './tsd.utils'; 2 | export * from './typecheck'; 3 | 4 | export function timeout(count: number) { 5 | return new Promise((resolve) => setTimeout(resolve, count)); 6 | } 7 | -------------------------------------------------------------------------------- /test/utils/tsd.utils.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import tsd from 'tsd'; 3 | 4 | export async function runTypesDiagnostics(dirName: string, fileName: string) { 5 | const diagnostic = await tsd({ 6 | cwd: dirName, 7 | testFiles: [path.basename(fileName)], 8 | typingsFile: `./${path.basename(fileName)}`, 9 | }); 10 | 11 | if (diagnostic.length) { 12 | console.error( 13 | diagnostic.map( 14 | (m) => `Error in file ${m.fileName}:${m.line}:${m.column}: 15 | ${m.message}` 16 | ) 17 | ); 18 | } 19 | 20 | return diagnostic; 21 | } 22 | -------------------------------------------------------------------------------- /test/utils/typecheck.ts: -------------------------------------------------------------------------------- 1 | // Check required param 2 | export function required(arg: string) {} 3 | 4 | // Check optional param 5 | export function optional(arg: undefined extends T ? T : never) {} 6 | 7 | // Check array params 8 | export function array(arg: string[]) {} 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./playground/.nuxt/tsconfig.json", 3 | "compilerOptions": { 4 | "module": "ESNext" 5 | }, 6 | "include": ["./src/**/*.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { defineConfig } from 'vitest/config'; 3 | 4 | export default defineConfig({ 5 | test: { 6 | globals: true, 7 | testTimeout: 10000, 8 | threads: false, 9 | }, 10 | resolve: { 11 | alias: { 12 | $$: path.resolve(__dirname, './test'), 13 | }, 14 | }, 15 | }); 16 | --------------------------------------------------------------------------------