├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .vscode └── settings.json ├── README.md ├── apps └── demo │ ├── .eslintrc.js │ ├── .gitignore │ ├── app │ ├── [dynamic] │ │ ├── [nested] │ │ │ └── page.tsx │ │ ├── page │ │ │ └── page.tsx │ │ └── route │ │ │ └── route.ts │ ├── catch-all │ │ └── [...catchall] │ │ │ └── page.tsx │ ├── layout.tsx │ ├── optional-catch-all │ │ └── [[...oca]] │ │ │ └── page.tsx │ ├── page.tsx │ ├── parallel-route │ │ ├── @test │ │ │ ├── [dynamic] │ │ │ │ └── default.tsx │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ └── page.tsx │ └── special-char │ │ └── [special-char] │ │ └── page.tsx │ ├── next-env.d.ts │ ├── next.config.mjs │ ├── package.json │ └── tsconfig.json ├── license.md ├── package.json ├── packages ├── eslint-config-custom │ ├── library.js │ ├── next.js │ ├── package.json │ └── react-internal.js ├── nextjs-route-types │ ├── .eslintrc.js │ ├── .gitignore │ ├── license.md │ ├── package.json │ ├── readme.md │ ├── src │ │ ├── error.ts │ │ ├── generate-files.ts │ │ ├── get-directory-tree.ts │ │ ├── get-file-content.ts │ │ ├── index.ts │ │ ├── types.ts │ │ └── utils.ts │ └── tsconfig.json └── tsconfig │ ├── base.json │ ├── nextjs.json │ ├── package.json │ └── react-library.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── prettier.config.js ├── tsconfig.json └── turbo.json /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | types: [opened, synchronize] 8 | 9 | jobs: 10 | build: 11 | name: Build and Test 12 | timeout-minutes: 15 13 | runs-on: ubuntu-latest 14 | env: 15 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 16 | TURBO_TEAM: ${{ vars.TURBO_TEAM }} 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: pnpm/action-setup@v4 20 | with: 21 | version: 9.1.3 22 | - uses: actions/setup-node@v4 23 | with: 24 | node-version: 20 25 | cache: "pnpm" 26 | - run: pnpm install --frozen-lockfile 27 | - run: pnpm build 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # debug 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # local env files 26 | .env 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | 32 | # turbo 33 | .turbo 34 | 35 | # vercel 36 | .vercel 37 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers = true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | .next-types 3 | dist 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.rulers": [100], 3 | "eslint.workingDirectories": [ 4 | "./apps/demo", 5 | "./packages/eslint-config-custom", 6 | "./packages/nextjs-route-types", 7 | "./packages/tsconfig" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | packages/nextjs-route-types/readme.md -------------------------------------------------------------------------------- /apps/demo/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["custom/next"], 3 | }; 4 | -------------------------------------------------------------------------------- /apps/demo/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | .next-types -------------------------------------------------------------------------------- /apps/demo/app/[dynamic]/[nested]/page.tsx: -------------------------------------------------------------------------------- 1 | import { expectType } from "ts-expect"; 2 | 3 | import type { PageProps } from "./$types"; 4 | 5 | export default async function Page({ params }: PageProps) { 6 | expectType<{ dynamic: string; nested: string }>(await params); 7 | return
Hello world
; 8 | } 9 | -------------------------------------------------------------------------------- /apps/demo/app/[dynamic]/page/page.tsx: -------------------------------------------------------------------------------- 1 | import { expectType } from "ts-expect"; 2 | 3 | import type { PageProps } from "./$types"; 4 | 5 | export default async function Page({ params, searchParams }: PageProps) { 6 | expectType<{ dynamic: string }>(await params); 7 | expectType((await searchParams).hello); 8 | return
Hello world
; 9 | } 10 | -------------------------------------------------------------------------------- /apps/demo/app/[dynamic]/route/route.ts: -------------------------------------------------------------------------------- 1 | import { expectType } from "ts-expect"; 2 | 3 | import type { RouteHandler } from "./$types"; 4 | 5 | export const GET: RouteHandler = async (request, { params }) => { 6 | expectType(request); 7 | expectType<{ dynamic: string }>(await params); 8 | return new Response(); 9 | }; 10 | -------------------------------------------------------------------------------- /apps/demo/app/catch-all/[...catchall]/page.tsx: -------------------------------------------------------------------------------- 1 | import { expectType } from "ts-expect"; 2 | 3 | import type { PageProps } from "./$types"; 4 | 5 | export default async function Page({ params }: PageProps) { 6 | expectType<{ catchall: string[] }>(await params); 7 | return
Hello world
; 8 | } 9 | -------------------------------------------------------------------------------- /apps/demo/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { LayoutProps } from "./$types"; 2 | 3 | export default function Layout({ children }: LayoutProps) { 4 | return ( 5 | 6 | {children} 7 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /apps/demo/app/optional-catch-all/[[...oca]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { expectType } from "ts-expect"; 2 | 3 | import type { PageProps } from "./$types"; 4 | 5 | export default async function Page({ params }: PageProps) { 6 | expectType<{ oca: string[] | undefined }>(await params); 7 | return
Hello world
; 8 | } 9 | -------------------------------------------------------------------------------- /apps/demo/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { expectType } from "ts-expect"; 2 | 3 | import type { PageProps } from "./$types"; 4 | 5 | export default async function Page({ params }: PageProps) { 6 | expectType>(await params); 7 | return
Hello world
; 8 | } 9 | -------------------------------------------------------------------------------- /apps/demo/app/parallel-route/@test/[dynamic]/default.tsx: -------------------------------------------------------------------------------- 1 | import { expectType } from "ts-expect"; 2 | 3 | import type { DefaultProps } from "./$types"; 4 | 5 | export default async function Default({ params }: DefaultProps) { 6 | expectType<{ dynamic: string }>(await params); 7 | return
Hello world
; 8 | } 9 | -------------------------------------------------------------------------------- /apps/demo/app/parallel-route/@test/page.tsx: -------------------------------------------------------------------------------- 1 | export default function Page() { 2 | return
Hello world
; 3 | } 4 | -------------------------------------------------------------------------------- /apps/demo/app/parallel-route/layout.tsx: -------------------------------------------------------------------------------- 1 | import { expectType } from "ts-expect"; 2 | 3 | import type { LayoutProps } from "./$types"; 4 | 5 | export default function Layout(props: LayoutProps) { 6 | expectType<{ test: React.ReactNode; children: React.ReactNode }>(props); 7 | return ( 8 |
9 | {props.test} {props.children} 10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /apps/demo/app/parallel-route/page.tsx: -------------------------------------------------------------------------------- 1 | export default function Page() { 2 | return
Hello world
; 3 | } 4 | -------------------------------------------------------------------------------- /apps/demo/app/special-char/[special-char]/page.tsx: -------------------------------------------------------------------------------- 1 | import { expectType } from "ts-expect"; 2 | 3 | import type { PageProps } from "./$types"; 4 | 5 | export default async function Page({ params }: PageProps) { 6 | expectType((await params)["special-char"]); 7 | return
Hello world
; 8 | } 9 | -------------------------------------------------------------------------------- /apps/demo/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. 6 | -------------------------------------------------------------------------------- /apps/demo/next.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { withNextJSRouteTypes } from "nextjs-route-types"; 3 | 4 | /** @type {import("next").NextConfig} */ 5 | const nextConfig = { 6 | reactStrictMode: true, 7 | experimental: { 8 | webpackBuildWorker: true, 9 | }, 10 | }; 11 | 12 | export default withNextJSRouteTypes(nextConfig); 13 | -------------------------------------------------------------------------------- /apps/demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "next build", 7 | "dev": "next dev", 8 | "lint": "next lint", 9 | "start": "next start" 10 | }, 11 | "dependencies": { 12 | "next": "15.0.0", 13 | "nextjs-route-types": "workspace:*", 14 | "react": "19.0.0-rc-65a56d0e-20241020", 15 | "react-dom": "19.0.0-rc-65a56d0e-20241020", 16 | "ts-expect": "1.3.0" 17 | }, 18 | "devDependencies": { 19 | "@next/eslint-plugin-next": "15.0.0", 20 | "@types/node": "20.14.2", 21 | "@types/react": "18.3.3", 22 | "@types/react-dom": "18.3.0", 23 | "eslint-config-custom": "workspace:*", 24 | "tsconfig": "workspace:*", 25 | "typescript": "5.4.5" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /apps/demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/nextjs.json", 3 | "compilerOptions": { 4 | "plugins": [{ "name": "next" }], 5 | "paths": { "~/*": ["./*"] }, 6 | "rootDirs": [".", ".next-types"] 7 | }, 8 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 9 | "exclude": ["node_modules"] 10 | } 11 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023-2024 Vu Van Dung 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "project", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "turbo run build", 7 | "dev": "turbo run dev", 8 | "format": "prettier --write .", 9 | "lint": "turbo run lint", 10 | "publish-packages": "turbo run build lint && pnpm publish --access public --recursive" 11 | }, 12 | "devDependencies": { 13 | "@trivago/prettier-plugin-sort-imports": "4.3.0", 14 | "eslint": "8.57.0", 15 | "prettier": "3.3.2", 16 | "prettier-plugin-packagejson": "2.5.0", 17 | "prettier-plugin-tailwindcss": "0.6.3", 18 | "tsconfig": "workspace:*", 19 | "turbo": "2.0.3" 20 | }, 21 | "packageManager": "pnpm@9.1.3" 22 | } 23 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/library.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require("node:path"); 2 | 3 | const project = resolve(process.cwd(), "tsconfig.json"); 4 | 5 | /* 6 | * This is a custom ESLint configuration for use with 7 | * typescript packages. 8 | * 9 | * This config extends the Vercel Engineering Style Guide. 10 | * For more information, see https://github.com/vercel/style-guide 11 | * 12 | */ 13 | 14 | module.exports = { 15 | extends: ["@vercel/style-guide/eslint/node", "@vercel/style-guide/eslint/typescript"].map( 16 | require.resolve, 17 | ), 18 | parserOptions: { 19 | project, 20 | }, 21 | globals: { 22 | React: true, 23 | JSX: true, 24 | }, 25 | settings: { 26 | "import/resolver": { 27 | typescript: { 28 | project, 29 | }, 30 | }, 31 | }, 32 | ignorePatterns: ["node_modules/", "dist/"], 33 | rules: { 34 | "@typescript-eslint/explicit-function-return-type": "off", 35 | "@typescript-eslint/no-unused-vars": "warn", 36 | "import/order": "off", // handled by Prettier 37 | "no-console": "off", 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/next.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require("node:path"); 2 | 3 | const project = resolve(process.cwd(), "tsconfig.json"); 4 | 5 | /* 6 | * This is a custom ESLint configuration for use with 7 | * Next.js apps. 8 | * 9 | * This config extends the Vercel Engineering Style Guide. 10 | * For more information, see https://github.com/vercel/style-guide 11 | * 12 | */ 13 | 14 | module.exports = { 15 | extends: [ 16 | "@vercel/style-guide/eslint/node", 17 | "@vercel/style-guide/eslint/browser", 18 | "@vercel/style-guide/eslint/typescript", 19 | "@vercel/style-guide/eslint/react", 20 | "@vercel/style-guide/eslint/next", 21 | "eslint-config-turbo", 22 | ].map(require.resolve), 23 | parserOptions: { 24 | project, 25 | }, 26 | globals: { 27 | React: true, 28 | JSX: true, 29 | }, 30 | settings: { 31 | "import/resolver": { 32 | typescript: { 33 | project, 34 | }, 35 | }, 36 | }, 37 | ignorePatterns: ["node_modules/", "dist/"], 38 | // add rules configurations here 39 | rules: { 40 | "@typescript-eslint/explicit-function-return-type": "off", 41 | "@typescript-eslint/no-unused-vars": "warn", 42 | "import/no-default-export": "off", 43 | "import/order": "off", // handled by Prettier 44 | "no-console": "off", 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-config-custom", 3 | "version": "0.0.0", 4 | "private": true, 5 | "devDependencies": { 6 | "@vercel/style-guide": "6.0.0", 7 | "eslint-config-turbo": "2.0.3", 8 | "typescript": "5.4.5" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/react-internal.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require("node:path"); 2 | 3 | const project = resolve(process.cwd(), "tsconfig.json"); 4 | 5 | /* 6 | * This is a custom ESLint configuration for use with 7 | * internal (bundled by their consumer) libraries 8 | * that utilize React. 9 | * 10 | * This config extends the Vercel Engineering Style Guide. 11 | * For more information, see https://github.com/vercel/style-guide 12 | * 13 | */ 14 | 15 | module.exports = { 16 | extends: [ 17 | "@vercel/style-guide/eslint/browser", 18 | "@vercel/style-guide/eslint/typescript", 19 | "@vercel/style-guide/eslint/react", 20 | ].map(require.resolve), 21 | parserOptions: { 22 | project, 23 | }, 24 | globals: { 25 | JSX: true, 26 | }, 27 | settings: { 28 | "import/resolver": { 29 | typescript: { 30 | project, 31 | }, 32 | }, 33 | }, 34 | ignorePatterns: ["node_modules/", "dist/", ".eslintrc.js"], 35 | 36 | rules: { 37 | // add specific rules configurations here 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /packages/nextjs-route-types/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["custom/library"], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/nextjs-route-types/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /packages/nextjs-route-types/license.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023-2024 Vu Van Dung 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 | -------------------------------------------------------------------------------- /packages/nextjs-route-types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-route-types", 3 | "version": "2.0.1", 4 | "description": "Automatic type generation for Next.js app router routes", 5 | "bugs": "https://github.com/joulev/nextjs-route-types/issues", 6 | "repository": "joulev/nextjs-route-types", 7 | "license": "MIT", 8 | "main": "./dist/index.js", 9 | "module": "./dist/index.mjs", 10 | "types": "./dist/index.d.ts", 11 | "files": [ 12 | "dist" 13 | ], 14 | "scripts": { 15 | "build": "tsup src/index.ts --format cjs,esm --dts", 16 | "dev": "pnpm build --watch", 17 | "lint": "eslint --ext .ts,.tsx ." 18 | }, 19 | "dependencies": { 20 | "chokidar": "^3.5.3" 21 | }, 22 | "devDependencies": { 23 | "@types/node": "20.14.2", 24 | "eslint": "8.57.0", 25 | "eslint-config-custom": "workspace:*", 26 | "next": "15.0.0", 27 | "tsconfig": "workspace:*", 28 | "tsup": "8.1.0", 29 | "typescript": "5.4.5", 30 | "webpack": "5.92.0" 31 | }, 32 | "peerDependencies": { 33 | "next": "^15.0.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/nextjs-route-types/readme.md: -------------------------------------------------------------------------------- 1 | # nextjs-route-types 2 | 3 | Automatic type generation for Next.js app router routes, ~~copied from~~ highly inspired by [SvelteKit](https://kit.svelte.dev). 4 | 5 | Screenshot of nextjs-route-types in action 6 | 7 | **Version 2 only supports Next.js 15 and after. If you want to continue using Next.js 13 or 14, use version 1.1.1 of this library.** 8 | 9 | ## Installation 10 | 11 | 1. Install the package from NPM: 12 | 13 | ```sh 14 | npm install nextjs-route-types # npm 15 | yarn add nextjs-route-types # yarn 16 | pnpm add nextjs-route-types # pnpm 17 | bun add nextjs-route-types # bun 18 | ``` 19 | 20 | 2. Use the Next.js plugin in your Next.js config file 21 | 22 | ```js 23 | const { withNextJSRouteTypes } = require("nextjs-route-types"); 24 | 25 | /** @type {import("next").NextConfig} */ 26 | const nextConfig = { 27 | // your Next.js configurations 28 | }; 29 | 30 | module.exports = withNextJSRouteTypes(nextConfig); 31 | ``` 32 | 33 | 3. Configure `tsconfig.json`: Add `"rootDirs": [".", ".next-types"]` to your `compilerOptions`. This step is necessary, we need this for TypeScript to know where to look when we import from `./$types`. 34 | 35 | 4. If you use Git you might want to add `.next-types` to `.gitignore`. 36 | 37 | ## Usage 38 | 39 | In any files inside the `app` directory (or `src/app` if you use it), you can import certain types from `"./$types"`: 40 | 41 | ```tsx 42 | // app/[dynamic]/[nested]/page.tsx 43 | import type { PageProps } from "./$types"; 44 | 45 | export default async function Page({ params }: PageProps) { 46 | console.log((await params).dynamic); // string 47 | return
Hello world
; 48 | } 49 | ``` 50 | 51 | ```tsx 52 | // app/[dynamic]/[...another]/route.ts 53 | import type { RouteHandler } from "./$types"; 54 | 55 | export const GET: RouteHandler = async (request, { params }) => { 56 | console.log((await params).another); // string[]; 57 | return new Response(); 58 | }; 59 | ``` 60 | 61 | `./$types` exports the following types: `SearchParams`, `Params`, `DefaultProps`, `ErrorProps`, `LayoutProps`, `LoadingProps`, `NotFoundProps`, `PageProps`, `TemplateProps`, `RouteHandlerContext` and `RouteHandler`. 62 | 63 | > **Note** 64 | > Editor IntelliSense might not work and you likely have to type that import statement manually. This is a known issue that I don't know how to fix – PRs welcome. 65 | 66 | ## Credits 67 | 68 | [SvelteKit](https://kit.svelte.dev) for the idea of using `rootDirs` for this. 69 | 70 | [`nextjs-routes`](https://github.com/tatethurston/nextjs-routes) on which the Next.js plugin part of this code is based. 71 | -------------------------------------------------------------------------------- /packages/nextjs-route-types/src/error.ts: -------------------------------------------------------------------------------- 1 | export class NextJSRouteTypesError extends Error { 2 | constructor(message: string) { 3 | super(message); 4 | this.name = "NextJSRouteTypesError"; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/nextjs-route-types/src/generate-files.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import path from "node:path"; 3 | 4 | import type { DirectoryTree, GetFileContent } from "./types"; 5 | import { removeCwdFromPath } from "./utils"; 6 | 7 | const FOLDER_NAME = ".next-types"; 8 | const FILE_NAME = "$types.ts"; 9 | const root = path.join(process.cwd(), FOLDER_NAME); 10 | 11 | async function generateFilesRecursive( 12 | rootAppDir: string, 13 | tree: DirectoryTree, 14 | pathSoFar: string[], 15 | getFileContent: GetFileContent, 16 | ) { 17 | await Promise.all( 18 | tree.map(async item => { 19 | const newPath = [...pathSoFar, item.name]; 20 | const fullPath = path.join(rootAppDir, ...newPath); 21 | await fs.mkdir(fullPath, { recursive: true }); 22 | await fs.writeFile(path.join(fullPath, FILE_NAME), getFileContent(newPath, item.children)); 23 | await generateFilesRecursive(rootAppDir, item.children, newPath, getFileContent); 24 | }), 25 | ); 26 | } 27 | 28 | export async function generateFiles( 29 | appDir: string, 30 | tree: DirectoryTree, 31 | getFileContent: GetFileContent, 32 | ) { 33 | const rootAppDir = path.join(root, removeCwdFromPath(appDir)); 34 | await fs.mkdir(rootAppDir, { recursive: true }); 35 | await fs.writeFile(path.join(rootAppDir, FILE_NAME), getFileContent([], tree)); 36 | await generateFilesRecursive(rootAppDir, tree, [], getFileContent); 37 | } 38 | -------------------------------------------------------------------------------- /packages/nextjs-route-types/src/get-directory-tree.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import path from "node:path"; 3 | 4 | import type { DirectoryTree, DirectoryTreeItem } from "./types"; 5 | 6 | async function getDirectoryTreeRecursive(dir: string): Promise { 7 | const directory = await fs.readdir(dir, { withFileTypes: true }); 8 | const tree = await Promise.all( 9 | directory.map(async item => { 10 | if (!item.isDirectory()) return null; 11 | return { 12 | name: item.name, 13 | children: await getDirectoryTreeRecursive(path.join(dir, item.name)), 14 | }; 15 | }), 16 | ); 17 | return tree.filter((item): item is DirectoryTreeItem => Boolean(item)); 18 | } 19 | 20 | export function getDirectoryTree(appDir: string) { 21 | return getDirectoryTreeRecursive(appDir); 22 | } 23 | -------------------------------------------------------------------------------- /packages/nextjs-route-types/src/get-file-content.ts: -------------------------------------------------------------------------------- 1 | import type { DirectoryTree, GetFileContent } from "./types"; 2 | 3 | enum PathSegmentType { 4 | OptionalCatchAll = "OptionalCatchAll", 5 | CatchAll = "CatchAll", 6 | Dynamic = "Dynamic", 7 | } 8 | 9 | function getDynamicParamsFromPath(path: string[]): [PathSegmentType, string][] { 10 | return path 11 | .filter(segment => segment.startsWith("[") && segment.endsWith("]")) 12 | .map(segment => segment.slice(1, -1)) 13 | .map(segment => { 14 | if (segment.startsWith("[...")) 15 | return [PathSegmentType.OptionalCatchAll, segment.slice(4, -1)]; 16 | if (segment.startsWith("...")) return [PathSegmentType.CatchAll, segment.slice(3)]; 17 | return [PathSegmentType.Dynamic, segment]; 18 | }); 19 | } 20 | 21 | function getTsTypeFromPathSegmentType(type: PathSegmentType) { 22 | switch (type) { 23 | case PathSegmentType.OptionalCatchAll: 24 | return "string[] | undefined"; 25 | case PathSegmentType.CatchAll: 26 | return "string[]"; 27 | case PathSegmentType.Dynamic: 28 | return "string"; 29 | } 30 | } 31 | 32 | function getParallelRoutesFromChildren(children: DirectoryTree) { 33 | const parallelRoutes = children 34 | .filter(child => child.name.startsWith("@")) 35 | .map(child => child.name.slice(1)); 36 | if (!parallelRoutes.includes("children")) parallelRoutes.push("children"); 37 | return parallelRoutes; 38 | } 39 | 40 | export const getFileContent: GetFileContent = (path, children) => { 41 | const params = getDynamicParamsFromPath(path); 42 | const paramsTsInterfaceContent = params 43 | .map(([type, name]) => ` "${name}": ${getTsTypeFromPathSegmentType(type)}`) 44 | .join(";\n") 45 | .trim(); 46 | const parallelRoutes = getParallelRoutesFromChildren(children); 47 | const layoutPropsTsInterfaceContent = parallelRoutes 48 | .map(route => ` "${route}": ReactNode`) 49 | .concat(" params: Promise") 50 | .join(";\n") 51 | .trim(); 52 | return ` 53 | import type { NextRequest } from "next/server"; 54 | import type { ReactNode } from "react"; 55 | 56 | type EmptyObject = Record; 57 | 58 | export type SearchParams = Record; 59 | export type Params = ${params.length ? `{\n ${paramsTsInterfaceContent};\n}` : "EmptyObject"}; 60 | 61 | export type DefaultProps = { 62 | params: Promise; 63 | }; 64 | export type ErrorProps = { 65 | error: Error & { digest?: string }; 66 | reset: () => void; 67 | }; 68 | export type LayoutProps = {\n ${layoutPropsTsInterfaceContent};\n}; 69 | export type LoadingProps = EmptyObject; 70 | export type NotFoundProps = EmptyObject; 71 | export type PageProps = { 72 | params: Promise; 73 | searchParams: Promise; 74 | }; 75 | export type TemplateProps = LayoutProps; 76 | 77 | export type RouteHandlerContext = { 78 | params: Promise; 79 | }; 80 | type HandlerReturn = Response | Promise; 81 | export type RouteHandler = (request: NextRequest, context: RouteHandlerContext) => HandlerReturn; 82 | `.trimStart(); 83 | }; 84 | -------------------------------------------------------------------------------- /packages/nextjs-route-types/src/index.ts: -------------------------------------------------------------------------------- 1 | import { watch } from "chokidar"; 2 | import type { NextConfig } from "next"; 3 | import type { Configuration, WebpackPluginInstance } from "webpack"; 4 | 5 | import { NextJSRouteTypesError } from "./error"; 6 | import { generateFiles } from "./generate-files"; 7 | import { getDirectoryTree } from "./get-directory-tree"; 8 | import { getFileContent } from "./get-file-content"; 9 | import { getAppDirectory } from "./utils"; 10 | 11 | type WebpackConfigContext = Parameters>[1]; 12 | 13 | // https://github.com/tatethurston/nextjs-routes/blob/5ac00a885e06aa89b8eab029288d32955d7df5a6/packages/nextjs-routes/src/config.ts#L14-L23 14 | function debounce unknown>( 15 | fn: Fn, 16 | ms: number, 17 | ): (...args: Parameters) => void { 18 | let id: NodeJS.Timeout; 19 | return (...args) => { 20 | clearTimeout(id); 21 | id = setTimeout(() => fn(...args), ms); 22 | }; 23 | } 24 | 25 | class NextJSRouteTypesPlugin implements WebpackPluginInstance { 26 | name = "NextJSRouteTypesPlugin"; 27 | constructor(private readonly context: WebpackConfigContext) {} 28 | 29 | async main(appDir: string) { 30 | const tree = await getDirectoryTree(appDir); 31 | await generateFiles(appDir, tree, getFileContent); 32 | } 33 | 34 | async apply() { 35 | if (this.context.isServer) return; 36 | try { 37 | const appDir = await getAppDirectory(); 38 | if (this.context.dev) { 39 | const watcher = watch(appDir, { persistent: true }); 40 | const generate = debounce(() => this.main(appDir), 50); 41 | watcher.on("add", generate).on("unlink", generate); 42 | } else { 43 | await this.main(appDir); 44 | } 45 | } catch (e) { 46 | if (e instanceof NextJSRouteTypesError) { 47 | console.error(e.message); 48 | return; 49 | } 50 | throw e; 51 | } 52 | } 53 | } 54 | 55 | export function withNextJSRouteTypes(nextConfig: NextConfig): NextConfig { 56 | return { 57 | ...nextConfig, 58 | webpack: (config: Configuration, context) => { 59 | config.plugins ??= []; 60 | config.plugins.push(new NextJSRouteTypesPlugin(context)); 61 | 62 | // invoke any existing webpack extensions 63 | if (nextConfig.webpack) return nextConfig.webpack(config, context) as unknown; 64 | return config; 65 | }, 66 | }; 67 | } 68 | -------------------------------------------------------------------------------- /packages/nextjs-route-types/src/types.ts: -------------------------------------------------------------------------------- 1 | export interface DirectoryTreeItem { 2 | name: string; 3 | children: DirectoryTree; 4 | } 5 | 6 | export type DirectoryTree = DirectoryTreeItem[]; 7 | 8 | export type GetFileContent = (dirNames: string[], children: DirectoryTree) => string; 9 | -------------------------------------------------------------------------------- /packages/nextjs-route-types/src/utils.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import path from "node:path"; 3 | 4 | import { NextJSRouteTypesError } from "./error"; 5 | 6 | async function directoryExists(dir: string) { 7 | try { 8 | const stat = await fs.lstat(dir); 9 | return stat.isDirectory(); 10 | } catch { 11 | return false; 12 | } 13 | } 14 | 15 | export async function getAppDirectory() { 16 | let appDir = path.join(process.cwd(), "app"); 17 | if (await directoryExists(appDir)) return appDir; 18 | appDir = path.join(process.cwd(), "src", "app"); 19 | if (await directoryExists(appDir)) return appDir; 20 | throw new NextJSRouteTypesError("Could not find app directory"); 21 | } 22 | 23 | export function removeCwdFromPath(str: string) { 24 | const cwd = process.cwd(); 25 | if (str.startsWith(cwd)) return str.slice(cwd.length + 1); 26 | return str; 27 | } 28 | -------------------------------------------------------------------------------- /packages/nextjs-route-types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/base.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": ["."], 7 | "exclude": ["dist", "build", "node_modules"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/tsconfig/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Default", 4 | "compilerOptions": { 5 | "composite": false, 6 | "declaration": true, 7 | "declarationMap": true, 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "inlineSources": false, 11 | "isolatedModules": true, 12 | "moduleResolution": "node", 13 | "noUnusedLocals": false, 14 | "noUnusedParameters": false, 15 | "preserveWatchOutput": true, 16 | "skipLibCheck": true, 17 | "strict": true, 18 | "strictNullChecks": true 19 | }, 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /packages/tsconfig/nextjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Next.js", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "plugins": [{ "name": "next" }], 7 | "allowJs": true, 8 | "declaration": false, 9 | "declarationMap": false, 10 | "incremental": true, 11 | "jsx": "preserve", 12 | "lib": ["dom", "dom.iterable", "esnext"], 13 | "module": "esnext", 14 | "noEmit": true, 15 | "resolveJsonModule": true, 16 | "strict": false, 17 | "target": "es5" 18 | }, 19 | "include": ["src", "next-env.d.ts"], 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /packages/tsconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tsconfig", 3 | "version": "0.0.0", 4 | "private": true 5 | } 6 | -------------------------------------------------------------------------------- /packages/tsconfig/react-library.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "React Library", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "jsx": "react-jsx", 7 | "lib": ["ES2015", "DOM"], 8 | "module": "ESNext", 9 | "target": "es6" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | - "apps/*" 4 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /** @type {import("@trivago/prettier-plugin-sort-imports").PrettierConfig} */ 3 | module.exports = { 4 | semi: true, 5 | printWidth: 100, 6 | arrowParens: "avoid", 7 | tabWidth: 2, 8 | plugins: [ 9 | "@trivago/prettier-plugin-sort-imports", 10 | "prettier-plugin-tailwindcss", 11 | "prettier-plugin-packagejson", 12 | ], 13 | importOrder: ["^~/.*$", "^\\.\\.?/.*$"], 14 | importOrderSortSpecifiers: true, 15 | importOrderSeparation: true, 16 | }; 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/base.json" 3 | } 4 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "globalDependencies": ["**/.env.*local"], 4 | "tasks": { 5 | "build": { 6 | "dependsOn": ["^build"], 7 | "outputs": [".next/**", "!.next/cache/**", "dist/**"] 8 | }, 9 | "lint": {}, 10 | "dev": { 11 | "cache": false, 12 | "persistent": true 13 | } 14 | } 15 | } 16 | --------------------------------------------------------------------------------