diff --git a/.env.example b/.env.example index 1d34646f3..8cd02f224 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,7 @@ POSTGRES_URL=postgresql://root:root@localhost:5432/vitnode -NEXT_PUBLIC_BACKEND_URL=http://localhost:3000 -NEXT_PUBLIC_BACKEND_CLIENT_URL=http://localhost:3000 -NEXT_PUBLIC_FRONTEND_URL=http://localhost:3000 +NEXT_PUBLIC_API_URL=http://localhost:3000 +NEXT_PUBLIC_WEB_URL=http://localhost:3000 # === Docker Database Postgres === POSTGRES_USER=root diff --git a/apps/api/drizzle.config.ts b/apps/api/drizzle.config.ts new file mode 100644 index 000000000..0d630172b --- /dev/null +++ b/apps/api/drizzle.config.ts @@ -0,0 +1,12 @@ +import { defineVitNodeDrizzleConfig } from '@vitnode/core/drizzle.config'; + +import { POSTGRES_URL, vitNodeApiConfig } from './src/vitnode.api.config'; + +export default defineVitNodeDrizzleConfig({ + vitNodeApiConfig, + out: './migrations/', + dialect: 'postgresql', + dbCredentials: { + url: POSTGRES_URL, + }, +}); diff --git a/apps/api/eslint.config.mjs b/apps/api/eslint.config.mjs new file mode 100644 index 000000000..dd3efc12a --- /dev/null +++ b/apps/api/eslint.config.mjs @@ -0,0 +1,8 @@ +import eslintVitNode from '@vitnode/eslint-config/eslint'; + +export default [ + ...eslintVitNode, + { + ignores: ['drizzle.config.ts'], + }, +]; diff --git a/apps/api/package.json b/apps/api/package.json new file mode 100644 index 000000000..efcd03033 --- /dev/null +++ b/apps/api/package.json @@ -0,0 +1,42 @@ +{ + "name": "api", + "version": "1.2.0-canary.31", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc && tsc-alias -p tsconfig.json", + "start": "node dist/index.js", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "drizzle-kit": "drizzle-kit" + }, + "dependencies": { + "@hono/zod-openapi": "^0.19.8", + "@hono/zod-validator": "^0.7.0", + "@react-email/components": "^0.1.1", + "@vitnode/core": "workspace:*", + "drizzle-kit": "^0.31.3", + "drizzle-orm": "^0.44.2", + "hono": "^4.8.3", + "next-intl": "^4.3.1", + "react": "^19.1", + "react-dom": "^19.1", + "zod": "^3.25.67" + }, + "devDependencies": { + "@hono/node-server": "^1.15.0", + "@types/node": "^24", + "@types/react": "^19.1", + "@types/react-dom": "^19.1", + "@vitnode/eslint-config": "workspace:*", + "dotenv": "^17.1.0", + "eslint": "^9.29.0", + "prettier": "^3.6.1", + "prettier-plugin-tailwindcss": "^0.6.12", + "react-email": "^4.0.17", + "tsc-alias": "^1.8.16", + "tsx": "^4.20.3", + "typescript": "^5.8.3" + } +} diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts new file mode 100644 index 000000000..98369de4f --- /dev/null +++ b/apps/api/src/index.ts @@ -0,0 +1,29 @@ +import { serve } from '@hono/node-server'; +import { OpenAPIHono } from '@hono/zod-openapi'; +import { VitNodeAPI } from '@vitnode/core/api/config'; + +import { vitNodeApiConfig } from './vitnode.api.config.js'; +import { vitNodeConfig } from './vitnode.config.js'; + +const app = new OpenAPIHono().basePath('/api'); + +VitNodeAPI({ + app, + vitNodeApiConfig, + vitNodeConfig, +}); + +serve( + { + fetch: app.fetch, + port: 8080, + }, + info => { + const initMessage = '\x1b[34m[VitNode]\x1b[0m'; + + // eslint-disable-next-line no-console + console.log( + `${initMessage} API server is running on http://localhost:${info.port}`, + ); + }, +); diff --git a/apps/api/src/vitnode.api.config.ts b/apps/api/src/vitnode.api.config.ts new file mode 100644 index 000000000..5111961a1 --- /dev/null +++ b/apps/api/src/vitnode.api.config.ts @@ -0,0 +1,17 @@ +import { buildApiConfig } from '@vitnode/core/vitnode.config'; +import * as dotenv from 'dotenv'; +import { drizzle } from 'drizzle-orm/postgres-js'; + +dotenv.config(); + +export const POSTGRES_URL = + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + process.env.POSTGRES_URL || 'postgresql://root:root@localhost:5432/vitnode'; + +export const vitNodeApiConfig = buildApiConfig({ + plugins: [], + dbProvider: drizzle({ + connection: POSTGRES_URL, + casing: 'camelCase', + }), +}); diff --git a/apps/api/src/vitnode.config.ts b/apps/api/src/vitnode.config.ts new file mode 100644 index 000000000..4f20e8a11 --- /dev/null +++ b/apps/api/src/vitnode.config.ts @@ -0,0 +1,32 @@ +import { buildConfig, handleRequestConfig } from '@vitnode/core/vitnode.config'; +import { getRequestConfig } from 'next-intl/server'; + +export const vitNodeConfig = buildConfig({ + metadata: { + title: 'VitNode', + shortTitle: 'VitNode', + }, + plugins: [], + i18n: { + locales: [ + { + code: 'en', + name: 'English', + }, + ], + defaultLocale: 'en', + }, + theme: { + defaultTheme: 'light', + }, +}); + +// This is the request config for the app. It will be used in the app router. +export default getRequestConfig( + async ({ requestLocale }) => + await handleRequestConfig({ + requestLocale, + vitNodeConfig, + pathToMessages: async path => await import(`./locales/${path}`), + }), +); diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json new file mode 100644 index 000000000..9498a3d91 --- /dev/null +++ b/apps/api/tsconfig.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vitnode/eslint-config/tsconfig", + "compilerOptions": { + "target": "ESNext", + "module": "NodeNext", + "baseUrl": ".", + "outDir": "./dist", + "types": ["node"], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/apps/docs/.gitignore b/apps/docs/.gitignore index 55a12ae71..c3a96f073 100644 --- a/apps/docs/.gitignore +++ b/apps/docs/.gitignore @@ -1,5 +1,6 @@ # deps /node_modules +.turbo # generated content .contentlayer diff --git a/apps/docs/.prettierrc.mjs b/apps/docs/.prettierrc.mjs deleted file mode 100644 index 92fe83d4d..000000000 --- a/apps/docs/.prettierrc.mjs +++ /dev/null @@ -1,11 +0,0 @@ -import vitnodePrettier from '@vitnode/eslint-config/prettierrc'; - -/** - * @see https://prettier.io/docs/en/configuration.html - * @type {import("prettier").Config} - */ -const config = { - ...vitnodePrettier, -}; - -export default config; diff --git a/apps/docs/content/docs/dev/deployments/cloud/vercel.mdx b/apps/docs/content/docs/dev/deployments/cloud/vercel.mdx index 750605896..a2015a5ea 100644 --- a/apps/docs/content/docs/dev/deployments/cloud/vercel.mdx +++ b/apps/docs/content/docs/dev/deployments/cloud/vercel.mdx @@ -7,6 +7,13 @@ description: How to deploy your Vitnode app on Vercel. We're working hard to bring you the best documentation experience. +## Limitations + +Some features of Vitnode are not supported on Vercel due to its serverless architecture. These include: + +- Self-hosted static files +- WebSockets + ## Requirements - A Vercel account diff --git a/apps/docs/package.json b/apps/docs/package.json index 7b7caa85e..c90a6aa74 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -35,29 +35,28 @@ "motion": "^12.23.0", "next": "^15.3.5", "next-intl": "^4.3.4", - "react": "^19.1.0", - "react-dom": "^19.1.0", + "react": "^19.1", + "react-dom": "^19.1", "react-hook-form": "^7.60.0", "react-use": "^17.6.0", - "sonner": "^2.0.6", - "zod": "^3.25.74" + "sonner": "^2.0.6" }, "devDependencies": { "@playwright/test": "^1.53.2", "@tailwindcss/postcss": "^4.1.11", "@types/mdx": "^2.0.13", "@types/node": "^24.0.10", - "@types/react": "^19.1.8", - "@types/react-dom": "^19.1.6", + "@types/react": "^19.1", + "@types/react-dom": "^19.1", "@vitnode/eslint-config": "workspace:*", "class-variance-authority": "^0.7.1", - "dotenv": "^17.0.1", "eslint": "^9.30.1", "postcss": "^8.5.6", "react-email": "^4.0.17", "shiki": "^3.7.0", "tailwindcss": "^4.1.11", "tw-animate-css": "^1.3.5", - "typescript": "^5.8.3" + "typescript": "^5.8.3", + "zod": "^3.25.74" } } diff --git a/apps/docs/src/app/[locale]/(docs)/docs/[[...slug]]/page.tsx b/apps/docs/src/app/[locale]/(docs)/docs/[[...slug]]/page.tsx index 5c62704cf..627049e0e 100644 --- a/apps/docs/src/app/[locale]/(docs)/docs/[[...slug]]/page.tsx +++ b/apps/docs/src/app/[locale]/(docs)/docs/[[...slug]]/page.tsx @@ -36,7 +36,7 @@ export default async function Page(props: {

