-
-
Notifications
You must be signed in to change notification settings - Fork 6
feat: Migrate to tanstack start - Stage 1 #760
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: canary
Are you sure you want to change the base?
Changes from all commits
cd5537a
bf94b10
8e6b4db
6a1d90c
b05047f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| { | ||
| "projectName": "web", | ||
| "mode": "file-router", | ||
| "typescript": true, | ||
| "packageManager": "npm", | ||
| "includeExamples": false, | ||
| "tailwind": true, | ||
| "projectPreset": "default", | ||
| "addOnOptions": {}, | ||
| "git": false, | ||
| "install": true, | ||
| "intent": true, | ||
| "routerOnly": false, | ||
| "version": 1, | ||
| "framework": "react", | ||
| "chosenAddOns": [ | ||
| "eslint", | ||
| "nitro" | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| POSTGRES_URL=postgresql://root:root@localhost:5432/vitnode | ||
| REDIS_URL=redis://localhost:6379 | ||
|
|
||
| # This app serves its own API at `/api/*`, so both name the same origin - and it | ||
| # has to be the port `pnpm dev` actually serves (`vite dev --port 3001`). Point | ||
| # either of them at 3000 and the browser talks to whatever else is on 3000, | ||
| # usually `apps/docs`, instead of the API mounted in this process. | ||
| # | ||
| # Neither side strictly needs them: `resolveApiOrigin()` in | ||
| # `src/server/fetcher.server.ts` takes the origin off the request being | ||
| # rendered, and in the browser `CONFIG.api` falls back to the origin the page | ||
| # was served from - so a preview deployment on a generated hostname needs no | ||
| # config at all. Set `NEXT_PUBLIC_API_URL` only to point at a separate API | ||
| # server; `NEXT_PUBLIC_WEB_URL` is still what the API stamps cookies with. | ||
| NEXT_PUBLIC_WEB_URL=http://localhost:3001 | ||
| NEXT_PUBLIC_API_URL=http://localhost:3001 | ||
|
|
||
| # === CRON Secret for Internal API Calls === | ||
| CRON_SECRET=your-secure-cron-secret-key | ||
|
|
||
| # === Docker Database Postgres === | ||
| POSTGRES_USER=root | ||
| POSTGRES_PASSWORD=root | ||
| POSTGRES_NAME=vitnode | ||
|
|
||
| # === Docker Redis === | ||
| REDIS_PASSWORD=root |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| node_modules | ||
| .DS_Store | ||
| dist | ||
| dist-ssr | ||
| *.local | ||
| .env | ||
| .nitro | ||
| .tanstack | ||
| .wrangler | ||
| .output | ||
| .vinxi | ||
| __unconfig* | ||
| todos.json |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| { | ||
| "files.watcherExclude": { | ||
| "**/routeTree.gen.ts": true | ||
| }, | ||
| "search.exclude": { | ||
| "**/routeTree.gen.ts": true | ||
| }, | ||
| "files.readonlyInclude": { | ||
| "**/routeTree.gen.ts": true | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import eslintVitNode from "@vitnode/config/eslint"; | ||
| import eslintVitNodeReact from "@vitnode/config/eslint.react"; | ||
| import { fileURLToPath } from "node:url"; | ||
| import { dirname } from "node:path"; | ||
|
|
||
| const __dirname = dirname(fileURLToPath(import.meta.url)); | ||
|
|
||
| export default [ | ||
| ...eslintVitNode, | ||
| ...eslintVitNodeReact, | ||
| { | ||
| // Build output, not source. `eslint .` walks these otherwise and every file | ||
| // in them fails to parse: they are outside `tsconfig.json`'s `include`. | ||
| ignores: [ | ||
| ".source", | ||
| ".nitro/**", | ||
| ".output/**", | ||
| ".tanstack/**", | ||
| "dist/**", | ||
| "src/routeTree.gen.ts", | ||
| "prettier.config.js", | ||
| ], | ||
| }, | ||
| { | ||
| languageOptions: { | ||
| parserOptions: { | ||
| project: "./tsconfig.json", | ||
| tsconfigRootDir: __dirname, | ||
| }, | ||
| }, | ||
| }, | ||
| ]; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| { | ||
| "name": "web", | ||
| "private": true, | ||
| "type": "module", | ||
| "imports": { | ||
| "#/*": "./src/*" | ||
| }, | ||
| "scripts": { | ||
| "dev": "vite dev --port 3001", | ||
| "generate-routes": "tsr generate", | ||
| "build": "vite build", | ||
| "preview": "vite preview", | ||
| "test": "vitest run", | ||
| "lint": "eslint .", | ||
| "format": "prettier --write . && eslint --fix", | ||
| "check": "prettier --check .", | ||
| "start": "node .output/server/index.mjs", | ||
| "typecheck": "tsc --noEmit", | ||
| "test:types": "tsc --noEmit", | ||
| "lint:fix": "eslint . --fix" | ||
| }, | ||
| "dependencies": { | ||
| "@hono/zod-openapi": "^1.5.1", | ||
| "@tailwindcss/vite": "^4.1.18", | ||
| "@tanstack/react-devtools": "^0.10.12", | ||
| "@tanstack/react-router": "^1.170.32", | ||
| "@tanstack/react-router-devtools": "^1.167.1", | ||
| "@tanstack/react-start": "^1.168.49", | ||
| "@vitnode/blog": "workspace:*", | ||
| "@vitnode/core": "workspace:*", | ||
| "@vitnode/example": "workspace:*", | ||
| "dotenv": "^17.4.2", | ||
| "drizzle-kit": "1.0.0-rc.4", | ||
| "drizzle-orm": "1.0.0-rc.4", | ||
| "hono": "^4.12.31", | ||
| "next-intl": "^4.13.7", | ||
| "nitro": "3.0.260610-beta", | ||
| "react": "^19.2.0", | ||
| "react-dom": "^19.2.0", | ||
| "tailwindcss": "^4.1.18", | ||
| "zod": "^4.4.3" | ||
| }, | ||
| "devDependencies": { | ||
| "@tanstack/devtools-vite": "^0.8.5", | ||
| "@tanstack/eslint-config": "^0.4.0", | ||
| "@tanstack/router-cli": "^1.132.0", | ||
| "@types/node": "^22.10.2", | ||
| "@types/react": "^19.2.0", | ||
| "@types/react-dom": "^19.2.0", | ||
| "@vitejs/plugin-react": "^6.0.1", | ||
| "@vitnode/config": "workspace:*", | ||
| "eslint": "^10.7.0", | ||
| "typescript": "^6.0.2", | ||
| "vite": "^8.0.0", | ||
| "vitest": "^4.1.10" | ||
| }, | ||
| "pnpm": { | ||
| "onlyBuiltDependencies": [ | ||
| "esbuild", | ||
| "lightningcss" | ||
| ] | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| // @ts-check | ||
|
|
||
| /** @type {import('prettier').Config} */ | ||
| const config = { | ||
| semi: false, | ||
| singleQuote: true, | ||
| trailingComma: "all", | ||
| }; | ||
|
|
||
| export default config; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| import type { usersModule } from '@vitnode/core/api/modules/users/users.module' | ||
|
|
||
| import { createServerFn } from '@tanstack/react-start' | ||
| import { clientModule } from '@vitnode/core/lib/fetcher-client' | ||
|
|
||
| import { fetcherServer } from '#/server/fetcher.server' | ||
|
|
||
| /** | ||
| * The users module by type only, so nothing the API needs at runtime - Hono, | ||
| * Drizzle, the plugin tree - is reachable from a module the router imports. | ||
| * `clientModule` keeps the route paths, methods and response schemas fully | ||
| * typed while carrying just the `pluginId` the fetcher reads. | ||
| */ | ||
| const users = clientModule<typeof usersModule>('@vitnode/core') | ||
|
|
||
| export type SessionApi = Awaited<ReturnType<typeof getSession>> | ||
|
|
||
| /** | ||
| * The signed-in visitor, or `{ user: null }` - the TanStack Start counterpart of | ||
| * `@vitnode/core`'s `getSessionApi()`. | ||
| * | ||
| * A `createServerFn` rather than a route `loader`, because a loader also runs in | ||
| * the browser on client-side navigation and there is no request to read there. | ||
| * As a server function it runs on the server both times: directly during SSR, | ||
| * and over same-origin RPC afterwards - which carries the visitor's cookies to | ||
| * this server, where `fetcherServer` forwards them on. | ||
| * | ||
| * Deliberately not cached, for the same reason `getSessionApi()` is not: the | ||
| * response is per-visitor and changes the moment they sign in or edit their | ||
| * profile, so there is no shared entry to hand out. The database work behind it | ||
| * is cached in Redis by the API instead. | ||
| * | ||
| * One call per navigation as long as callers read it through the route's loader | ||
| * data. Core wraps its version in React's `cache()` because a Next layout, | ||
| * header and page each ask for the session while rendering one page; if the same | ||
| * shape appears here, that per-render memoisation has to come with it. | ||
| */ | ||
| export const getSession = createServerFn().handler(async () => { | ||
| const response = await fetcherServer(users, { | ||
| method: 'get', | ||
| module: 'users', | ||
| path: '/session', | ||
| }) | ||
|
Comment on lines
+39
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a cookie-less visitor calls Useful? React with 👍 / 👎. |
||
|
|
||
| // A non-200 (a 429 from the rate limiter, say) carries something other than a | ||
| // session, so read it as "nobody is signed in" rather than crashing the render | ||
| // while parsing it. One shape either way, so callers never have to narrow. | ||
| if (response.status !== 200) { | ||
| return { ai: { models: [] }, user: null } | ||
| } | ||
|
|
||
| return await response.json() | ||
| }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| /* eslint-disable */ | ||
|
|
||
| // @ts-nocheck | ||
|
|
||
| // noinspection JSUnusedGlobalSymbols | ||
|
|
||
| // This file was automatically generated by TanStack Router. | ||
| // You should NOT make any changes in this file as it will be overwritten. | ||
| // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. | ||
|
|
||
| import { Route as rootRouteImport } from './routes/__root' | ||
| import { Route as IndexRouteImport } from './routes/index' | ||
| import { Route as ApiSplatRouteImport } from './routes/api/$' | ||
|
|
||
| const IndexRoute = IndexRouteImport.update({ | ||
| id: '/', | ||
| path: '/', | ||
| getParentRoute: () => rootRouteImport, | ||
| } as any) | ||
| const ApiSplatRoute = ApiSplatRouteImport.update({ | ||
| id: '/api/$', | ||
| path: '/api/$', | ||
| getParentRoute: () => rootRouteImport, | ||
| } as any) | ||
|
|
||
| export interface FileRoutesByFullPath { | ||
| '/': typeof IndexRoute | ||
| '/api/$': typeof ApiSplatRoute | ||
| } | ||
| export interface FileRoutesByTo { | ||
| '/': typeof IndexRoute | ||
| '/api/$': typeof ApiSplatRoute | ||
| } | ||
| export interface FileRoutesById { | ||
| __root__: typeof rootRouteImport | ||
| '/': typeof IndexRoute | ||
| '/api/$': typeof ApiSplatRoute | ||
| } | ||
| export interface FileRouteTypes { | ||
| fileRoutesByFullPath: FileRoutesByFullPath | ||
| fullPaths: '/' | '/api/$' | ||
| fileRoutesByTo: FileRoutesByTo | ||
| to: '/' | '/api/$' | ||
| id: '__root__' | '/' | '/api/$' | ||
| fileRoutesById: FileRoutesById | ||
| } | ||
| export interface RootRouteChildren { | ||
| IndexRoute: typeof IndexRoute | ||
| ApiSplatRoute: typeof ApiSplatRoute | ||
| } | ||
|
|
||
| declare module '@tanstack/react-router' { | ||
| interface FileRoutesByPath { | ||
| '/': { | ||
| id: '/' | ||
| path: '/' | ||
| fullPath: '/' | ||
| preLoaderRoute: typeof IndexRouteImport | ||
| parentRoute: typeof rootRouteImport | ||
| } | ||
| '/api/$': { | ||
| id: '/api/$' | ||
| path: '/api/$' | ||
| fullPath: '/api/$' | ||
| preLoaderRoute: typeof ApiSplatRouteImport | ||
| parentRoute: typeof rootRouteImport | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const rootRouteChildren: RootRouteChildren = { | ||
| IndexRoute: IndexRoute, | ||
| ApiSplatRoute: ApiSplatRoute, | ||
| } | ||
| export const routeTree = rootRouteImport | ||
| ._addFileChildren(rootRouteChildren) | ||
| ._addFileTypes<FileRouteTypes>() | ||
|
|
||
| import type { getRouter } from './router.tsx' | ||
| import type { createStart } from '@tanstack/react-start' | ||
| declare module '@tanstack/react-start' { | ||
| interface Register { | ||
| ssr: true | ||
| router: Awaited<ReturnType<typeof getRouter>> | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| import { createRouter as createTanStackRouter } from '@tanstack/react-router' | ||
| import { routeTree } from './routeTree.gen' | ||
|
|
||
| export function getRouter() { | ||
| const router = createTanStackRouter({ | ||
| routeTree, | ||
| scrollRestoration: true, | ||
| defaultPreload: 'intent', | ||
| defaultPreloadStaleTime: 0, | ||
| }) | ||
|
|
||
| return router | ||
| } | ||
|
|
||
| declare module '@tanstack/react-router' { | ||
| interface Register { | ||
| router: ReturnType<typeof getRouter> | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The development server listens on port 3001, while
apps/web/.env.exampleconfigures bothNEXT_PUBLIC_API_URLandNEXT_PUBLIC_WEB_URLashttp://localhost:3000. When developers use the supplied environment and run this script,fetcherServerand browser-side core fetchers therefore call port 3000 rather than the Hono API mounted in this app; with the existing docs app running there this silently exercises the wrong API, and without it session/API calls fail. Make the configured origin and listening port agree.Useful? React with 👍 / 👎.