├── .gitignore ├── .prettierrc.json ├── LICENSE.txt ├── README.md ├── assets ├── nextjs │ ├── README.md.template │ ├── hooks.ts │ └── makeRoute.tsx ├── qwikcity │ ├── README.md.template │ ├── hooks.ts │ └── makeRoute.tsx ├── react-router │ ├── README.md.template │ ├── index.ts │ └── makeRoute.tsx └── shared │ ├── index.ts.template │ ├── info.ts.template │ ├── openapi-import.template │ ├── openapi-register.template │ ├── openapi.template.ts │ └── utils.ts ├── docs ├── nextjs.md ├── qwikcity.md └── react-router.md ├── examples ├── nextjs │ ├── WALKTHROUGH.md │ ├── finished │ │ ├── .eslintrc.json │ │ ├── .gitignore │ │ ├── README.md │ │ ├── components.json │ │ ├── declarative-routing.config.json │ │ ├── next.config.mjs │ │ ├── package.json │ │ ├── pnpm-lock.yaml │ │ ├── postcss.config.js │ │ ├── public │ │ │ ├── next.svg │ │ │ └── vercel.svg │ │ ├── revert │ │ ├── src │ │ │ ├── app │ │ │ │ ├── api │ │ │ │ │ └── pokemon │ │ │ │ │ │ ├── [pokemonId] │ │ │ │ │ │ ├── route.info.ts │ │ │ │ │ │ └── route.ts │ │ │ │ │ │ ├── route.info.ts │ │ │ │ │ │ └── route.ts │ │ │ │ ├── components │ │ │ │ │ ├── PokemonCard.tsx │ │ │ │ │ ├── PokemonGrid.tsx │ │ │ │ │ ├── PokemonInfo.tsx │ │ │ │ │ └── SelectableGrid.tsx │ │ │ │ ├── favicon.ico │ │ │ │ ├── globals.css │ │ │ │ ├── layout.tsx │ │ │ │ ├── page.info.ts │ │ │ │ ├── page.tsx │ │ │ │ ├── pokemon │ │ │ │ │ └── [pokemonId] │ │ │ │ │ │ ├── page.info.ts │ │ │ │ │ │ └── page.tsx │ │ │ │ └── search │ │ │ │ │ ├── SearchList.tsx │ │ │ │ │ ├── page.info.ts │ │ │ │ │ └── page.tsx │ │ │ ├── components │ │ │ │ └── ui │ │ │ │ │ └── input.tsx │ │ │ ├── lib │ │ │ │ └── utils.ts │ │ │ ├── pokemon.ts │ │ │ ├── routes │ │ │ │ ├── README.md │ │ │ │ ├── hooks.ts │ │ │ │ ├── index.ts │ │ │ │ ├── makeRoute.tsx │ │ │ │ └── utils.ts │ │ │ └── types.ts │ │ ├── tailwind.config.ts │ │ └── tsconfig.json │ └── starter │ │ ├── .eslintrc.json │ │ ├── .gitignore │ │ ├── README.md │ │ ├── components.json │ │ ├── next.config.mjs │ │ ├── package.json │ │ ├── pnpm-lock.yaml │ │ ├── postcss.config.js │ │ ├── public │ │ ├── next.svg │ │ └── vercel.svg │ │ ├── revert │ │ ├── src │ │ ├── app │ │ │ ├── api │ │ │ │ └── pokemon │ │ │ │ │ ├── [pokemonId] │ │ │ │ │ └── route.ts │ │ │ │ │ └── route.ts │ │ │ ├── components │ │ │ │ ├── PokemonCard.tsx │ │ │ │ ├── PokemonGrid.tsx │ │ │ │ ├── PokemonInfo.tsx │ │ │ │ └── SelectableGrid.tsx │ │ │ ├── favicon.ico │ │ │ ├── globals.css │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ ├── pokemon │ │ │ │ └── [pokemonId] │ │ │ │ │ └── page.tsx │ │ │ └── search │ │ │ │ ├── SearchList.tsx │ │ │ │ └── page.tsx │ │ ├── components │ │ │ └── ui │ │ │ │ └── input.tsx │ │ ├── lib │ │ │ └── utils.ts │ │ ├── pokemon.ts │ │ └── types.ts │ │ ├── tailwind.config.ts │ │ └── tsconfig.json ├── qwikcity │ ├── WALKTHROUGH.md │ ├── finished │ │ ├── .eslintignore │ │ ├── .eslintrc.cjs │ │ ├── .gitignore │ │ ├── .prettierignore │ │ ├── .prettierrc.js │ │ ├── .vscode │ │ │ ├── launch.json │ │ │ ├── qwik-city.code-snippets │ │ │ └── qwik.code-snippets │ │ ├── README.md │ │ ├── declarative-routing.config.json │ │ ├── package.json │ │ ├── pnpm-lock.yaml │ │ ├── postcss.config.cjs │ │ ├── public │ │ │ ├── favicon.svg │ │ │ ├── manifest.json │ │ │ └── robots.txt │ │ ├── src │ │ │ ├── components │ │ │ │ ├── PokemonCard.tsx │ │ │ │ ├── PokemonGrid.tsx │ │ │ │ ├── PokemonInfo.tsx │ │ │ │ ├── SelectableGrid.tsx │ │ │ │ ├── router-head │ │ │ │ │ └── router-head.tsx │ │ │ │ └── ui │ │ │ │ │ └── input.tsx │ │ │ ├── declarativeRoutes │ │ │ │ ├── README.md │ │ │ │ ├── hooks.ts │ │ │ │ ├── index.ts │ │ │ │ └── makeRoute.tsx │ │ │ ├── entry.dev.tsx │ │ │ ├── entry.preview.tsx │ │ │ ├── entry.ssr.tsx │ │ │ ├── global.css │ │ │ ├── lib │ │ │ │ └── utils.ts │ │ │ ├── pokemon.ts │ │ │ ├── root.tsx │ │ │ ├── routes │ │ │ │ ├── api │ │ │ │ │ └── pokemon │ │ │ │ │ │ ├── [pokemonId] │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ └── index.ts │ │ │ │ ├── index.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── pokemon │ │ │ │ │ └── [pokemonId] │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── routeInfo.ts │ │ │ │ ├── routeInfo.ts │ │ │ │ ├── search │ │ │ │ │ ├── SearchList.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── routeInfo.ts │ │ │ │ └── service-worker.ts │ │ │ └── types.ts │ │ ├── tailwind.config.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ └── starter │ │ ├── .eslintignore │ │ ├── .eslintrc.cjs │ │ ├── .gitignore │ │ ├── .prettierignore │ │ ├── .prettierrc.js │ │ ├── .vscode │ │ ├── launch.json │ │ ├── qwik-city.code-snippets │ │ └── qwik.code-snippets │ │ ├── README.md │ │ ├── package.json │ │ ├── pnpm-lock.yaml │ │ ├── postcss.config.cjs │ │ ├── public │ │ ├── favicon.svg │ │ ├── manifest.json │ │ └── robots.txt │ │ ├── src │ │ ├── components │ │ │ ├── PokemonCard.tsx │ │ │ ├── PokemonGrid.tsx │ │ │ ├── PokemonInfo.tsx │ │ │ ├── SelectableGrid.tsx │ │ │ ├── router-head │ │ │ │ └── router-head.tsx │ │ │ └── ui │ │ │ │ └── input.tsx │ │ ├── entry.dev.tsx │ │ ├── entry.preview.tsx │ │ ├── entry.ssr.tsx │ │ ├── global.css │ │ ├── lib │ │ │ └── utils.ts │ │ ├── pokemon.ts │ │ ├── root.tsx │ │ ├── routes │ │ │ ├── api │ │ │ │ └── pokemon │ │ │ │ │ ├── [pokemonId] │ │ │ │ │ └── index.ts │ │ │ │ │ └── index.ts │ │ │ ├── index.tsx │ │ │ ├── layout.tsx │ │ │ ├── pokemon │ │ │ │ └── [pokemonId] │ │ │ │ │ └── index.tsx │ │ │ ├── search │ │ │ │ ├── SearchList.tsx │ │ │ │ └── index.tsx │ │ │ └── service-worker.ts │ │ └── types.ts │ │ ├── tailwind.config.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts └── react-router │ ├── WALKTHROUGH.md │ ├── finished │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── README.md │ ├── components.json │ ├── declarative-routing.config.json │ ├── index.html │ ├── package.json │ ├── pnpm-lock.yaml │ ├── postcss.config.js │ ├── public │ │ └── vite.svg │ ├── revert │ ├── src │ │ ├── App.tsx │ │ ├── assets │ │ │ └── react.svg │ │ ├── components │ │ │ ├── PokemonCard.tsx │ │ │ ├── PokemonGrid.tsx │ │ │ ├── PokemonInfo.tsx │ │ │ ├── SearchList.tsx │ │ │ ├── SelectableGrid.tsx │ │ │ └── ui │ │ │ │ └── input.tsx │ │ ├── index.css │ │ ├── lib │ │ │ └── utils.ts │ │ ├── main.tsx │ │ ├── pages │ │ │ ├── Detail.tsx │ │ │ ├── Home.tsx │ │ │ ├── Search.tsx │ │ │ └── _Layout.tsx │ │ ├── pokemon.ts │ │ ├── routeTable.ts │ │ ├── routes │ │ │ ├── README.md │ │ │ ├── index.ts │ │ │ └── makeRoute.tsx │ │ ├── types.ts │ │ └── vite-env.d.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts │ └── starter │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── README.md │ ├── components.json │ ├── index.html │ ├── package.json │ ├── pnpm-lock.yaml │ ├── postcss.config.js │ ├── public │ └── vite.svg │ ├── revert │ ├── src │ ├── App.tsx │ ├── assets │ │ └── react.svg │ ├── components │ │ ├── PokemonCard.tsx │ │ ├── PokemonGrid.tsx │ │ ├── PokemonInfo.tsx │ │ ├── SearchList.tsx │ │ ├── SelectableGrid.tsx │ │ └── ui │ │ │ └── input.tsx │ ├── index.css │ ├── lib │ │ └── utils.ts │ ├── main.tsx │ ├── pages │ │ ├── Detail.tsx │ │ ├── Home.tsx │ │ ├── Search.tsx │ │ └── _Layout.tsx │ ├── pokemon.ts │ ├── routeTable.ts │ ├── types.ts │ └── vite-env.d.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── package.json ├── pnpm-lock.yaml ├── src ├── config.ts ├── index.ts ├── init.ts ├── nextjs │ └── init.ts ├── qwikcity │ └── init.ts ├── react-router │ └── init.ts ├── shared │ ├── build-tools.ts │ ├── build.ts │ ├── utils.ts │ └── watch.ts └── template.ts ├── tsconfig.json └── tsup.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .idea 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": true, 4 | "htmlWhitespaceSensitivity": "css", 5 | "insertPragma": false, 6 | "jsxBracketSameLine": false, 7 | "jsxSingleQuote": true, 8 | "printWidth": 80, 9 | "proseWrap": "preserve", 10 | "quoteProps": "as-needed", 11 | "requirePragma": false, 12 | "semi": true, 13 | "singleQuote": false, 14 | "tabWidth": 2, 15 | "trailingComma": "none", 16 | "useTabs": false 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Jack Herrington 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 | ## Declarative Routes 2 | 3 | `declarative-routing` sets up an optional declarative routing system for React or QwikCity projects. For NextJS, it maintains a global list of both pages and API routes and provides components and functions to easily navigate to pages, or make API requests. 4 | 5 | ## What are Declarative Routes? 6 | 7 | Typesafe routing is a way to ensure that your routes are structured properly; the parameters in the URL are correct and a route handler exists for that route. Declarative routing goes to the next step and ensures that your link is going to the correct route. 8 | 9 | With typesafe routing you still have to deal with urls; `` Product ``. With declarative routing you can use a component that is typed to the route, and that will generate the correct URL for you. `Product`. Later on, if the route changes, or the parameters change, the `ProductDetail.Link` component will be updated to reflect that everwhere it is used in your application. 10 | 11 | ## Installation 12 | 13 | For NextJS projects follow the [NextJS installation instructions](https://github.com/ProNextJS/declarative-routing/blob/main/docs/nextjs.md). 14 | 15 | For React Router projects follow the [React Router installation instructions](https://github.com/ProNextJS/declarative-routing/blob/main/docs/react-router.md). 16 | 17 | For QwikCity projects follow the [QwikCity installation instructions](https://github.com/ProNextJS/declarative-routing/blob/main/docs/qwikcity.md). 18 | 19 | # Credit where credit is due 20 | 21 | This system is based on the work in [Fix Next.JS Routing To Have Full Type-Safety](https://www.flightcontrol.dev/blog/fix-nextjs-routing-to-have-full-type-safety). However the original article had a significantly different interface and didn't cover API routes at all. 22 | -------------------------------------------------------------------------------- /assets/nextjs/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/navigation"; 2 | import { 3 | useParams as useNextParams, 4 | useSearchParams as useNextSearchParams 5 | } from "next/navigation"; 6 | import { z } from "zod"; 7 | 8 | import { RouteBuilder } from "./makeRoute"; 9 | 10 | const emptySchema = z.object({}); 11 | 12 | type PushOptions = Parameters["push"]>[1]; 13 | 14 | export function usePush< 15 | Params extends z.ZodSchema, 16 | Search extends z.ZodSchema = typeof emptySchema 17 | >(builder: RouteBuilder) { 18 | const { push } = useRouter(); 19 | return ( 20 | p: z.input, 21 | search?: z.input, 22 | options?: PushOptions 23 | ) => { 24 | push(builder(p, search), options); 25 | }; 26 | } 27 | 28 | export function useParams< 29 | Params extends z.ZodSchema, 30 | Search extends z.ZodSchema = typeof emptySchema 31 | >(builder: RouteBuilder): z.output { 32 | const res = builder.paramsSchema.safeParse(useNextParams()); 33 | if (!res.success) { 34 | throw new Error( 35 | `Invalid route params for route ${builder.routeName}: ${res.error.message}` 36 | ); 37 | } 38 | return res.data; 39 | } 40 | 41 | export function useSearchParams< 42 | Params extends z.ZodSchema, 43 | Search extends z.ZodSchema = typeof emptySchema 44 | >(builder: RouteBuilder): z.output { 45 | const res = builder.searchSchema!.safeParse( 46 | convertURLSearchParamsToObject(useNextSearchParams()) 47 | ); 48 | if (!res.success) { 49 | throw new Error( 50 | `Invalid search params for route ${builder.routeName}: ${res.error.message}` 51 | ); 52 | } 53 | return res.data; 54 | } 55 | 56 | function convertURLSearchParamsToObject( 57 | params: Readonly | null 58 | ): Record { 59 | if (!params) { 60 | return {}; 61 | } 62 | 63 | const obj: Record = {}; 64 | // @ts-ignore 65 | for (const [key, value] of params.entries()) { 66 | if (params.getAll(key).length > 1) { 67 | obj[key] = params.getAll(key); 68 | } else { 69 | obj[key] = value; 70 | } 71 | } 72 | return obj; 73 | } 74 | -------------------------------------------------------------------------------- /assets/qwikcity/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useLocation } from "@builder.io/qwik-city"; 2 | import type { z } from "zod"; 3 | import type { RouteBuilder, emptySchema } from "./makeRoute"; 4 | 5 | export function useParams< 6 | Params extends z.ZodSchema, 7 | Search extends z.ZodSchema = typeof emptySchema 8 | >(builder: RouteBuilder): z.output { 9 | const location = useLocation(); 10 | const res = builder.paramsSchema.safeParse(location.params); 11 | if (!res.success) { 12 | throw new Error( 13 | `Invalid route params for route ${builder.routeName}: ${res.error.message}` 14 | ); 15 | } 16 | return res.data; 17 | } 18 | 19 | export function useSearchParams< 20 | Params extends z.ZodSchema, 21 | Search extends z.ZodSchema = typeof emptySchema 22 | >(builder: RouteBuilder): z.output { 23 | const location = useLocation(); 24 | 25 | const res = builder.searchSchema!.safeParse( 26 | convertURLSearchParamsToObject(location.url.searchParams) 27 | ); 28 | if (!res.success) { 29 | throw new Error( 30 | `Invalid search params for route ${builder.routeName}: ${res.error.message}` 31 | ); 32 | } 33 | return res.data; 34 | } 35 | 36 | function convertURLSearchParamsToObject( 37 | params: Readonly | null 38 | ): Record { 39 | if (!params) { 40 | return {}; 41 | } 42 | 43 | const obj: Record = {}; 44 | // @ts-ignore 45 | for (const [key, value] of params.entries()) { 46 | if (params.getAll(key).length > 1) { 47 | obj[key] = params.getAll(key); 48 | } else { 49 | obj[key] = value; 50 | } 51 | } 52 | 53 | return obj; 54 | } 55 | -------------------------------------------------------------------------------- /assets/react-router/index.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { makeRoute } from "./makeRoute"; 3 | 4 | export const Home = makeRoute("/", { 5 | name: "Home", 6 | params: z.object({}), 7 | search: z.object({}) 8 | }); 9 | 10 | /* 11 | Define your routes here, like this: 12 | 13 | export const Product = makeRoute("/product/:productId", { 14 | name: "Product", 15 | params: z.object({ 16 | productId: z.string(), 17 | }), 18 | search: z.object({}), 19 | }); 20 | */ 21 | -------------------------------------------------------------------------------- /assets/shared/index.ts.template: -------------------------------------------------------------------------------- 1 | // Automatically generated by declarative-routing, do NOT edit 2 | import { z } from "zod"; 3 | import { {{{imports}}} } from "./makeRoute"; 4 | 5 | const defaultInfo = { 6 | search: z.object({}) 7 | }; 8 | 9 | {{#each routeImports}} 10 | import * as {{{this.importKey}}}Route from "{{{this.importPath}}}"; 11 | {{/each}} 12 | 13 | {{#each pageRoutes}} 14 | export const {{{this.importKey}}} = makeRoute( 15 | "{{{this.pathTemplate}}}", 16 | { 17 | ...defaultInfo, 18 | ...{{{this.importKey}}}Route.Route 19 | } 20 | ); 21 | {{/each}} 22 | 23 | {{#each apiRoutes}} 24 | export const {{{this.lowerVerb}}}{{{this.importKey}}} = make{{{this.upperVerb}}}Route( 25 | "{{{this.pathTemplate}}}", 26 | { 27 | ...defaultInfo, 28 | ...{{{this.importKey}}}Route.Route 29 | }, 30 | {{#if isNotDELETE}}{{{this.importKey}}}Route.{{{this.verb}}}{{/if}} 31 | ); 32 | {{/each}} -------------------------------------------------------------------------------- /assets/shared/info.ts.template: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const Route = { 4 | name: "{{{name}}}", 5 | params: z.object({ 6 | {{#each params}} 7 | {{{this}}}, 8 | {{/each}} 9 | }) 10 | }; 11 | 12 | {{#each verbs}} 13 | export const {{{this.verb}}} = { 14 | {{#each this.keys}} 15 | {{{this}}}: z.object({}), 16 | {{/each}} 17 | }; 18 | {{/each}} 19 | -------------------------------------------------------------------------------- /assets/shared/openapi-import.template: -------------------------------------------------------------------------------- 1 | import * as {{{importKey}}} from "{{{pathPrefix}}}/{{{srcDir}}}{{{import}}}"; 2 | -------------------------------------------------------------------------------- /assets/shared/openapi-register.template: -------------------------------------------------------------------------------- 1 | registry.registerPath({ 2 | method: "{{{lowerVerb}}}", 3 | path: "{{{pathTemplate}}}", 4 | summary: "", 5 | request: { 6 | {{#if isNotDELETE}} 7 | params: {{{importKey}}}.Route.params, 8 | {{/if}} 9 | {{#if isPOSTorPUT}} 10 | body: { 11 | required: true, 12 | content: { 13 | "application/json": { 14 | schema: {{{importKey}}}.{{{verb}}}.body, 15 | }, 16 | }, 17 | }, 18 | {{/if}} 19 | }, 20 | responses: { 21 | 200: { 22 | description: "Success", 23 | content: { 24 | "application/json": { 25 | schema: {{{importKey}}}.{{{verb}}}.result, 26 | }, 27 | }, 28 | }, 29 | }, 30 | }); -------------------------------------------------------------------------------- /assets/shared/openapi.template.ts: -------------------------------------------------------------------------------- 1 | import { 2 | OpenAPIRegistry, 3 | OpenApiGeneratorV3 4 | } from "@asteasolutions/zod-to-openapi"; 5 | import * as yaml from "yaml"; 6 | import * as fs from "fs"; 7 | 8 | // \{{IMPORTS}} 9 | 10 | const registry = new OpenAPIRegistry(); 11 | 12 | // \{{REGISTRATIONS}} 13 | 14 | const generator = new OpenApiGeneratorV3(registry.definitions); 15 | const docs = generator.generateDocument({ 16 | openapi: "3.0.0", 17 | info: { 18 | version: "1.0.0", 19 | title: "My API", 20 | description: "This is the API" 21 | }, 22 | servers: [{ url: "v1" }] 23 | }); 24 | 25 | fs.writeFileSync(`./openapi-docs.yml`, yaml.stringify(docs), { 26 | encoding: "utf-8" 27 | }); 28 | -------------------------------------------------------------------------------- /docs/react-router.md: -------------------------------------------------------------------------------- 1 | ## Installation and Usage on NextJS 2 | 3 | Initialize your NextJS application: 4 | 5 | ```bash 6 | npx declarative-routing init 7 | ``` 8 | 9 | This will generate an `./src/routes` directory (you can specify the location during the install) that you can use to navigate to routes. It also generates a `README.md` file in the routes directory that contains information about how to use the system. 10 | -------------------------------------------------------------------------------- /examples/nextjs/finished/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /examples/nextjs/finished/.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /examples/nextjs/finished/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /examples/nextjs/finished/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /examples/nextjs/finished/declarative-routing.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "mode": "nextjs", 3 | "src": "./src/app", 4 | "routes": "./src/routes" 5 | } -------------------------------------------------------------------------------- /examples/nextjs/finished/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: "https", 7 | hostname: "raw.githubusercontent.com", 8 | port: "", 9 | }, 10 | ], 11 | }, 12 | }; 13 | 14 | export default nextConfig; 15 | -------------------------------------------------------------------------------- /examples/nextjs/finished/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pokemon", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "dr:build": "npx declarative-routing build", 11 | "dr:build:watch": "npx declarative-routing build --watch" 12 | }, 13 | "dependencies": { 14 | "class-variance-authority": "^0.7.0", 15 | "clsx": "^2.1.0", 16 | "lucide-react": "^0.331.0", 17 | "next": "14.2.0", 18 | "query-string": "^9.0.0", 19 | "react": "^18", 20 | "react-dom": "^18", 21 | "tailwind-merge": "^2.2.1", 22 | "tailwindcss-animate": "^1.0.7", 23 | "zod": "^3.23.4" 24 | }, 25 | "devDependencies": { 26 | "@tailwindcss/container-queries": "^0.1.1", 27 | "@types/node": "^20", 28 | "@types/react": "^18", 29 | "@types/react-dom": "^18", 30 | "autoprefixer": "^10.0.1", 31 | "eslint": "^8", 32 | "eslint-config-next": "14.1.0", 33 | "postcss": "^8", 34 | "tailwindcss": "^3.3.0", 35 | "typescript": "^5" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/nextjs/finished/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /examples/nextjs/finished/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/nextjs/finished/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/nextjs/finished/revert: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | find . -name "route.info.ts" | xargs rm 3 | find . -name "page.info.ts" | xargs rm 4 | git reset --hard HEAD 5 | rm -fr declarative-routing.config.json src/routes/ 6 | -------------------------------------------------------------------------------- /examples/nextjs/finished/src/app/api/pokemon/[pokemonId]/route.info.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const Route = { 4 | name: "ApiPokemonPokemonId", 5 | params: z.object({ 6 | pokemonId: z.string(), 7 | }) 8 | }; 9 | 10 | export const GET = { 11 | result: z.object({}), 12 | }; 13 | -------------------------------------------------------------------------------- /examples/nextjs/finished/src/app/api/pokemon/[pokemonId]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse, NextRequest } from "next/server"; 2 | 3 | import { getPokemon } from "@/pokemon"; 4 | 5 | export const dynamic = "force-dynamic"; 6 | 7 | export async function GET( 8 | req: NextRequest, 9 | { params }: { params: { pokemonId: string } } 10 | ) { 11 | return NextResponse.json(await getPokemon(+(params.pokemonId || ""))); 12 | } 13 | -------------------------------------------------------------------------------- /examples/nextjs/finished/src/app/api/pokemon/route.info.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | import { PokemonSchema } from "@/types"; 4 | 5 | export const Route = { 6 | name: "PokemonSearch", 7 | params: z.object({}), 8 | search: z.object({ 9 | q: z.string().default(""), 10 | limit: z.number().default(10) 11 | }) 12 | }; 13 | 14 | export const GET = { 15 | result: z.array(PokemonSchema) 16 | }; 17 | -------------------------------------------------------------------------------- /examples/nextjs/finished/src/app/api/pokemon/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { safeParseSearchParams } from "@/routes/utils" 3 | import { Route } from "./route.info" 4 | import { getFullPokemon } from "@/pokemon"; 5 | 6 | export const dynamic = "force-dynamic"; 7 | 8 | export async function GET(req: NextRequest) { 9 | const { q, limit } = safeParseSearchParams(Route.search, req.nextUrl.searchParams) 10 | return NextResponse.json(await getFullPokemon(limit, q)); 11 | } 12 | -------------------------------------------------------------------------------- /examples/nextjs/finished/src/app/components/PokemonCard.tsx: -------------------------------------------------------------------------------- 1 | import { Pokemon } from "@/types"; 2 | import Image from "next/image"; 3 | 4 | export function PokemonCard({ pokemon }: { pokemon: Pokemon }) { 5 | return ( 6 |
7 |
8 | {pokemon.name} 15 |
16 |
17 |

{pokemon.name}

18 |
19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /examples/nextjs/finished/src/app/components/PokemonGrid.tsx: -------------------------------------------------------------------------------- 1 | import { PokemonDetail } from "@/routes"; 2 | 3 | import { PokemonCard } from "./PokemonCard"; 4 | import { Pokemon } from "@/types"; 5 | 6 | export function PokemonGrid({ pokemon }: { pokemon: Pokemon[] }) { 7 | return ( 8 |
9 | {pokemon.map((p) => ( 10 | 11 | 12 | 13 | ))} 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /examples/nextjs/finished/src/app/components/PokemonInfo.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Image from "next/image"; 3 | import { useEffect, useState } from "react"; 4 | import { Pokemon } from "@/types"; 5 | 6 | export function PokemonInfo({ 7 | id, 8 | pokemon: initialPokemon, 9 | }: { 10 | id: number; 11 | pokemon?: Pokemon; 12 | }) { 13 | const [data, setData] = useState(); 14 | 15 | useEffect(() => { 16 | if (!initialPokemon) { 17 | fetch(`/api/pokemon/${id}`) 18 | .then((res) => res.json()) 19 | .then((data) => setData(data)); 20 | } 21 | }, [id, initialPokemon]); 22 | 23 | const pokemon = initialPokemon || data; 24 | 25 | return pokemon ? ( 26 |
27 |
28 |
29 | {pokemon.name} 36 |
37 | 38 |
39 |

{pokemon.name}

40 |
41 |
Species
42 |
{pokemon.species}
43 |
44 |
45 |
Types
46 |
{pokemon.types}
47 |
48 |
49 |
Stats
50 |
{pokemon.stats}
51 |
52 |
53 |
Moves
54 |
{pokemon.moves}
55 |
56 |
57 |
58 |
59 | ) : null; 60 | } 61 | -------------------------------------------------------------------------------- /examples/nextjs/finished/src/app/components/SelectableGrid.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useState } from "react"; 3 | 4 | import { Pokemon } from "@/types"; 5 | 6 | import { PokemonDetail } from "@/routes"; 7 | 8 | import { PokemonCard } from "./PokemonCard"; 9 | import { PokemonInfo } from "./PokemonInfo"; 10 | 11 | export function SelectableGrid({ pokemon }: { pokemon: Pokemon[] }) { 12 | const [selected, setSelected] = useState(); 13 | 14 | return ( 15 |
16 |
17 | {pokemon.map((p) => ( 18 |
setSelected(p)} key={p.id}> 19 | 20 |
21 | ))} 22 |
23 | {selected && ( 24 |
25 | 26 | 27 | 28 |
29 | )} 30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /examples/nextjs/finished/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProNextJS/declarative-routing/5d933852b072461eea9aa266cb55d0a0f1ee01f7/examples/nextjs/finished/src/app/favicon.ico -------------------------------------------------------------------------------- /examples/nextjs/finished/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 222.2 84% 4.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 84% 4.9%; 15 | 16 | --primary: 222.2 47.4% 11.2%; 17 | --primary-foreground: 210 40% 98%; 18 | 19 | --secondary: 210 40% 96.1%; 20 | --secondary-foreground: 222.2 47.4% 11.2%; 21 | 22 | --muted: 210 40% 96.1%; 23 | --muted-foreground: 215.4 16.3% 46.9%; 24 | 25 | --accent: 210 40% 96.1%; 26 | --accent-foreground: 222.2 47.4% 11.2%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 210 40% 98%; 30 | 31 | --border: 214.3 31.8% 91.4%; 32 | --input: 214.3 31.8% 91.4%; 33 | --ring: 222.2 84% 4.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 222.2 84% 4.9%; 40 | --foreground: 210 40% 98%; 41 | 42 | --card: 222.2 84% 4.9%; 43 | --card-foreground: 210 40% 98%; 44 | 45 | --popover: 222.2 84% 4.9%; 46 | --popover-foreground: 210 40% 98%; 47 | 48 | --primary: 210 40% 98%; 49 | --primary-foreground: 222.2 47.4% 11.2%; 50 | 51 | --secondary: 217.2 32.6% 17.5%; 52 | --secondary-foreground: 210 40% 98%; 53 | 54 | --muted: 217.2 32.6% 17.5%; 55 | --muted-foreground: 215 20.2% 65.1%; 56 | 57 | --accent: 217.2 32.6% 17.5%; 58 | --accent-foreground: 210 40% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 210 40% 98%; 62 | 63 | --border: 217.2 32.6% 17.5%; 64 | --input: 217.2 32.6% 17.5%; 65 | --ring: 212.7 26.8% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } -------------------------------------------------------------------------------- /examples/nextjs/finished/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | 4 | import { Home, Search } from "@/routes"; 5 | 6 | import "./globals.css"; 7 | 8 | const inter = Inter({ subsets: ["latin"] }); 9 | 10 | export const metadata: Metadata = { 11 | title: "Create Next App", 12 | description: "Generated by create next app" 13 | }; 14 | 15 | export default function RootLayout({ 16 | children 17 | }: Readonly<{ 18 | children: React.ReactNode; 19 | }>) { 20 | return ( 21 | 22 | 23 |
24 | Home 25 | Search 26 |
27 |
{children}
28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /examples/nextjs/finished/src/app/page.info.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const Route = { 4 | name: "Home", 5 | params: z.object({ 6 | }) 7 | }; 8 | 9 | -------------------------------------------------------------------------------- /examples/nextjs/finished/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { getFullPokemon } from "@/pokemon"; 2 | 3 | import { SelectableGrid } from "@/app/components/SelectableGrid"; 4 | 5 | export default async function Home() { 6 | const pokemon = await getFullPokemon(); 7 | 8 | return ( 9 |
10 | 11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /examples/nextjs/finished/src/app/pokemon/[pokemonId]/page.info.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const Route = { 4 | name: "PokemonDetail", 5 | params: z.object({ 6 | pokemonId: z.coerce.number() 7 | }) 8 | }; 9 | -------------------------------------------------------------------------------- /examples/nextjs/finished/src/app/pokemon/[pokemonId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { PokemonInfo } from "@/app/components/PokemonInfo"; 2 | 3 | import { getPokemon } from "@/pokemon"; 4 | 5 | export default async function PokemonDetailPage({ 6 | params: { pokemonId }, 7 | }: { 8 | params: { pokemonId: string }; 9 | }) { 10 | const pokemon = await getPokemon(+pokemonId); 11 | return ( 12 |
13 | 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /examples/nextjs/finished/src/app/search/SearchList.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useState } from "react"; 3 | import { Input } from "@/components/ui/input"; 4 | 5 | import { Pokemon } from "@/types"; 6 | 7 | import { PokemonGrid } from "@/app/components/PokemonGrid"; 8 | 9 | import { Search as SearchRoute, getPokemonSearch } from "@/routes"; 10 | import { useSearchParams } from "@/routes/hooks"; 11 | 12 | export default function Search({ 13 | pokemon: initialPokemon 14 | }: { 15 | pokemon: Pokemon[]; 16 | }) { 17 | const q = useSearchParams(SearchRoute).q || ""; 18 | const [query, setQuery] = useState(q); 19 | const [pokemon, setPokemon] = useState(initialPokemon); 20 | 21 | const search = async () => { 22 | const data = await getPokemonSearch({}, { q: query }); 23 | setPokemon(data); 24 | }; 25 | 26 | return ( 27 |
28 |
29 | setQuery(e.target.value)} 34 | onKeyUp={(e) => { 35 | if (e.key !== "Enter") return; 36 | search(); 37 | }} 38 | /> 39 | 47 |
48 | 49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /examples/nextjs/finished/src/app/search/page.info.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const Route = { 4 | name: "Search", 5 | params: z.object({}), 6 | search: z.object({ 7 | q: z.string().optional(), 8 | limit: z.coerce.number().optional() 9 | }) 10 | }; 11 | -------------------------------------------------------------------------------- /examples/nextjs/finished/src/app/search/page.tsx: -------------------------------------------------------------------------------- 1 | import { getFullPokemon } from "@/pokemon"; 2 | 3 | import SearchList from "./SearchList"; 4 | 5 | export const dynamic = "force-dynamic"; 6 | 7 | export default async function SearchPage({ 8 | searchParams 9 | }: { 10 | searchParams: { q?: string }; 11 | }) { 12 | const pokemon = await getFullPokemon(10, searchParams.q); 13 | 14 | return ( 15 |
16 | 17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /examples/nextjs/finished/src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /examples/nextjs/finished/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /examples/nextjs/finished/src/pokemon.ts: -------------------------------------------------------------------------------- 1 | import { Pokemon } from "./types"; 2 | 3 | function upperCaseFirstLetter(string: string) { 4 | return string.charAt(0).toUpperCase() + string.slice(1); 5 | } 6 | 7 | function summarizePokemon(pokemon: any): Pokemon { 8 | return { 9 | name: upperCaseFirstLetter(pokemon.name), 10 | id: pokemon.id, 11 | image: 12 | pokemon.sprites?.other?.["official-artwork"]?.front_default || 13 | pokemon.sprites.front_default, 14 | species: pokemon.species.name, 15 | types: pokemon.types 16 | .slice(0, 10) 17 | .map((s: any) => s.type.name) 18 | .join(", "), 19 | stats: pokemon.stats 20 | .slice(0, 10) 21 | .map((s: any) => `${s.stat.name}: ${s.base_stat}`) 22 | .join(", "), 23 | moves: pokemon.moves 24 | .slice(0, 10) 25 | .map((m: any) => m.move.name) 26 | .join(", "), 27 | }; 28 | } 29 | 30 | export async function getFullPokemon( 31 | limit: number = 10, 32 | q?: string 33 | ): Promise { 34 | const resp = await fetch( 35 | `https://pokeapi.co/api/v2/pokemon?limit=${ 36 | q ? 10000 : limit || 10 37 | }&offset=0` 38 | ); 39 | const json = await resp.json(); 40 | 41 | let results: any[] = json.results; 42 | if (q) { 43 | results = results 44 | .filter(({ name }: { name: string }) => name.includes(q)) 45 | .slice(0, +limit); 46 | } 47 | 48 | return await Promise.all( 49 | results.slice(0, +limit).map(async ({ url }: { url: string }) => { 50 | const resp = await fetch(url); 51 | return summarizePokemon(await resp.json()); 52 | }) 53 | ); 54 | } 55 | 56 | export async function getPokemon(id: number): Promise { 57 | const resp = await fetch(`https://pokeapi.co/api/v2/pokemon/${id}`); 58 | return summarizePokemon(await resp.json()); 59 | } 60 | -------------------------------------------------------------------------------- /examples/nextjs/finished/src/routes/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/navigation"; 2 | import { 3 | useParams as useNextParams, 4 | useSearchParams as useNextSearchParams 5 | } from "next/navigation"; 6 | import { z } from "zod"; 7 | 8 | import { RouteBuilder } from "./makeRoute"; 9 | 10 | const emptySchema = z.object({}); 11 | 12 | type PushOptions = Parameters["push"]>[1]; 13 | 14 | export function usePush< 15 | Params extends z.ZodSchema, 16 | Search extends z.ZodSchema = typeof emptySchema 17 | >(builder: RouteBuilder) { 18 | const { push } = useRouter(); 19 | return ( 20 | p: z.input, 21 | search?: z.input, 22 | options?: PushOptions 23 | ) => { 24 | push(builder(p, search), options); 25 | }; 26 | } 27 | 28 | export function useParams< 29 | Params extends z.ZodSchema, 30 | Search extends z.ZodSchema = typeof emptySchema 31 | >(builder: RouteBuilder): z.output { 32 | const res = builder.paramsSchema.safeParse(useNextParams()); 33 | if (!res.success) { 34 | throw new Error( 35 | `Invalid route params for route ${builder.routeName}: ${res.error.message}` 36 | ); 37 | } 38 | return res.data; 39 | } 40 | 41 | export function useSearchParams< 42 | Params extends z.ZodSchema, 43 | Search extends z.ZodSchema = typeof emptySchema 44 | >(builder: RouteBuilder): z.output { 45 | const res = builder.searchSchema!.safeParse( 46 | convertURLSearchParamsToObject(useNextSearchParams()) 47 | ); 48 | if (!res.success) { 49 | throw new Error( 50 | `Invalid search params for route ${builder.routeName}: ${res.error.message}` 51 | ); 52 | } 53 | return res.data; 54 | } 55 | 56 | function convertURLSearchParamsToObject( 57 | params: Readonly | null 58 | ): Record { 59 | if (!params) { 60 | return {}; 61 | } 62 | 63 | const obj: Record = {}; 64 | // @ts-ignore 65 | for (const [key, value] of params.entries()) { 66 | if (params.getAll(key).length > 1) { 67 | obj[key] = params.getAll(key); 68 | } else { 69 | obj[key] = value; 70 | } 71 | } 72 | return obj; 73 | } 74 | -------------------------------------------------------------------------------- /examples/nextjs/finished/src/routes/index.ts: -------------------------------------------------------------------------------- 1 | // Automatically generated by declarative-routing, do NOT edit 2 | import { z } from "zod"; 3 | import { makeGetRoute, makeRoute } from "./makeRoute"; 4 | 5 | const defaultInfo = { 6 | search: z.object({}) 7 | }; 8 | 9 | import * as HomeRoute from "@/app/page.info"; 10 | import * as PokemonSearchRoute from "@/app/api/pokemon/route.info"; 11 | import * as ApiPokemonPokemonIdRoute from "@/app/api/pokemon/[pokemonId]/route.info"; 12 | import * as PokemonDetailRoute from "@/app/pokemon/[pokemonId]/page.info"; 13 | import * as SearchRoute from "@/app/search/page.info"; 14 | 15 | export const Home = makeRoute( 16 | "/", 17 | { 18 | ...defaultInfo, 19 | ...HomeRoute.Route 20 | } 21 | ); 22 | export const PokemonDetail = makeRoute( 23 | "/pokemon/[pokemonId]", 24 | { 25 | ...defaultInfo, 26 | ...PokemonDetailRoute.Route 27 | } 28 | ); 29 | export const Search = makeRoute( 30 | "/search", 31 | { 32 | ...defaultInfo, 33 | ...SearchRoute.Route 34 | } 35 | ); 36 | 37 | export const getPokemonSearch = makeGetRoute( 38 | "/api/pokemon", 39 | { 40 | ...defaultInfo, 41 | ...PokemonSearchRoute.Route 42 | }, 43 | PokemonSearchRoute.GET 44 | ); 45 | export const getApiPokemonPokemonId = makeGetRoute( 46 | "/api/pokemon/[pokemonId]", 47 | { 48 | ...defaultInfo, 49 | ...ApiPokemonPokemonIdRoute.Route 50 | }, 51 | ApiPokemonPokemonIdRoute.GET 52 | ); 53 | -------------------------------------------------------------------------------- /examples/nextjs/finished/src/types.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const PokemonSchema = z.object({ 4 | id: z.number(), 5 | name: z.string(), 6 | species: z.string(), 7 | types: z.string(), 8 | stats: z.string(), 9 | moves: z.string(), 10 | image: z.string(), 11 | }); 12 | 13 | export type Pokemon = z.infer; 14 | -------------------------------------------------------------------------------- /examples/nextjs/finished/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /examples/nextjs/starter/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /examples/nextjs/starter/.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /examples/nextjs/starter/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /examples/nextjs/starter/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /examples/nextjs/starter/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: "https", 7 | hostname: "raw.githubusercontent.com", 8 | port: "", 9 | }, 10 | ], 11 | }, 12 | }; 13 | 14 | export default nextConfig; 15 | -------------------------------------------------------------------------------- /examples/nextjs/starter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pokemon", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "class-variance-authority": "^0.7.0", 13 | "clsx": "^2.1.0", 14 | "lucide-react": "^0.331.0", 15 | "next": "14.2.0", 16 | "react": "^18", 17 | "react-dom": "^18", 18 | "tailwind-merge": "^2.2.1", 19 | "tailwindcss-animate": "^1.0.7" 20 | }, 21 | "devDependencies": { 22 | "@tailwindcss/container-queries": "^0.1.1", 23 | "@types/node": "^20", 24 | "@types/react": "^18", 25 | "@types/react-dom": "^18", 26 | "autoprefixer": "^10.0.1", 27 | "eslint": "^8", 28 | "eslint-config-next": "14.1.0", 29 | "postcss": "^8", 30 | "tailwindcss": "^3.3.0", 31 | "typescript": "^5" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/nextjs/starter/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /examples/nextjs/starter/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/nextjs/starter/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/nextjs/starter/revert: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | find . -name "route.info.ts" | xargs rm 3 | find . -name "page.info.ts" | xargs rm 4 | git reset --hard HEAD 5 | rm -fr declarative-routing.config.json src/routes/ 6 | -------------------------------------------------------------------------------- /examples/nextjs/starter/src/app/api/pokemon/[pokemonId]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse, NextRequest } from "next/server"; 2 | 3 | import { getPokemon } from "@/pokemon"; 4 | 5 | export const dynamic = "force-dynamic"; 6 | 7 | export async function GET( 8 | req: NextRequest, 9 | { params }: { params: { pokemonId: string } } 10 | ) { 11 | return NextResponse.json(await getPokemon(+(params.pokemonId || ""))); 12 | } 13 | -------------------------------------------------------------------------------- /examples/nextjs/starter/src/app/api/pokemon/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | 3 | import { getFullPokemon } from "@/pokemon"; 4 | 5 | export const dynamic = "force-dynamic"; 6 | 7 | export async function GET(req: NextRequest) { 8 | const url = new URL(req.url); 9 | const q = url.searchParams.get("q") ?? ""; 10 | const limit = url.searchParams.get("limit") ?? 10; 11 | return NextResponse.json(await getFullPokemon(+limit, q)); 12 | } 13 | -------------------------------------------------------------------------------- /examples/nextjs/starter/src/app/components/PokemonCard.tsx: -------------------------------------------------------------------------------- 1 | import { Pokemon } from "@/types"; 2 | import Image from "next/image"; 3 | 4 | export function PokemonCard({ pokemon }: { pokemon: Pokemon }) { 5 | return ( 6 |
7 |
8 | {pokemon.name} 15 |
16 |
17 |

{pokemon.name}

18 |
19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /examples/nextjs/starter/src/app/components/PokemonGrid.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import { PokemonCard } from "./PokemonCard"; 4 | import { Pokemon } from "@/types"; 5 | 6 | export function PokemonGrid({ pokemon }: { pokemon: Pokemon[] }) { 7 | return ( 8 |
9 | {pokemon.map((p) => ( 10 | 11 | 12 | 13 | ))} 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /examples/nextjs/starter/src/app/components/PokemonInfo.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Image from "next/image"; 3 | import { useEffect, useState } from "react"; 4 | import { Pokemon } from "@/types"; 5 | 6 | export function PokemonInfo({ 7 | id, 8 | pokemon: initialPokemon, 9 | }: { 10 | id: number; 11 | pokemon?: Pokemon; 12 | }) { 13 | const [data, setData] = useState(); 14 | 15 | useEffect(() => { 16 | if (!initialPokemon) { 17 | fetch(`/api/pokemon/${id}`) 18 | .then((res) => res.json()) 19 | .then((data) => setData(data)); 20 | } 21 | }, [id, initialPokemon]); 22 | 23 | const pokemon = initialPokemon || data; 24 | 25 | return pokemon ? ( 26 |
27 |
28 |
29 | {pokemon.name} 36 |
37 | 38 |
39 |

{pokemon.name}

40 |
41 |
Species
42 |
{pokemon.species}
43 |
44 |
45 |
Types
46 |
{pokemon.types}
47 |
48 |
49 |
Stats
50 |
{pokemon.stats}
51 |
52 |
53 |
Moves
54 |
{pokemon.moves}
55 |
56 |
57 |
58 |
59 | ) : null; 60 | } 61 | -------------------------------------------------------------------------------- /examples/nextjs/starter/src/app/components/SelectableGrid.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useState } from "react"; 3 | import Link from "next/link"; 4 | 5 | import { Pokemon } from "@/types"; 6 | 7 | import { PokemonCard } from "./PokemonCard"; 8 | import { PokemonInfo } from "./PokemonInfo"; 9 | 10 | export function SelectableGrid({ pokemon }: { pokemon: Pokemon[] }) { 11 | const [selected, setSelected] = useState(); 12 | 13 | return ( 14 |
15 |
16 | {pokemon.map((p) => ( 17 |
setSelected(p)} key={p.id}> 18 | 19 |
20 | ))} 21 |
22 | {selected && ( 23 |
24 | 25 | 26 | 27 |
28 | )} 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /examples/nextjs/starter/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProNextJS/declarative-routing/5d933852b072461eea9aa266cb55d0a0f1ee01f7/examples/nextjs/starter/src/app/favicon.ico -------------------------------------------------------------------------------- /examples/nextjs/starter/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 222.2 84% 4.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 84% 4.9%; 15 | 16 | --primary: 222.2 47.4% 11.2%; 17 | --primary-foreground: 210 40% 98%; 18 | 19 | --secondary: 210 40% 96.1%; 20 | --secondary-foreground: 222.2 47.4% 11.2%; 21 | 22 | --muted: 210 40% 96.1%; 23 | --muted-foreground: 215.4 16.3% 46.9%; 24 | 25 | --accent: 210 40% 96.1%; 26 | --accent-foreground: 222.2 47.4% 11.2%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 210 40% 98%; 30 | 31 | --border: 214.3 31.8% 91.4%; 32 | --input: 214.3 31.8% 91.4%; 33 | --ring: 222.2 84% 4.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 222.2 84% 4.9%; 40 | --foreground: 210 40% 98%; 41 | 42 | --card: 222.2 84% 4.9%; 43 | --card-foreground: 210 40% 98%; 44 | 45 | --popover: 222.2 84% 4.9%; 46 | --popover-foreground: 210 40% 98%; 47 | 48 | --primary: 210 40% 98%; 49 | --primary-foreground: 222.2 47.4% 11.2%; 50 | 51 | --secondary: 217.2 32.6% 17.5%; 52 | --secondary-foreground: 210 40% 98%; 53 | 54 | --muted: 217.2 32.6% 17.5%; 55 | --muted-foreground: 215 20.2% 65.1%; 56 | 57 | --accent: 217.2 32.6% 17.5%; 58 | --accent-foreground: 210 40% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 210 40% 98%; 62 | 63 | --border: 217.2 32.6% 17.5%; 64 | --input: 217.2 32.6% 17.5%; 65 | --ring: 212.7 26.8% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } -------------------------------------------------------------------------------- /examples/nextjs/starter/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import Link from "next/link"; 4 | 5 | import "./globals.css"; 6 | 7 | const inter = Inter({ subsets: ["latin"] }); 8 | 9 | export const metadata: Metadata = { 10 | title: "Create Next App", 11 | description: "Generated by create next app", 12 | }; 13 | 14 | export default function RootLayout({ 15 | children, 16 | }: Readonly<{ 17 | children: React.ReactNode; 18 | }>) { 19 | return ( 20 | 21 | 22 |
23 | 24 | Home 25 | 26 | 27 | Search 28 | 29 |
30 |
{children}
31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /examples/nextjs/starter/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { getFullPokemon } from "@/pokemon"; 2 | 3 | import { SelectableGrid } from "@/app/components/SelectableGrid"; 4 | 5 | export default async function Home() { 6 | const pokemon = await getFullPokemon(); 7 | 8 | return ( 9 |
10 | 11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /examples/nextjs/starter/src/app/pokemon/[pokemonId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { PokemonInfo } from "@/app/components/PokemonInfo"; 2 | 3 | import { getPokemon } from "@/pokemon"; 4 | 5 | export default async function PokemonDetailPage({ 6 | params: { pokemonId }, 7 | }: { 8 | params: { pokemonId: string }; 9 | }) { 10 | const pokemon = await getPokemon(+pokemonId); 11 | return ( 12 |
13 | 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /examples/nextjs/starter/src/app/search/SearchList.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useState } from "react"; 3 | import { Input } from "@/components/ui/input"; 4 | 5 | import { Pokemon } from "@/types"; 6 | 7 | import { PokemonGrid } from "@/app/components/PokemonGrid"; 8 | import { useSearchParams } from "next/navigation"; 9 | 10 | export default function Search({ 11 | pokemon: initialPokemon, 12 | }: { 13 | pokemon: Pokemon[]; 14 | }) { 15 | const q = (useSearchParams().get("q") as string) || ""; 16 | const [query, setQuery] = useState(q); 17 | const [pokemon, setPokemon] = useState(initialPokemon); 18 | 19 | const search = async () => { 20 | const resp = await fetch(`/api/pokemon?q=${encodeURIComponent(query)}`); 21 | const data = await resp.json(); 22 | setPokemon(data); 23 | }; 24 | 25 | return ( 26 |
27 |
28 | setQuery(e.target.value)} 33 | onKeyUp={(e) => { 34 | if (e.key !== "Enter") return; 35 | search(); 36 | }} 37 | /> 38 | 46 |
47 | 48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /examples/nextjs/starter/src/app/search/page.tsx: -------------------------------------------------------------------------------- 1 | import { getFullPokemon } from "@/pokemon"; 2 | 3 | import SearchList from "./SearchList"; 4 | 5 | export const dynamic = "force-dynamic"; 6 | 7 | export default async function SearchPage({ 8 | searchParams, 9 | }: { 10 | searchParams: { q?: string }; 11 | }) { 12 | const pokemon = await getFullPokemon(10, searchParams.q); 13 | 14 | return ( 15 |
16 | 17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /examples/nextjs/starter/src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /examples/nextjs/starter/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /examples/nextjs/starter/src/pokemon.ts: -------------------------------------------------------------------------------- 1 | import { Pokemon } from "./types"; 2 | 3 | function upperCaseFirstLetter(string: string) { 4 | return string.charAt(0).toUpperCase() + string.slice(1); 5 | } 6 | 7 | function summarizePokemon(pokemon: any): Pokemon { 8 | return { 9 | name: upperCaseFirstLetter(pokemon.name), 10 | id: pokemon.id, 11 | image: 12 | pokemon.sprites?.other?.["official-artwork"]?.front_default || 13 | pokemon.sprites.front_default, 14 | species: pokemon.species.name, 15 | types: pokemon.types 16 | .slice(0, 10) 17 | .map((s: any) => s.type.name) 18 | .join(", "), 19 | stats: pokemon.stats 20 | .slice(0, 10) 21 | .map((s: any) => `${s.stat.name}: ${s.base_stat}`) 22 | .join(", "), 23 | moves: pokemon.moves 24 | .slice(0, 10) 25 | .map((m: any) => m.move.name) 26 | .join(", "), 27 | }; 28 | } 29 | 30 | export async function getFullPokemon( 31 | limit: number = 10, 32 | q?: string 33 | ): Promise { 34 | const resp = await fetch( 35 | `https://pokeapi.co/api/v2/pokemon?limit=${ 36 | q ? 10000 : limit || 10 37 | }&offset=0` 38 | ); 39 | const json = await resp.json(); 40 | 41 | let results: any[] = json.results; 42 | if (q) { 43 | results = results 44 | .filter(({ name }: { name: string }) => name.includes(q)) 45 | .slice(0, +limit); 46 | } 47 | 48 | return await Promise.all( 49 | results.slice(0, +limit).map(async ({ url }: { url: string }) => { 50 | const resp = await fetch(url); 51 | return summarizePokemon(await resp.json()); 52 | }) 53 | ); 54 | } 55 | 56 | export async function getPokemon(id: number): Promise { 57 | const resp = await fetch(`https://pokeapi.co/api/v2/pokemon/${id}`); 58 | return summarizePokemon(await resp.json()); 59 | } 60 | -------------------------------------------------------------------------------- /examples/nextjs/starter/src/types.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const PokemonSchema = z.object({ 4 | id: z.number(), 5 | name: z.string(), 6 | species: z.string(), 7 | types: z.string(), 8 | stats: z.string(), 9 | moves: z.string(), 10 | image: z.string(), 11 | }); 12 | 13 | export type Pokemon = z.infer; 14 | -------------------------------------------------------------------------------- /examples/nextjs/starter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /examples/qwikcity/finished/.eslintignore: -------------------------------------------------------------------------------- 1 | **/*.log 2 | **/.DS_Store 3 | *. 4 | .vscode/settings.json 5 | .history 6 | .yarn 7 | bazel-* 8 | bazel-bin 9 | bazel-out 10 | bazel-qwik 11 | bazel-testlogs 12 | dist 13 | dist-dev 14 | lib 15 | lib-types 16 | etc 17 | external 18 | node_modules 19 | temp 20 | tsc-out 21 | tsdoc-metadata.json 22 | target 23 | output 24 | rollup.config.js 25 | build 26 | .cache 27 | .vscode 28 | .rollup.cache 29 | dist 30 | tsconfig.tsbuildinfo 31 | vite.config.ts 32 | *.spec.tsx 33 | *.spec.ts 34 | .netlify 35 | pnpm-lock.yaml 36 | package-lock.json 37 | yarn.lock 38 | server 39 | -------------------------------------------------------------------------------- /examples/qwikcity/finished/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | es2021: true, 6 | node: true, 7 | }, 8 | extends: [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:qwik/recommended", 12 | ], 13 | parser: "@typescript-eslint/parser", 14 | parserOptions: { 15 | tsconfigRootDir: __dirname, 16 | project: ["./tsconfig.json"], 17 | ecmaVersion: 2021, 18 | sourceType: "module", 19 | ecmaFeatures: { 20 | jsx: true, 21 | }, 22 | }, 23 | plugins: ["@typescript-eslint"], 24 | rules: { 25 | "@typescript-eslint/no-explicit-any": "off", 26 | "@typescript-eslint/explicit-module-boundary-types": "off", 27 | "@typescript-eslint/no-inferrable-types": "off", 28 | "@typescript-eslint/no-non-null-assertion": "off", 29 | "@typescript-eslint/no-empty-interface": "off", 30 | "@typescript-eslint/no-namespace": "off", 31 | "@typescript-eslint/no-empty-function": "off", 32 | "@typescript-eslint/no-this-alias": "off", 33 | "@typescript-eslint/ban-types": "off", 34 | "@typescript-eslint/ban-ts-comment": "off", 35 | "prefer-spread": "off", 36 | "no-case-declarations": "off", 37 | "no-console": "off", 38 | "@typescript-eslint/no-unused-vars": ["error"], 39 | "@typescript-eslint/consistent-type-imports": "warn", 40 | "@typescript-eslint/no-unnecessary-condition": "warn", 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /examples/qwikcity/finished/.gitignore: -------------------------------------------------------------------------------- 1 | # Build 2 | /dist 3 | /lib 4 | /lib-types 5 | /server 6 | 7 | # Development 8 | node_modules 9 | *.local 10 | 11 | # Cache 12 | .cache 13 | .mf 14 | .rollup.cache 15 | tsconfig.tsbuildinfo 16 | 17 | # Logs 18 | logs 19 | *.log 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | pnpm-debug.log* 24 | lerna-debug.log* 25 | 26 | # Editor 27 | .vscode/* 28 | !.vscode/launch.json 29 | !.vscode/*.code-snippets 30 | 31 | .idea 32 | .DS_Store 33 | *.suo 34 | *.ntvs* 35 | *.njsproj 36 | *.sln 37 | *.sw? 38 | 39 | # Yarn 40 | .yarn/* 41 | !.yarn/releases 42 | -------------------------------------------------------------------------------- /examples/qwikcity/finished/.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.log 2 | **/.DS_Store 3 | *. 4 | .vscode/settings.json 5 | .history 6 | .yarn 7 | bazel-* 8 | bazel-bin 9 | bazel-out 10 | bazel-qwik 11 | bazel-testlogs 12 | dist 13 | dist-dev 14 | lib 15 | lib-types 16 | etc 17 | external 18 | node_modules 19 | temp 20 | tsc-out 21 | tsdoc-metadata.json 22 | target 23 | output 24 | rollup.config.js 25 | build 26 | .cache 27 | .vscode 28 | .rollup.cache 29 | tsconfig.tsbuildinfo 30 | vite.config.ts 31 | *.spec.tsx 32 | *.spec.ts 33 | .netlify 34 | pnpm-lock.yaml 35 | package-lock.json 36 | yarn.lock 37 | server 38 | -------------------------------------------------------------------------------- /examples/qwikcity/finished/.prettierrc.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: ['prettier-plugin-tailwindcss'], 3 | } -------------------------------------------------------------------------------- /examples/qwikcity/finished/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch Chrome", 9 | "request": "launch", 10 | "type": "chrome", 11 | "url": "http://localhost:5173", 12 | "webRoot": "${workspaceFolder}" 13 | }, 14 | { 15 | "type": "node", 16 | "name": "dev.debug", 17 | "request": "launch", 18 | "skipFiles": ["/**"], 19 | "cwd": "${workspaceFolder}", 20 | "program": "${workspaceFolder}/node_modules/vite/bin/vite.js", 21 | "args": ["--mode", "ssr", "--force"] 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /examples/qwikcity/finished/.vscode/qwik-city.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "onRequest": { 3 | "scope": "javascriptreact,typescriptreact", 4 | "prefix": "qonRequest", 5 | "description": "onRequest function for a route index", 6 | "body": [ 7 | "export const onRequest: RequestHandler = (request) => {", 8 | " $0", 9 | "};", 10 | ], 11 | }, 12 | "loader$": { 13 | "scope": "javascriptreact,typescriptreact", 14 | "prefix": "qloader$", 15 | "description": "loader$()", 16 | "body": ["export const $1 = routeLoader$(() => {", " $0", "});"], 17 | }, 18 | "action$": { 19 | "scope": "javascriptreact,typescriptreact", 20 | "prefix": "qaction$", 21 | "description": "action$()", 22 | "body": ["export const $1 = routeAction$((data) => {", " $0", "});"], 23 | }, 24 | "Full Page": { 25 | "scope": "javascriptreact,typescriptreact", 26 | "prefix": "qpage", 27 | "description": "Simple page component", 28 | "body": [ 29 | "import { component$ } from '@builder.io/qwik';", 30 | "", 31 | "export default component$(() => {", 32 | " $0", 33 | "});", 34 | ], 35 | }, 36 | } 37 | -------------------------------------------------------------------------------- /examples/qwikcity/finished/.vscode/qwik.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "Qwik component (simple)": { 3 | "scope": "javascriptreact,typescriptreact", 4 | "prefix": "qcomponent$", 5 | "description": "Simple Qwik component", 6 | "body": [ 7 | "export const ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} = component$(() => {", 8 | " return <${2:div}>$4", 9 | "});", 10 | ], 11 | }, 12 | "Qwik component (props)": { 13 | "scope": "typescriptreact", 14 | "prefix": "qcomponent$ + props", 15 | "description": "Qwik component w/ props", 16 | "body": [ 17 | "export interface ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}Props {", 18 | " $2", 19 | "}", 20 | "", 21 | "export const $1 = component$<$1Props>((props) => {", 22 | " const ${2:count} = useSignal(0);", 23 | " return (", 24 | " <${3:div} on${4:Click}$={(ev) => {$5}}>", 25 | " $6", 26 | " ", 27 | " );", 28 | "});", 29 | ], 30 | }, 31 | "Qwik signal": { 32 | "scope": "javascriptreact,typescriptreact", 33 | "prefix": "quseSignal", 34 | "description": "useSignal() declaration", 35 | "body": ["const ${1:foo} = useSignal($2);", "$0"], 36 | }, 37 | "Qwik store": { 38 | "scope": "javascriptreact,typescriptreact", 39 | "prefix": "quseStore", 40 | "description": "useStore() declaration", 41 | "body": ["const ${1:state} = useStore({", " $2", "});", "$0"], 42 | }, 43 | "$ hook": { 44 | "scope": "javascriptreact,typescriptreact", 45 | "prefix": "q$", 46 | "description": "$() function hook", 47 | "body": ["$(() => {", " $0", "});", ""], 48 | }, 49 | "useVisibleTask": { 50 | "scope": "javascriptreact,typescriptreact", 51 | "prefix": "quseVisibleTask", 52 | "description": "useVisibleTask$() function hook", 53 | "body": ["useVisibleTask$(({ track }) => {", " $0", "});", ""], 54 | }, 55 | "useTask": { 56 | "scope": "javascriptreact,typescriptreact", 57 | "prefix": "quseTask$", 58 | "description": "useTask$() function hook", 59 | "body": [ 60 | "useTask$(({ track }) => {", 61 | " track(() => $1);", 62 | " $0", 63 | "});", 64 | "", 65 | ], 66 | }, 67 | "useResource": { 68 | "scope": "javascriptreact,typescriptreact", 69 | "prefix": "quseResource$", 70 | "description": "useResource$() declaration", 71 | "body": [ 72 | "const $1 = useResource$(({ track, cleanup }) => {", 73 | " $0", 74 | "});", 75 | "", 76 | ], 77 | }, 78 | } 79 | -------------------------------------------------------------------------------- /examples/qwikcity/finished/declarative-routing.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "mode": "qwikcity", 3 | "src": "./src/routes", 4 | "routes": "./src/declarativeRoutes" 5 | } -------------------------------------------------------------------------------- /examples/qwikcity/finished/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-qwik-empty-starter", 3 | "description": "App with Routing built-in ready to create your app", 4 | "engines": { 5 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 6 | }, 7 | "engines-annotation": "Mostly required by sharp which needs a Node-API v9 compatible runtime", 8 | "private": true, 9 | "trustedDependencies": [ 10 | "sharp" 11 | ], 12 | "trustedDependencies-annotation": "Needed for bun to allow running install scripts", 13 | "type": "module", 14 | "scripts": { 15 | "build": "qwik build", 16 | "build.client": "vite build", 17 | "build.preview": "vite build --ssr src/entry.preview.tsx", 18 | "build.types": "tsc --incremental --noEmit", 19 | "deploy": "echo 'Run \"npm run qwik add\" to install a server adapter'", 20 | "dev": "vite --mode ssr", 21 | "dev.debug": "node --inspect-brk ./node_modules/vite/bin/vite.js --mode ssr --force", 22 | "fmt": "prettier --write .", 23 | "fmt.check": "prettier --check .", 24 | "lint": "eslint \"src/**/*.ts*\"", 25 | "preview": "qwik build preview && vite preview --open", 26 | "start": "vite --open --mode ssr", 27 | "qwik": "qwik", 28 | "dr:build": "npx declarative-routing build", 29 | "dr:build:watch": "npx declarative-routing build --watch" 30 | }, 31 | "devDependencies": { 32 | "@builder.io/qwik": "^1.5.2", 33 | "@builder.io/qwik-city": "^1.5.2", 34 | "@tailwindcss/container-queries": "^0.1.1", 35 | "@types/eslint": "^8.56.6", 36 | "@types/node": "^20.11.30", 37 | "@typescript-eslint/eslint-plugin": "^7.3.1", 38 | "@typescript-eslint/parser": "^7.3.1", 39 | "autoprefixer": "^10.4.14", 40 | "eslint": "^8.57.0", 41 | "eslint-plugin-qwik": "^1.5.2", 42 | "postcss": "^8.4.31", 43 | "prettier": "^3.2.5", 44 | "prettier-plugin-tailwindcss": "^0.5.4", 45 | "tailwindcss": "3.3.3", 46 | "typescript": "5.3.3", 47 | "undici": "*", 48 | "vite": "^5.1.6", 49 | "vite-tsconfig-paths": "^4.2.1" 50 | }, 51 | "dependencies": { 52 | "clsx": "^2.1.0", 53 | "query-string": "^9.0.0", 54 | "tailwind-merge": "^2.3.0", 55 | "tailwindcss-animate": "^1.0.7", 56 | "zod": "^3.22.5" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /examples/qwikcity/finished/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /examples/qwikcity/finished/public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/qwikcity/finished/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/web-manifest-combined.json", 3 | "name": "qwik-project-name", 4 | "short_name": "Welcome to Qwik", 5 | "start_url": ".", 6 | "display": "standalone", 7 | "background_color": "#fff", 8 | "description": "A Qwik project app." 9 | } 10 | -------------------------------------------------------------------------------- /examples/qwikcity/finished/public/robots.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProNextJS/declarative-routing/5d933852b072461eea9aa266cb55d0a0f1ee01f7/examples/qwikcity/finished/public/robots.txt -------------------------------------------------------------------------------- /examples/qwikcity/finished/src/components/PokemonCard.tsx: -------------------------------------------------------------------------------- 1 | import { component$ } from "@builder.io/qwik"; 2 | import type { Pokemon } from "~/types"; 3 | 4 | export const PokemonCard = component$<{ pokemon: Pokemon }>(({ pokemon }) => { 5 | return ( 6 |
7 |
8 | {pokemon.name} 15 |
16 |
17 |

{pokemon.name}

18 |
19 |
20 | ); 21 | }); 22 | -------------------------------------------------------------------------------- /examples/qwikcity/finished/src/components/PokemonGrid.tsx: -------------------------------------------------------------------------------- 1 | import { PokemonCard } from "./PokemonCard"; 2 | import type { Pokemon } from "~/types"; 3 | import { component$ } from "@builder.io/qwik"; 4 | import { PokemonPokemonId } from "~/declarativeRoutes"; 5 | 6 | export const PokemonGrid = component$<{ pokemon: Pokemon[] }>(({ pokemon }) => { 7 | return ( 8 |
9 | {pokemon.map((p) => ( 10 | 11 | 12 | 13 | ))} 14 |
15 | ); 16 | }); 17 | -------------------------------------------------------------------------------- /examples/qwikcity/finished/src/components/PokemonInfo.tsx: -------------------------------------------------------------------------------- 1 | import type { Pokemon } from "~/types"; 2 | import { component$, useSignal, useTask$ } from "@builder.io/qwik"; 3 | import { useLocation } from "@builder.io/qwik-city"; 4 | 5 | export const PokemonInfo = component$<{ 6 | id: number; 7 | pokemon?: Pokemon; 8 | }>(({ id, pokemon: initialPokemon }) => { 9 | const pokemonData = useSignal(); 10 | const location = useLocation(); 11 | 12 | useTask$(async ({ track }) => { 13 | track(() => id); 14 | 15 | const res = await fetch(`${location.url.origin}/api/pokemon/${id}`); 16 | const json = await res.json(); 17 | 18 | pokemonData.value = json; 19 | }); 20 | 21 | const pokemon = initialPokemon || pokemonData.value; 22 | 23 | return pokemon ? ( 24 |
25 |
26 |
27 | {pokemon.name} 34 |
35 | 36 |
37 |

{pokemon.name}

38 |
39 |
Species
40 |
{pokemon.species}
41 |
42 |
43 |
Types
44 |
{pokemon.types}
45 |
46 |
47 |
Stats
48 |
{pokemon.stats}
49 |
50 |
51 |
Moves
52 |
{pokemon.moves}
53 |
54 |
55 |
56 |
57 | ) : null; 58 | }); 59 | -------------------------------------------------------------------------------- /examples/qwikcity/finished/src/components/SelectableGrid.tsx: -------------------------------------------------------------------------------- 1 | import { component$, useSignal } from "@builder.io/qwik"; 2 | import { PokemonPokemonId } from "~/declarativeRoutes"; 3 | import type { Pokemon } from "~/types"; 4 | import { PokemonCard } from "./PokemonCard"; 5 | import { PokemonInfo } from "./PokemonInfo"; 6 | 7 | export default component$<{ pokemon: Pokemon[] }>(({ pokemon }) => { 8 | const selectedId = useSignal(); 9 | 10 | return ( 11 |
12 |
13 | {pokemon.map((p) => ( 14 |
(selectedId.value = p.id)} key={p.id}> 15 | 16 |
17 | ))} 18 |
19 | {selectedId.value && ( 20 |
21 | 22 | 23 | 24 |
25 | )} 26 |
27 | ); 28 | }); 29 | -------------------------------------------------------------------------------- /examples/qwikcity/finished/src/components/router-head/router-head.tsx: -------------------------------------------------------------------------------- 1 | import { component$ } from "@builder.io/qwik"; 2 | import { useDocumentHead, useLocation } from "@builder.io/qwik-city"; 3 | 4 | /** 5 | * The RouterHead component is placed inside of the document `` element. 6 | */ 7 | export const RouterHead = component$(() => { 8 | const head = useDocumentHead(); 9 | const loc = useLocation(); 10 | 11 | return ( 12 | <> 13 | {head.title} 14 | 15 | 16 | 17 | 18 | 19 | {head.meta.map((m) => ( 20 | 21 | ))} 22 | 23 | {head.links.map((l) => ( 24 | 25 | ))} 26 | 27 | {head.styles.map((s) => ( 28 |