{page.data.description}

-
+
🎉{` `} VitNode 2.0 in progress... {/* */} -

+

Extendable Framework for Building Apps

-

+

Simplifies development with a powerful Plugin System, Admin Control Panel and extensible architecture.

diff --git a/apps/docs/src/app/[locale]/(main)/(home)/sections/admin/admin.tsx b/apps/docs/src/app/[locale]/(main)/(home)/sections/admin/admin.tsx index 34eb36d6b..9dfc71db9 100644 --- a/apps/docs/src/app/[locale]/(main)/(home)/sections/admin/admin.tsx +++ b/apps/docs/src/app/[locale]/(main)/(home)/sections/admin/admin.tsx @@ -13,8 +13,8 @@ export const AdminSection = () => {
-
-
+
+
payments illustration dark { return (
-

+

Start Building

-

+

Everything you need for modern web apps, zero config.

diff --git a/apps/docs/src/app/[locale]/(main)/(home)/sections/powering-by/logos/tailwindcss.tsx b/apps/docs/src/app/[locale]/(main)/(home)/sections/powering-by/logos/tailwindcss.tsx index 82cd065f9..0dee1a73b 100644 --- a/apps/docs/src/app/[locale]/(main)/(home)/sections/powering-by/logos/tailwindcss.tsx +++ b/apps/docs/src/app/[locale]/(main)/(home)/sections/powering-by/logos/tailwindcss.tsx @@ -2,7 +2,7 @@ export const TailwindCSSLogo = () => { return ( { -
-
+
+
diff --git a/apps/docs/src/examples/separator.tsx b/apps/docs/src/examples/separator.tsx index de660bcde..10dbf6dce 100644 --- a/apps/docs/src/examples/separator.tsx +++ b/apps/docs/src/examples/separator.tsx @@ -4,7 +4,7 @@ export default function ProgressDemo() { return (
-

Radix Primitives

+

Radix Primitives

An open-source UI component library.

diff --git a/apps/docs/src/vitnode.api.config.ts b/apps/docs/src/vitnode.api.config.ts index 45f3d6fd5..ed34849c9 100644 --- a/apps/docs/src/vitnode.api.config.ts +++ b/apps/docs/src/vitnode.api.config.ts @@ -4,13 +4,7 @@ import { DiscordSSOApiPlugin } from '@vitnode/core/api/adapters/sso/discord'; import { FacebookSSOApiPlugin } from '@vitnode/core/api/adapters/sso/facebook'; import { GoogleSSOApiPlugin } from '@vitnode/core/api/adapters/sso/google'; import { buildApiConfig } from '@vitnode/core/vitnode.config'; -import * as dotenv from 'dotenv'; import { drizzle } from 'drizzle-orm/postgres-js'; -import { join } from 'path'; - -dotenv.config({ - path: join(process.cwd(), '..', '..', '.env'), -}); export const POSTGRES_URL = // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing diff --git a/package.json b/package.json index 8447b5c19..b836265b6 100644 --- a/package.json +++ b/package.json @@ -17,11 +17,10 @@ "test:e2e": "turbo test:e2e" }, "devDependencies": { - "@types/node": "^24.0.10", + "@types/node": "^24", "@vitnode/eslint-config": "workspace:*", "prettier": "^3.6.2", "prettier-plugin-tailwindcss": "^0.6.13", - "tsx": "^4.20.3", "turbo": "^2.5.4", "typescript": "^5.8.3", "zod": "^3.25.74" diff --git a/packages/create-vitnode-app/README.md b/packages/create-vitnode-app/README.md index f80887a89..91a9fcb0c 100644 --- a/packages/create-vitnode-app/README.md +++ b/packages/create-vitnode-app/README.md @@ -37,8 +37,9 @@ bun create vitnode-app@latest ## Options -| Option | Description | -| ------------------- | ---------------------------------------------------------- | -| `--package-manager` | Specify the package manager to use. Support `npm`, `pnpm`. | -| `--eslint` | Initialize with eslint config. | -| `--skip-install` | Skip installing packages after initializing the project. | +| Option | Description | +| ------------------- | --------------------------------------------------------------------------------- | +| `--package-manager` | Specify the package manager to use. Support `npm`, `pnpm`. | +| `--eslint` | Initialize with eslint config. | +| `--skip-install` | Skip installing packages after initializing the project. | +| `--mode` | Specify the type of app to create. Support `singleApp`, `apiMonorepo`, `onlyApi`. | diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/root/.env.example b/packages/create-vitnode-app/copy-of-vitnode-app/root/.env.example index 1d34646f3..8cd02f224 100644 --- a/packages/create-vitnode-app/copy-of-vitnode-app/root/.env.example +++ b/packages/create-vitnode-app/copy-of-vitnode-app/root/.env.example @@ -1,8 +1,7 @@ POSTGRES_URL=postgresql://root:root@localhost:5432/vitnode -NEXT_PUBLIC_BACKEND_URL=http://localhost:3000 -NEXT_PUBLIC_BACKEND_CLIENT_URL=http://localhost:3000 -NEXT_PUBLIC_FRONTEND_URL=http://localhost:3000 +NEXT_PUBLIC_API_URL=http://localhost:3000 +NEXT_PUBLIC_WEB_URL=http://localhost:3000 # === Docker Database Postgres === POSTGRES_USER=root diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/root/tsconfig.json b/packages/create-vitnode-app/copy-of-vitnode-app/root/tsconfig.json new file mode 100644 index 000000000..13dc93961 --- /dev/null +++ b/packages/create-vitnode-app/copy-of-vitnode-app/root/tsconfig.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vitnode/eslint-config/tsconfig", + "compilerOptions": { + "target": "ESNext", + "module": "esnext", + "moduleResolution": "bundler", + "noEmit": true, + "baseUrl": ".", + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/packages/create-vitnode-app/src/create/create-package-json.ts b/packages/create-vitnode-app/src/create/create-package-json.ts index 588eac634..51e5c7946 100644 --- a/packages/create-vitnode-app/src/create/create-package-json.ts +++ b/packages/create-vitnode-app/src/create/create-package-json.ts @@ -3,6 +3,7 @@ import { dirname, join } from 'path'; import { fileURLToPath } from 'url'; import type { PackageJSON } from '../helpers/packages-json.js'; +import type { CreateCliReturn } from '../questions.js'; import { getAvailablePackageManagers } from '../helpers/get-available-package-managers.js'; @@ -15,10 +16,12 @@ export const createPackageJSON = async ({ root, eslint, docker, + mode, }: { appName: string; docker?: boolean; eslint: boolean; + mode: CreateCliReturn['mode']; packageManager: string; root: string; }) => { @@ -27,24 +30,22 @@ export const createPackageJSON = async ({ await readFile(join(__dirname, '..', '..', '..', 'package.json'), 'utf-8'), ); - const packageJson: PackageJSON = { - name: appName, + const apiPackageJson: PackageJSON = { + name: mode === 'apiMonorepo' ? 'api' : appName, version: '0.1.0', private: true, type: 'module', scripts: { - 'db:push': 'vitnode push', - 'db:migrate': 'vitnode migrate', - dev: 'vitnode init && next dev --turbopack', - build: 'next build --turbopack', - start: 'next start', + dev: 'tsx watch src/index.ts', + build: 'tsc && tsc-alias -p tsconfig.json', + start: 'node dist/index.js', ...(eslint ? { lint: 'eslint .', 'lint:fix': 'eslint . --fix', } : {}), - ...(docker + ...(docker && mode === 'onlyApi' ? { 'docker:dev': `docker compose -f ./docker-compose.yml -p ${appName}-vitnode-dev up -d`, } @@ -52,47 +53,207 @@ export const createPackageJSON = async ({ 'drizzle-kit': 'drizzle-kit', }, dependencies: { - '@hono/zod-openapi': '^0.19.9', + '@hono/zod-openapi': '^0.19.8', '@hono/zod-validator': '^0.7.0', - '@hookform/resolvers': '^5.1.1', '@react-email/components': '^0.1.1', '@vitnode/core': `^${pkg.version}`, - 'babel-plugin-react-compiler': '19.1.0-rc.2', - 'drizzle-kit': '^0.31.4', + 'drizzle-kit': '^0.31.3', 'drizzle-orm': '^0.44.2', - hono: '^4.8.4', - 'lucide-react': '^0.525.0', - next: '^15.3.5', - 'next-intl': '^4.3.4', - react: '^19.1.0', - 'react-dom': '^19.1.0', - 'react-hook-form': '^7.60.0', - sonner: '^2.0.6', - zod: '^3.25.74', + hono: '^4.8.3', + 'next-intl': '^4.3.1', + react: '^19.1', + 'react-dom': '^19.1', + zod: '^3.25.67', }, devDependencies: { - '@tailwindcss/postcss': '^4.1.11', + '@hono/node-server': '^1.15.0', '@types/node': '^24', '@types/react': '^19.1', '@types/react-dom': '^19.1', + '@vitnode/eslint-config': `^${pkg.version}`, + dotenv: '^17.1.0', ...(eslint ? { eslint: '^9.30.1', - '@vitnode/eslint-config': `^${pkg.version}`, - 'prettier-plugin-tailwindcss': '^0.6.12', - prettier: '^3.6.2', + ...(mode === 'onlyApi' + ? { + 'prettier-plugin-tailwindcss': '^0.6.12', + prettier: '^3.6.2', + } + : {}), } : {}), 'react-email': '^4.0.17', - tailwindcss: '^4.1.11', - 'tw-animate-css': '^1.3.5', + 'tsc-alias': '^1.8.16', + tsx: '^4.20.3', typescript: '^5.8.3', }, - packageManager: `${packageManager}@${availablePackageManagers[packageManager]}`, }; - await writeFile( - join(root, 'package.json'), - JSON.stringify(packageJson, null, 2), - ); + if (mode === 'singleApp') { + const packageJson: PackageJSON = { + name: appName, + version: '0.1.0', + private: true, + type: 'module', + scripts: { + 'db:push': 'vitnode push', + 'db:migrate': 'vitnode migrate', + dev: 'vitnode init && next dev --turbopack', + build: 'next build --turbopack', + start: 'next start', + ...(eslint + ? { + lint: 'eslint .', + 'lint:fix': 'eslint . --fix', + } + : {}), + ...(docker + ? { + 'docker:dev': `docker compose -f ./docker-compose.yml -p ${appName}-vitnode-dev up -d`, + } + : {}), + 'drizzle-kit': 'drizzle-kit', + }, + dependencies: { + '@hono/zod-openapi': '^0.19.9', + '@hono/zod-validator': '^0.7.0', + '@hookform/resolvers': '^5.1.1', + '@react-email/components': '^0.1.1', + '@vitnode/core': `^${pkg.version}`, + 'babel-plugin-react-compiler': '19.1.0-rc.2', + 'drizzle-kit': '^0.31.4', + 'drizzle-orm': '^0.44.2', + hono: '^4.8.4', + 'lucide-react': '^0.525.0', + next: '^15.3.5', + 'next-intl': '^4.3.4', + react: '^19.1', + 'react-dom': '^19.1', + 'react-hook-form': '^7.60.0', + sonner: '^2.0.6', + zod: '^3.25.74', + }, + devDependencies: { + '@tailwindcss/postcss': '^4.1.11', + '@types/node': '^24', + '@types/react': '^19.1', + '@types/react-dom': '^19.1', + '@vitnode/eslint-config': `^${pkg.version}`, + ...(eslint + ? { + eslint: '^9.30.1', + 'prettier-plugin-tailwindcss': '^0.6.12', + prettier: '^3.6.2', + } + : {}), + 'react-email': '^4.0.17', + turbo: '^2.5.4', + tailwindcss: '^4.1.11', + 'tw-animate-css': '^1.3.5', + typescript: '^5.8.3', + }, + packageManager: `${packageManager}@${availablePackageManagers[packageManager]}`, + }; + + await writeFile( + join(root, 'package.json'), + JSON.stringify(packageJson, null, 2), + ); + } else if (mode === 'apiMonorepo') { + const rootPackageJson: PackageJSON = { + name: appName, + private: true, + scripts: { + 'db:migrate': 'turbo db:migrate', + 'db:push': 'turbo db:push', + build: 'turbo build', + start: 'turbo start', + dev: ' turbo dev', + lint: 'turbo lint', + 'lint:fix': 'turbo lint:fix', + }, + devDependencies: { + '@types/node': '^24', + '@vitnode/eslint-config': `^${pkg.version}`, + ...(eslint + ? { + 'prettier-plugin-tailwindcss': '^0.6.12', + prettier: '^3.6.2', + } + : {}), + turbo: '^2.5.4', + typescript: '^5.8.3', + zod: '^3.25.74', + }, + packageManager: `${packageManager}@${availablePackageManagers[packageManager]}`, + workspaces: ['apps/*', 'plugins/*'], + }; + + await writeFile( + join(root, 'package.json'), + JSON.stringify(rootPackageJson, null, 2), + ); + + await writeFile( + join(root, 'apps', 'api', 'package.json'), + JSON.stringify(apiPackageJson, null, 2), + ); + + const webPackageJson: PackageJSON = { + name: 'web', + version: '0.1.0', + private: true, + type: 'module', + scripts: { + dev: 'vitnode init && next dev --turbopack', + build: 'next build --turbopack', + start: 'next start', + lint: 'eslint .', + 'lint:fix': 'eslint . --fix', + }, + dependencies: { + '@vitnode/core': `^${pkg.version}`, + 'babel-plugin-react-compiler': '19.1.0-rc.2', + 'lucide-react': '^0.525.0', + next: '^15.3.5', + 'next-intl': '^4.3.4', + react: '^19.1', + 'react-dom': '^19.1', + 'react-hook-form': '^7.60.0', + sonner: '^2.0.6', + }, + devDependencies: { + '@playwright/test': '^1.53.2', + '@tailwindcss/postcss': '^4.1.11', + '@types/mdx': '^2.0.13', + '@types/node': '^24.0.10', + '@types/react': '^19.1', + '@types/react-dom': '^19.1', + '@vitnode/eslint-config': `^${pkg.version}`, + 'class-variance-authority': '^0.7.1', + ...(eslint + ? { + eslint: '^9.30.1', + } + : {}), + postcss: '^8.5.6', + 'react-email': '^4.0.17', + tailwindcss: '^4.1.11', + 'tw-animate-css': '^1.3.5', + typescript: '^5.8.3', + zod: '^3.25.74', + }, + }; + + await writeFile( + join(root, 'apps', 'web', 'package.json'), + JSON.stringify(webPackageJson, null, 2), + ); + } else if (mode === 'onlyApi') { + await writeFile( + join(root, 'package.json'), + JSON.stringify(apiPackageJson, null, 2), + ); + } }; diff --git a/packages/create-vitnode-app/src/create/create-vitnode.ts b/packages/create-vitnode-app/src/create/create-vitnode.ts index a25729d2f..a56972a7a 100644 --- a/packages/create-vitnode-app/src/create/create-vitnode.ts +++ b/packages/create-vitnode-app/src/create/create-vitnode.ts @@ -22,6 +22,7 @@ export const createVitNode = async ({ eslint, install, docker, + mode, }: CreateCliReturn & { appName: string; root: string; @@ -45,11 +46,44 @@ export const createVitNode = async ({ if (!isFolderEmpty(root, appName)) { process.exit(1); } + const monorepoStructure = { + api: join(root, 'apps', 'api'), + web: join(root, 'apps', 'web'), + }; + + if (mode === 'apiMonorepo') { + spinner.text = 'Preparing monorepo structure...'; + // Create api, web folders + await Promise.all([ + mkdir(monorepoStructure.api, { recursive: true }), + mkdir(monorepoStructure.web, { recursive: true }), + ]); + } spinner.text = 'Copying files...'; - await cp(join(templatePath, 'root'), root, { - recursive: true, - }); + if (mode === 'singleApp') { + await Promise.all([ + cp(join(templatePath, 'root'), root, { + recursive: true, + }), + cp(join(templatePath, 'api-single-app'), root, { + recursive: true, + }), + ]); + } else if (mode === 'apiMonorepo') { + await Promise.all([ + cp(join(templatePath, 'root'), monorepoStructure.web, { + recursive: true, + }), + cp(join(templatePath, 'api'), monorepoStructure.api, { + recursive: true, + }), + ]); + } else if (mode === 'onlyApi') { + await cp(join(templatePath, 'api'), root, { + recursive: true, + }); + } if (eslint) { spinner.text = 'Copying eslint files...'; @@ -58,7 +92,6 @@ export const createVitNode = async ({ }); } - // Rename special files spinner.text = 'Renaming special files...'; await rename(join(root, '.gitignore_template'), join(root, '.gitignore')); @@ -69,6 +102,7 @@ export const createVitNode = async ({ packageManager, eslint, docker, + mode, }); if (docker) { diff --git a/packages/create-vitnode-app/src/helpers/packages-json.ts b/packages/create-vitnode-app/src/helpers/packages-json.ts index 240b6954e..90febe98f 100644 --- a/packages/create-vitnode-app/src/helpers/packages-json.ts +++ b/packages/create-vitnode-app/src/helpers/packages-json.ts @@ -9,6 +9,6 @@ export interface PackageJSON { private: boolean; scripts?: Record; type?: string; - version: string; + version?: string; workspaces?: string[]; } diff --git a/packages/create-vitnode-app/src/index.ts b/packages/create-vitnode-app/src/index.ts index 15718af38..ed2b53853 100644 --- a/packages/create-vitnode-app/src/index.ts +++ b/packages/create-vitnode-app/src/index.ts @@ -36,7 +36,7 @@ const init = async () => { let projectPath = ''; const program = new Command() - .version(packageJson.version) + .version(packageJson.version ?? '0.1.0') .argument('[project-directory]') .usage(`${color.green('[project-directory]')} [options]`) .action(name => { @@ -55,6 +55,12 @@ const init = async () => { 'Skip installing packages after initializing the project.', ); program.option('--plugin', 'Enable plugin mode.'); + program.addOption( + new Option( + '--mode ', + 'What type of app do you want to create?', + ).choices(['singleApp', 'apiMonorepo', 'onlyApi']), + ); program.parse(process.argv); diff --git a/packages/create-vitnode-app/src/questions.ts b/packages/create-vitnode-app/src/questions.ts index f15155aad..1233ccb43 100644 --- a/packages/create-vitnode-app/src/questions.ts +++ b/packages/create-vitnode-app/src/questions.ts @@ -10,6 +10,7 @@ export interface CreateCliReturn { docker?: boolean; eslint: boolean; install: boolean; + mode: 'apiMonorepo' | 'onlyApi' | 'singleApp'; packageManager: string; } @@ -22,6 +23,7 @@ export const createQuestionsCli = async ( eslint: optionsFromProgram.eslint, install: !optionsFromProgram.skipInstall, docker: optionsFromProgram.docker, + mode: optionsFromProgram.mode, }; if (!optionsFromProgram.packageManager) { @@ -48,6 +50,32 @@ export const createQuestionsCli = async ( }); } + if (optionsFromProgram.mode === undefined) { + options.mode = await select({ + message: `What type of ${color.blue('app')} do you want to create?`, + choices: [ + { + name: `Single App - ${color.blue('Next.js')} & ${color.blue('Hono.js')}`, + description: + 'Create a single app with Next.js and Hono.js in the same project.', + value: 'singleApp', + }, + { + name: `Monorepo App - ${color.blue('Next.js')} & ${color.blue('Hono.js')}`, + description: + 'Create a monorepo with both Next.js and Hono.js apps separately.', + value: 'apiMonorepo', + }, + { + name: `Only API - ${color.blue('Hono.js')}`, + description: 'Create only an API app using Hono.js without Next.js.', + value: 'onlyApi', + }, + ], + default: 'singleApp', + }); + } + if (optionsFromProgram.eslint === undefined) { options.eslint = await confirm({ message: `Would you like to use ${color.blue('ESLint')}?`, diff --git a/packages/vitnode/package.json b/packages/vitnode/package.json index df1b3c162..bf01425b1 100644 --- a/packages/vitnode/package.json +++ b/packages/vitnode/package.json @@ -46,13 +46,14 @@ "@testing-library/react": "^16.3.0", "@types/node": "^24.0.10", "@types/nodemailer": "^6.4.17", - "@types/react": "^19.1.8", - "@types/react-dom": "^19.1.6", + "@types/react": "^19.1", + "@types/react-dom": "^19.1", "@vitejs/plugin-react": "^4.6.0", "@vitest/coverage-v8": "^3.2.4", "@vitnode/eslint-config": "workspace:*", "chokidar": "^4.0.3", "concurrently": "^9.2.0", + "dotenv": "^17.1.0", "drizzle-kit": "^0.31.4", "drizzle-orm": "^0.44.2", "eslint": "^9.30.1", @@ -61,8 +62,8 @@ "lucide-react": "^0.525.0", "next": "^15.3.5", "next-intl": "^4.3.4", - "react": "^19.1.0", - "react-dom": "^19.1.0", + "react": "^19.1", + "react-dom": "^19.1", "react-email": "^4.0.17", "react-hook-form": "^7.60.0", "sonner": "^2.0.6", @@ -113,7 +114,6 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", - "dotenv": "^17.0.1", "input-otp": "^1.4.2", "next-themes": "^0.4.6", "nodemailer": "^7.0.4", diff --git a/packages/vitnode/scripts/get-config.ts b/packages/vitnode/scripts/get-config.ts index 79c9c1257..033045cc7 100644 --- a/packages/vitnode/scripts/get-config.ts +++ b/packages/vitnode/scripts/get-config.ts @@ -2,16 +2,25 @@ import { join } from 'path'; import { pathToFileURL } from 'url'; -import type { VitNodeConfig } from '../src/vitnode.config'; +import type { VitNodeApiConfig, VitNodeConfig } from '../src/vitnode.config'; -export const getConfig = async (): Promise => { - const configPath = join(process.cwd(), 'src', 'vitnode.config.ts'); +type ConfigType = T extends 'config' + ? VitNodeConfig + : VitNodeApiConfig; + +export const getConfig = async ({ + type = 'config' as T, +}: { + type?: T; +}): Promise> => { + const configPath = join(process.cwd(), 'src', `vitnode.${type}.ts`); try { const configUrl = pathToFileURL(configPath).href; const loaded = await import(configUrl); - const config = loaded.vitNodeConfig; + const config = + type === 'config' ? loaded.vitNodeConfig : loaded.vitNodeApiConfig; - return config; + return config as ConfigType; } catch (error) { console.error('Failed to load config:', error); process.exit(1); diff --git a/packages/vitnode/scripts/prepare-database.ts b/packages/vitnode/scripts/prepare-database.ts index 0adbe6c48..0c402af77 100644 --- a/packages/vitnode/scripts/prepare-database.ts +++ b/packages/vitnode/scripts/prepare-database.ts @@ -1,28 +1,16 @@ /* eslint-disable no-console */ -import * as dotenv from 'dotenv'; import { count } from 'drizzle-orm'; -import { drizzle } from 'drizzle-orm/postgres-js'; -import { join } from 'path'; import { core_admin_permissions } from '@/database/admins.js'; import { core_languages, core_languages_words } from '@/database/languages.js'; import { core_moderators_permissions } from '@/database/moderators.js'; import { core_roles } from '@/database/roles.js'; +import { getConfig } from './get-config.js'; import { preparePluginsFiles } from './prepare-plugins-files.js'; import { runInteractiveShellCommand } from './run-interactive-shell-command.js'; -dotenv.config({ - path: join(process.cwd(), '..', '..', '.env'), -}); - -const dbClient = drizzle({ - connection: - process.env.POSTGRES_URL ?? 'postgresql://root:root@localhost:5432/vitnode', - casing: 'camelCase', -}); - export const generateDatabaseMigrations = async () => { try { await runInteractiveShellCommand('npm', ['run', 'drizzle-kit', 'up']); @@ -52,6 +40,9 @@ export const runPush = async () => { }; export const initialDataForDatabase = async () => { + const config = await getConfig({ type: 'api.config' }); + const dbClient = config.dbProvider; + const [roleCount] = await dbClient .select({ count: count(), @@ -164,10 +155,10 @@ export const prepareDatabase = async ({ }) => { console.log(`${initMessage} [1/4] Prepare plugins files...`); await preparePluginsFiles(); - console.log(`${initMessage} [2/4] Generate migrations...`); - await generateDatabaseMigrations(); - console.log(`${initMessage} [3/4] Run migrations...`); - await runMigrations(); + // console.log(`${initMessage} [2/4] Generate migrations...`); + // await generateDatabaseMigrations(); + // console.log(`${initMessage} [3/4] Run migrations...`); + // await runMigrations(); console.log(`\n${initMessage} [4/4] Insert initial data...`); await initialDataForDatabase(); console.log(`${initMessage} \x1b[32mDatabase prepared successfully.\x1b[0m`); diff --git a/packages/vitnode/scripts/prepare-plugins-files.ts b/packages/vitnode/scripts/prepare-plugins-files.ts index d3e514f46..a4945717c 100644 --- a/packages/vitnode/scripts/prepare-plugins-files.ts +++ b/packages/vitnode/scripts/prepare-plugins-files.ts @@ -14,7 +14,7 @@ import { } from './shared/file-utils'; export const preparePluginsFiles = async () => { - const config = await getConfig(); + const config = await getConfig({}); const plugins: string[] = [ ...config.plugins.map(plugin => plugin.pluginId), '@vitnode/core', diff --git a/packages/vitnode/scripts/run-interactive-shell-command.ts b/packages/vitnode/scripts/run-interactive-shell-command.ts index f42ba7aba..1fcd7ae12 100644 --- a/packages/vitnode/scripts/run-interactive-shell-command.ts +++ b/packages/vitnode/scripts/run-interactive-shell-command.ts @@ -5,7 +5,11 @@ export const runInteractiveShellCommand = async ( args: string[] = [], ) => { return new Promise((resolve, reject) => { - const child = spawn(cmd, args, { stdio: 'inherit', shell: true }); + const child = spawn(cmd, args, { + stdio: 'inherit', + shell: true, + env: process.env, + }); child.on('error', error => { reject(error); diff --git a/packages/vitnode/scripts/scripts.ts b/packages/vitnode/scripts/scripts.ts index 898daa265..c32681cd9 100644 --- a/packages/vitnode/scripts/scripts.ts +++ b/packages/vitnode/scripts/scripts.ts @@ -1,6 +1,8 @@ #!/usr/bin/env node /* eslint-disable no-console */ +import * as dotenv from 'dotenv'; + import { processPlugin } from './plugin.js'; import { generateDatabaseMigrations, @@ -11,6 +13,8 @@ import { } from './prepare-database.js'; import { preparePluginsFiles } from './prepare-plugins-files.js'; +dotenv.config(); + const initMessage = '\x1b[34m[VitNode]\x1b[0m'; const command = process.argv[2]; diff --git a/packages/vitnode/src/api/middlewares/rate-limiter.middleware.ts b/packages/vitnode/src/api/middlewares/rate-limiter.middleware.ts index 6d45cea50..3b3f1852e 100644 --- a/packages/vitnode/src/api/middlewares/rate-limiter.middleware.ts +++ b/packages/vitnode/src/api/middlewares/rate-limiter.middleware.ts @@ -18,7 +18,7 @@ const createRateLimiter = ({ return new RateLimiterMemory({ keyPrefix, - points: CONFIG.node_development ? 120 : (options?.points ?? 80), // 120 req in dev, 80 in prod + points: options?.points ?? 80, // 80 in prod duration: options?.duration ?? 60, // per 60 seconds ...options, }); @@ -27,6 +27,13 @@ const createRateLimiter = ({ export const rateLimiterMiddleware = ( options?: Omit, ) => { + if (CONFIG.node_development) { + // In development, we disable the rate limiter for easier testing + return async (c: Context, next: Next) => { + await next(); + }; + } + const rateLimiter = createRateLimiter({ ...options, keyPrefix: 'vitnode-api-rate-limiter', @@ -37,10 +44,10 @@ export const rateLimiterMiddleware = ( try { await rateLimiter.consume(key); - - await next(); } catch { return c.text('Too Many Requests', 429); } + + await next(); }; }; diff --git a/packages/vitnode/src/api/models/device.ts b/packages/vitnode/src/api/models/device.ts index 629239a01..178c514b5 100644 --- a/packages/vitnode/src/api/models/device.ts +++ b/packages/vitnode/src/api/models/device.ts @@ -44,7 +44,7 @@ export class DeviceModel { httpOnly: true, secure: this.c.get('core').authorization.cookieSecure, path: '/', - domain: CONFIG.frontend.hostname, + domain: CONFIG.web.hostname, expires: new Date( Date.now() + this.c.get('core').authorization.deviceCookieExpires, ), diff --git a/packages/vitnode/src/api/models/email.ts b/packages/vitnode/src/api/models/email.ts index ae6e3c5a5..534476216 100644 --- a/packages/vitnode/src/api/models/email.ts +++ b/packages/vitnode/src/api/models/email.ts @@ -48,7 +48,7 @@ export class EmailModel { children: content, metadata: { ...core.metadata, - url: CONFIG.frontend.href, + url: CONFIG.web.href, }, logo: core.email?.options?.logo, }); diff --git a/packages/vitnode/src/api/models/session-admin.ts b/packages/vitnode/src/api/models/session-admin.ts index 79269c038..acbb70aa9 100644 --- a/packages/vitnode/src/api/models/session-admin.ts +++ b/packages/vitnode/src/api/models/session-admin.ts @@ -78,7 +78,7 @@ export class SessionAdminModel { expires: new Date( Date.now() + this.c.get('core').authorization.adminCookieExpires, ), - domain: CONFIG.frontend.hostname, + domain: CONFIG.web.hostname, }); return { token }; @@ -100,7 +100,7 @@ export class SessionAdminModel { deleteCookie(this.c, this.c.get('core').authorization.adminCookieName, { path: '/', - domain: CONFIG.frontend.hostname, + domain: CONFIG.web.hostname, }); } @@ -132,7 +132,7 @@ export class SessionAdminModel { if (!session) { deleteCookie(this.c, this.c.get('core').authorization.adminCookieName, { path: '/', - domain: CONFIG.frontend.hostname, + domain: CONFIG.web.hostname, }); return null; diff --git a/packages/vitnode/src/api/models/session.ts b/packages/vitnode/src/api/models/session.ts index 3849fb4db..8bff250af 100644 --- a/packages/vitnode/src/api/models/session.ts +++ b/packages/vitnode/src/api/models/session.ts @@ -56,7 +56,7 @@ export class SessionModel { Date.now() + this.c.get('core').authorization.cookie_expires, ) : undefined, - domain: CONFIG.frontend.hostname, + domain: CONFIG.web.hostname, }); return { token }; diff --git a/packages/vitnode/src/api/models/sso.ts b/packages/vitnode/src/api/models/sso.ts index 40dac8e21..69fa191b7 100644 --- a/packages/vitnode/src/api/models/sso.ts +++ b/packages/vitnode/src/api/models/sso.ts @@ -25,7 +25,7 @@ export interface SSOApiPlugin { } export const getRedirectUri = (code: string) => - new URL(`${CONFIG.frontend.href}login/sso/${code}`).toString(); + new URL(`${CONFIG.web.href}login/sso/${code}`).toString(); export class SSOModel { constructor(c: Context) { @@ -160,7 +160,7 @@ export class SSOModel { httpOnly: true, secure: this.c.get('core').authorization.cookieSecure, path: '/', - domain: CONFIG.frontend.hostname, + domain: CONFIG.web.hostname, }, ); diff --git a/packages/vitnode/src/api/modules/users/routes/test.route.ts b/packages/vitnode/src/api/modules/users/routes/test.route.ts index 848b60926..21f595b0d 100644 --- a/packages/vitnode/src/api/modules/users/routes/test.route.ts +++ b/packages/vitnode/src/api/modules/users/routes/test.route.ts @@ -1,9 +1,7 @@ -import { render } from '@react-email/components'; import { z } from 'zod'; import { buildRoute } from '@/api/lib/route'; import { CONFIG_PLUGIN } from '@/config'; -import DefaultTemplate from '@/emails/default-template'; export const testRoute = buildRoute({ ...CONFIG_PLUGIN, diff --git a/packages/vitnode/src/emails/default-template.tsx b/packages/vitnode/src/emails/default-template.tsx index 289fa99e0..a4b69d546 100644 --- a/packages/vitnode/src/emails/default-template.tsx +++ b/packages/vitnode/src/emails/default-template.tsx @@ -14,19 +14,18 @@ import { } from '@react-email/components'; import { createTranslator } from 'next-intl'; -import type { EnvVariablesVitNode } from '../api/middlewares/global.middleware'; - import { CONFIG } from '../lib/config'; -type EmailFromMiddlewareType = NonNullable< - NonNullable['options'] ->; - interface DefaultTemplateEmailProps { children: React.ReactNode; head?: React.ReactNode; - logo?: EmailFromMiddlewareType['logo']; - metadata: EnvVariablesVitNode['core']['metadata'] & { + logo?: { + className?: string; + src: Blob | string; + }; + metadata: { + shortTitle?: string; + title: string; url: string; }; previewText?: string; @@ -39,7 +38,14 @@ export default function DefaultTemplateEmail({ logo, metadata, }: DefaultTemplateEmailProps) { - const intl = createTranslator({ messages: {}, locale: 'en' }); + const intl = createTranslator({ + messages: { + email: { + previewText: 'This is a preview text for the email template.', + }, + }, + locale: 'en', + }); return ( @@ -50,10 +56,9 @@ export default function DefaultTemplateEmail({ theme: { extend: { colors: { - background: '#edf2f8', + background: '#ffffff', primary: '#3160c0', foreground: '#0e1216', - card: '#ffffff', border: '#d9dfe4', }, }, @@ -76,9 +81,9 @@ export default function DefaultTemplateEmail({ )} -
+
- Join Us for an Exciting Event! + Join Us for an Exciting Event! - {intl('email.previewText')} Hello @@ -111,7 +116,7 @@ DefaultTemplateEmail.PreviewProps = { metadata: { title: 'VitNode - Email Template', shortTitle: 'VitNode', - url: CONFIG.frontend.href, + url: CONFIG.web.href, }, logo: { src: 'https://www.reactemailtemplate.com/_next/static/media/reactemailtemplate-logo.b3fb12d9.png', diff --git a/packages/vitnode/src/lib/config.ts b/packages/vitnode/src/lib/config.ts index c76739522..daa321aae 100644 --- a/packages/vitnode/src/lib/config.ts +++ b/packages/vitnode/src/lib/config.ts @@ -1,19 +1,14 @@ const ENVS = { - backend_url: process.env.NEXT_PUBLIC_BACKEND_URL, - backend_client_url: process.env.NEXT_PUBLIC_BACKEND_CLIENT_URL, - frontend_url: process.env.NEXT_PUBLIC_FRONTEND_URL, + api_url: process.env.NEXT_PUBLIC_API_URL, + web_url: process.env.NEXT_PUBLIC_WEB_URL, }; const urls = { - backend: new URL(ENVS.backend_url ?? 'http://localhost:3000'), - backend_client: new URL( - ENVS.backend_client_url ?? ENVS.backend_url ?? 'http://localhost:3000', - ), - frontend: new URL(ENVS.frontend_url ?? 'http://localhost:3000'), + api: new URL(ENVS.api_url ?? 'http://localhost:3000'), + web: new URL(ENVS.web_url ?? 'http://localhost:3000'), }; export const CONFIG = { node_development: process.env.NODE_ENV === 'development', - backend: urls.backend, - frontend: urls.frontend, + ...urls, }; diff --git a/packages/vitnode/src/lib/fetcher/core.ts b/packages/vitnode/src/lib/fetcher/core.ts index 505a73387..5e9de765c 100644 --- a/packages/vitnode/src/lib/fetcher/core.ts +++ b/packages/vitnode/src/lib/fetcher/core.ts @@ -92,8 +92,8 @@ export async function coreFetcher< // Construct the base URL const url = new URL( - `/api/${pluginId}${prefixPath}/${module}${formattedPath}`, - CONFIG.backend.origin, + `/api/${pluginId}${prefixPath}/${module}${formattedPath === '/' ? '' : formattedPath}`, + CONFIG.api.origin, ); // Add query parameters if they exist @@ -122,6 +122,13 @@ export async function coreFetcher< ...options, }); + if (!response.ok) { + const errorText = await response.text(); + throw new Error( + `${response.status} - ${url.toString()}\n${response.statusText ?? errorText}`, + ); + } + return response as InferResponseType< M, Routes, diff --git a/plugins/blog/package.json b/plugins/blog/package.json index 7e8ecc1fc..312a8e526 100644 --- a/plugins/blog/package.json +++ b/plugins/blog/package.json @@ -41,8 +41,8 @@ "lucide-react": "^0.525.0", "next": "^15.3.5", "next-intl": "^4.3.4", - "react": "^19.1.0", - "react-dom": "^19.1.0", + "react": "^19.1", + "react-dom": "^19.1", "react-hook-form": "^7.60.0", "sonner": "^2.0.6", "zod": "^3.25.74" @@ -50,8 +50,8 @@ "devDependencies": { "@swc/cli": "0.6.0", "@swc/core": "^1.12.9", - "@types/react": "^19.1.8", - "@types/react-dom": "^19.1.6", + "@types/react": "^19.1", + "@types/react-dom": "^19.1", "@vitnode/eslint-config": "workspace:*", "concurrently": "^9.2.0", "eslint": "^9.30.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d582abd5b..bdec88066 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,7 +9,7 @@ importers: .: devDependencies: '@types/node': - specifier: ^24.0.10 + specifier: ^24 version: 24.0.10 '@vitnode/eslint-config': specifier: workspace:* @@ -20,9 +20,6 @@ importers: prettier-plugin-tailwindcss: specifier: ^0.6.13 version: 0.6.13(prettier-plugin-astro@0.7.2)(prettier@3.6.2) - tsx: - specifier: ^4.20.3 - version: 4.20.3 turbo: specifier: ^2.5.4 version: 2.5.4 @@ -33,6 +30,82 @@ importers: specifier: ^3.25.74 version: 3.25.74 + apps/api: + dependencies: + '@hono/zod-openapi': + specifier: ^0.19.8 + version: 0.19.9(hono@4.8.4)(zod@3.25.74) + '@hono/zod-validator': + specifier: ^0.7.0 + version: 0.7.0(hono@4.8.4)(zod@3.25.74) + '@react-email/components': + specifier: ^0.1.1 + version: 0.1.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@vitnode/core': + specifier: workspace:* + version: link:../../packages/vitnode + drizzle-kit: + specifier: ^0.31.3 + version: 0.31.4 + drizzle-orm: + specifier: ^0.44.2 + version: 0.44.2(@neondatabase/serverless@0.10.4)(@types/pg@8.11.10)(gel@2.1.0)(pg@8.13.1)(postgres@3.4.7) + hono: + specifier: ^4.8.3 + version: 4.8.4 + next-intl: + specifier: ^4.3.1 + version: 4.3.4(next@15.3.5(@playwright/test@1.53.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(typescript@5.8.3) + react: + specifier: ^19.1 + version: 19.1.0 + react-dom: + specifier: ^19.1 + version: 19.1.0(react@19.1.0) + zod: + specifier: ^3.25.67 + version: 3.25.74 + devDependencies: + '@hono/node-server': + specifier: ^1.15.0 + version: 1.15.0(hono@4.8.4) + '@types/node': + specifier: ^24 + version: 24.0.10 + '@types/react': + specifier: ^19.1 + version: 19.1.8 + '@types/react-dom': + specifier: ^19.1 + version: 19.1.6(@types/react@19.1.8) + '@vitnode/eslint-config': + specifier: workspace:* + version: link:../../packages/eslint + dotenv: + specifier: ^17.1.0 + version: 17.1.0 + eslint: + specifier: ^9.29.0 + version: 9.30.1(jiti@2.4.2) + prettier: + specifier: ^3.6.1 + version: 3.6.2 + prettier-plugin-tailwindcss: + specifier: ^0.6.12 + version: 0.6.13(prettier-plugin-astro@0.7.2)(prettier@3.6.2) + react-email: + specifier: ^4.0.17 + version: 4.0.17(@playwright/test@1.53.2)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + tsc-alias: + specifier: ^1.8.16 + version: 1.8.16 + tsx: + specifier: ^4.20.3 + version: 4.20.3 + typescript: + specifier: ^5.8.3 + version: 5.8.3 + apps/docs: dependencies: '@hono/zod-openapi': @@ -79,12 +152,12 @@ importers: version: 15.3.5(@playwright/test@1.53.2)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) next-intl: specifier: ^4.3.4 - version: 4.3.4(next@15.3.5(@playwright/test@1.53.2)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(typescript@5.8.3) + version: 4.3.4(next@15.3.5(@playwright/test@1.53.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(typescript@5.8.3) react: - specifier: ^19.1.0 + specifier: ^19.1 version: 19.1.0 react-dom: - specifier: ^19.1.0 + specifier: ^19.1 version: 19.1.0(react@19.1.0) react-hook-form: specifier: ^7.60.0 @@ -95,9 +168,6 @@ importers: sonner: specifier: ^2.0.6 version: 2.0.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - zod: - specifier: ^3.25.74 - version: 3.25.74 devDependencies: '@playwright/test': specifier: ^1.53.2 @@ -112,10 +182,10 @@ importers: specifier: ^24.0.10 version: 24.0.10 '@types/react': - specifier: ^19.1.8 + specifier: ^19.1 version: 19.1.8 '@types/react-dom': - specifier: ^19.1.6 + specifier: ^19.1 version: 19.1.6(@types/react@19.1.8) '@vitnode/eslint-config': specifier: workspace:* @@ -123,9 +193,6 @@ importers: class-variance-authority: specifier: ^0.7.1 version: 0.7.1 - dotenv: - specifier: ^17.0.1 - version: 17.0.1 eslint: specifier: ^9.30.1 version: 9.30.1(jiti@2.4.2) @@ -147,6 +214,9 @@ importers: typescript: specifier: ^5.8.3 version: 5.8.3 + zod: + specifier: ^3.25.74 + version: 3.25.74 packages/create-vitnode-app: dependencies: @@ -248,9 +318,6 @@ importers: cmdk: specifier: ^1.1.1 version: 1.1.1(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - dotenv: - specifier: ^17.0.1 - version: 17.0.1 input-otp: specifier: ^1.4.2 version: 1.4.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -316,10 +383,10 @@ importers: specifier: ^6.4.17 version: 6.4.17 '@types/react': - specifier: ^19.1.8 + specifier: ^19.1 version: 19.1.8 '@types/react-dom': - specifier: ^19.1.6 + specifier: ^19.1 version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': specifier: ^4.6.0 @@ -336,6 +403,9 @@ importers: concurrently: specifier: ^9.2.0 version: 9.2.0 + dotenv: + specifier: ^17.1.0 + version: 17.1.0 drizzle-kit: specifier: ^0.31.4 version: 0.31.4 @@ -361,10 +431,10 @@ importers: specifier: ^4.3.4 version: 4.3.4(next@15.3.5(@babel/core@7.28.0)(@playwright/test@1.53.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(typescript@5.8.3) react: - specifier: ^19.1.0 + specifier: ^19.1 version: 19.1.0 react-dom: - specifier: ^19.1.0 + specifier: ^19.1 version: 19.1.0(react@19.1.0) react-email: specifier: ^4.0.17 @@ -428,12 +498,12 @@ importers: version: 15.3.5(@playwright/test@1.53.2)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) next-intl: specifier: ^4.3.4 - version: 4.3.4(next@15.3.5(@playwright/test@1.53.2)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(typescript@5.8.3) + version: 4.3.4(next@15.3.5(@playwright/test@1.53.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(typescript@5.8.3) react: - specifier: ^19.1.0 + specifier: ^19.1 version: 19.1.0 react-dom: - specifier: ^19.1.0 + specifier: ^19.1 version: 19.1.0(react@19.1.0) react-hook-form: specifier: ^7.60.0 @@ -452,10 +522,10 @@ importers: specifier: ^1.12.9 version: 1.12.9 '@types/react': - specifier: ^19.1.8 + specifier: ^19.1 version: 19.1.8 '@types/react-dom': - specifier: ^19.1.6 + specifier: ^19.1 version: 19.1.6(@types/react@19.1.8) '@vitnode/eslint-config': specifier: workspace:* @@ -1053,6 +1123,12 @@ packages: fumadocs-core: ^14.0.0 || ^15.0.0 react: 18.x.x || 19.x.x + '@hono/node-server@1.15.0': + resolution: {integrity: sha512-MjmK4l5N4dQpZ9OSWN0tCj7ejuc7WvuWMzSKtc89bnknJykAeHxzRigXBTYZk85H6Awrii6RM59iUiUluApu2A==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@hono/swagger-ui@0.5.2': resolution: {integrity: sha512-7wxLKdb8h7JTdZ+K8DJNE3KXQMIpJejkBTQjrYlUWF28Z1PGOKw6kUykARe5NTfueIN37jbyG/sBYsbzXzG53A==} peerDependencies: @@ -3548,8 +3624,8 @@ packages: domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} - dotenv@17.0.1: - resolution: {integrity: sha512-GLjkduuAL7IMJg/ZnOPm9AnWKJ82mSE2tzXLaJ/6hD6DhwGfZaXG77oB8qbReyiczNxnbxQKyh0OE5mXq0bAHA==} + dotenv@17.1.0: + resolution: {integrity: sha512-tG9VUTJTuju6GcXgbdsOuRhupE8cb4mRgY5JLRCh4MtGoVo3/gfGUtOMwmProM6d0ba2mCFvv+WrpYJV6qgJXQ==} engines: {node: '>=12'} drizzle-kit@0.31.4: @@ -7256,6 +7332,10 @@ snapshots: - supports-color optional: true + '@hono/node-server@1.15.0(hono@4.8.4)': + dependencies: + hono: 4.8.4 + '@hono/swagger-ui@0.5.2(hono@4.8.4)': dependencies: hono: 4.8.4 @@ -9756,7 +9836,7 @@ snapshots: domelementtype: 2.3.0 domhandler: 5.0.3 - dotenv@17.0.1: {} + dotenv@17.1.0: {} drizzle-kit@0.31.4: dependencies: @@ -11711,7 +11791,7 @@ snapshots: optionalDependencies: typescript: 5.8.3 - next-intl@4.3.4(next@15.3.5(@playwright/test@1.53.2)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(typescript@5.8.3): + next-intl@4.3.4(next@15.3.5(@playwright/test@1.53.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(typescript@5.8.3): dependencies: '@formatjs/intl-localematcher': 0.5.10 negotiator: 1.0.0 diff --git a/scripts/files/file-copy-manager.ts b/scripts/files/file-copy-manager.ts index c53722c73..0d5af3a78 100644 --- a/scripts/files/file-copy-manager.ts +++ b/scripts/files/file-copy-manager.ts @@ -15,15 +15,52 @@ export class FileCopyManager { 'copy-of-vitnode-app', 'root', ); + const singleAppApiDestPath = join( + this.env.WORKSPACE, + 'packages', + 'create-vitnode-app', + 'copy-of-vitnode-app', + 'apps', + 'api-single-app', + ); - if ( - !FileSystem.validatePath(sourcePath, 'web app directory') || - !FileSystem.validatePath(destPath, 'copy-of-vitnode-app directory') - ) { + if (!FileSystem.validatePath(sourcePath, 'web app directory')) { throw new Error('Required paths not found'); } - await this.copyFiles(sourcePath, destPath); + await this.copyFiles(sourcePath, destPath, [ + 'src/app/[locale]/(main)/[...rest]', + 'src/app/[locale]/(main)/not-found.tsx', + 'src/app/[locale]/admin', + 'src/app/favicon.ico', + 'src/app/global-error.tsx', + 'src/app/layout.tsx', + 'src/app/not-found.tsx', + 'postcss.config.mjs', + '.prettierrc.mjs', + ]); + + const apiSourcePath = join(this.env.WORKSPACE, 'apps', 'api'); + const apiDestPath = join( + this.env.WORKSPACE, + 'packages', + 'create-vitnode-app', + 'copy-of-vitnode-app', + 'apps', + 'api', + ); + + await this.copyFiles(apiSourcePath, apiDestPath, [ + 'src', + 'tsconfig.json', + 'drizzle.config.ts', + 'package.json', + ]); + + await this.copyFiles(apiSourcePath, singleAppApiDestPath, [ + 'src/app/api/[...route]', + 'drizzle.config.ts', + ]); } async copyFileOrDirectory( @@ -47,23 +84,11 @@ export class FileCopyManager { } } - async copyFiles(sourcePath: string, destPath: string): Promise { - // Define files and directories to copy (relative paths) - const filesToCopy = [ - 'src/app/[locale]/(main)/[...rest]', - 'src/app/[locale]/(main)/not-found.tsx', - 'src/app/[locale]/admin', - 'src/app/favicon.ico', - 'src/app/global-error.tsx', - 'src/app/layout.tsx', - 'src/app/not-found.tsx', - 'src/app/api/[...route]', - 'tsconfig.json', - 'postcss.config.mjs', - 'drizzle.config.ts', - '.prettierrc.mjs', - ]; - + async copyFiles( + sourcePath: string, + destPath: string, + filesToCopy: string[], + ): Promise { // Handle special files with different names const specialFiles = [ { source: '.gitignore', dest: '.gitignore_template' }, diff --git a/scripts/files/file-system.ts b/scripts/files/file-system.ts index aa3bfbf79..bbb25008a 100644 --- a/scripts/files/file-system.ts +++ b/scripts/files/file-system.ts @@ -23,7 +23,7 @@ export class FileSystem { } for (const item of readdirSync(from)) { - if (item === '(plugins)') continue; + if (item === '(plugins)' || item === '.env') continue; const sourcePath = join(from, item); const destinationPath = join(to, item); @@ -44,6 +44,7 @@ export class FileSystem { static validatePath(filePath: string, description: string): boolean { if (existsSync(filePath)) return true; + console.error(`✖ Missing ${description}: ${filePath}`); return false; } diff --git a/turbo.json b/turbo.json index a57517788..21757c1d2 100644 --- a/turbo.json +++ b/turbo.json @@ -10,28 +10,29 @@ "dependsOn": ["^db:migrate"], "persistent": true, "cache": false, + "inputs": ["$TURBO_DEFAULT$", ".env*"], "env": ["POSTGRES_URL"] }, "db:push": { "dependsOn": ["^db:push"], "cache": false, "persistent": true, + "inputs": ["$TURBO_DEFAULT$", ".env*"], "env": ["POSTGRES_URL"] }, "build:plugins": { "dependsOn": ["^build:plugins"], - "inputs": ["$TURBO_DEFAULT$"], "outputs": ["dist/**"] }, "build": { "dependsOn": ["^build:plugins", "^build"], "inputs": ["$TURBO_DEFAULT$", ".env*"], "outputs": [".next/**", "!.next/cache/**", "dist/**"], - "env": ["POSTGRES_URL"] + "env": ["POSTGRES_URL", "NEXT_PUBLIC_API_URL", "NEXT_PUBLIC_WEB_URL"] }, "build:scripts": { "dependsOn": ["^build:scripts"], - "inputs": ["$TURBO_DEFAULT$"], + "cache": false, "outputs": ["dist/**"] }, "lint": { @@ -43,7 +44,8 @@ "dev": { "cache": false, "persistent": true, - "env": ["POSTGRES_URL"] + "inputs": ["$TURBO_DEFAULT$", ".env*"], + "env": ["POSTGRES_URL", "NEXT_PUBLIC_API_URL", "NEXT_PUBLIC_WEB_URL"] }, "start": { "dependsOn": ["^start"]