├── .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 |
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 |
--------------------------------------------------------------------------------