diff --git a/.changeset/smart-pandas-sing.md b/.changeset/smart-pandas-sing.md new file mode 100644 index 0000000..e9744d1 --- /dev/null +++ b/.changeset/smart-pandas-sing.md @@ -0,0 +1,5 @@ +--- +"create-better-t-stack": patch +--- + +add svelte diff --git a/apps/cli/README.md b/apps/cli/README.md index 9d90c8b..d8c3ac7 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -21,22 +21,35 @@ Follow the prompts to configure your project or use the `--yes` flag for default ## Features -- **Monorepo**: Turborepo for optimized build system and workspace management -- **Frontend**: React, TanStack Router, TanStack Query, Tailwind CSS with shadcn/ui components -- **Native Apps**: Create React Native apps with Expo for iOS and Android -- **Backend Frameworks**: Choose between Hono, Express, or Elysia -- **API Layer**: End-to-end type safety with tRPC -- **Runtime Options**: Choose between Bun or Node.js for your server -- **Database Options**: SQLite (via Turso), PostgreSQL, MongoDB, or no database -- **ORM Selection**: Choose between Drizzle ORM or Prisma -- **Authentication**: Optional auth setup with Better-Auth -- **Progressive Web App**: Add PWA support with service workers and installable apps -- **Desktop Apps**: Build native desktop apps with Tauri integration -- **Documentation**: Add an Astro Starlight documentation site to your project -- **Code Quality**: Biome for linting and formatting -- **Git Hooks**: Husky with lint-staged for pre-commit checks -- **Examples**: Todo app with full CRUD functionality, AI Chat using AI SDK -- **Developer Experience**: Git initialization, various package manager support (npm, pnpm, bun) +- **TypeScript**: End-to-end type safety. +- **Monorepo Structure**: Choose between Turborepo for optimized builds or standard pnpm/npm/bun workspaces. +- **Frontend Options**: + - React with Vite: TanStack Router, React Router, or TanStack Start. + - Next.js (Full-stack or frontend-only). + - Nuxt (Vue framework). + - SvelteKit. + - React Native with Expo for mobile apps. + - None. +- **UI**: Tailwind CSS with shadcn/ui components pre-configured. +- **Backend Frameworks**: Choose between Hono, Express, Elysia, or use Next.js API routes. +- **API Layer**: End-to-end type safety with tRPC or oRPC. +- **Runtime Options**: Choose between Bun or Node.js for your server. +- **Database Options**: SQLite, PostgreSQL, MySQL, MongoDB, or no database. +- **ORM Selection**: Choose between Drizzle ORM (TypeScript-first), Prisma (feature-rich), or no ORM. +- **Database Setup**: Optional automated setup for Turso (SQLite), Neon (Postgres), Prisma Postgres (Supabase), or MongoDB Atlas. +- **Authentication**: Optional auth setup using Better-Auth (email/password, OAuth coming soon). +- **Addons**: + - **PWA**: Add Progressive Web App support. + - **Tauri**: Build native desktop applications. + - **Starlight**: Add an Astro-based documentation site. + - **Biome**: Integrated linting and formatting. + - **Husky**: Git hooks for code quality checks (lint-staged). + - **Turborepo**: Optimized monorepo build system. +- **Examples**: Include pre-built examples like a Todo app or an AI Chat interface (using Vercel AI SDK). +- **Developer Experience**: + - Automatic Git initialization. + - Choice of package manager (npm, pnpm, bun). + - Optional automatic dependency installation. ## Usage @@ -50,53 +63,61 @@ Options: --orm ORM type (none, drizzle, prisma) --auth Include authentication --no-auth Exclude authentication - --frontend Frontend types (tanstack-router, react-router, tanstack-start, native, none) - --addons Additional addons (pwa, tauri, starlight, biome, husky, none) + --frontend Frontend types (tanstack-router, react-router, tanstack-start, next, nuxt, svelte, native, none) + --addons Additional addons (pwa, tauri, starlight, biome, husky, turborepo, none) --examples Examples to include (todo, ai, none) --git Initialize git repository --no-git Skip git initialization --package-manager Package manager (npm, pnpm, bun) --install Install dependencies --no-install Skip installing dependencies - --db-setup Database setup (turso, prisma-postgres, mongodb-atlas, none) + --db-setup Database setup (turso, neon, prisma-postgres, mongodb-atlas, none) --backend Backend framework (hono, express, elysia) --runtime Runtime (bun, node) + --api API type (trpc, orpc) -h, --help Display help ``` ## Examples Create a project with default configuration: + ```bash npx create-better-t-stack my-app --yes ``` Create a project with specific options: + ```bash npx create-better-t-stack my-app --database postgres --orm drizzle --auth --addons pwa biome ``` Create a project with Elysia and Node.js runtime: + ```bash npx create-better-t-stack my-app --backend elysia --runtime node ``` Create a project with specific frontend options: + ```bash npx create-better-t-stack my-app --frontend tanstack-router native ``` Create a project with examples: + ```bash npx create-better-t-stack my-app --examples todo ai ``` Create a project with Turso database setup: + ```bash npx create-better-t-stack my-app --db-setup turso ``` Create a project with documentation site: + ```bash npx create-better-t-stack my-app --addons starlight ``` diff --git a/apps/cli/src/constants.ts b/apps/cli/src/constants.ts index 7edf3ee..432adfa 100644 --- a/apps/cli/src/constants.ts +++ b/apps/cli/src/constants.ts @@ -75,6 +75,7 @@ export const dependencyVersionMap = { ai: "^4.2.8", "@ai-sdk/google": "^1.2.3", "@ai-sdk/vue": "^1.2.8", + "@ai-sdk/svelte": "^2.1.9", "@prisma/extension-accelerate": "^1.3.0", @@ -82,6 +83,7 @@ export const dependencyVersionMap = { "@orpc/client": "^1.1.0", "@orpc/react-query": "^1.1.0", "@orpc/vue-query": "^1.1.0", + "@orpc/svelte-query": "^1.1.0", "@trpc/tanstack-react-query": "^11.0.0", "@trpc/server": "^11.0.0", diff --git a/apps/cli/src/helpers/addons-setup.ts b/apps/cli/src/helpers/addons-setup.ts index 64d11b4..3fbca77 100644 --- a/apps/cli/src/helpers/addons-setup.ts +++ b/apps/cli/src/helpers/addons-setup.ts @@ -13,6 +13,7 @@ export async function setupAddons(config: ProjectConfig) { const hasReactWebFrontend = frontend.includes("react-router") || frontend.includes("tanstack-router"); const hasNuxtFrontend = frontend.includes("nuxt"); + const hasSvelteFrontend = frontend.includes("svelte"); if (addons.includes("turborepo")) { await addPackageDependency({ @@ -24,7 +25,10 @@ export async function setupAddons(config: ProjectConfig) { if (addons.includes("pwa") && hasReactWebFrontend) { await setupPwa(projectDir, frontend); } - if (addons.includes("tauri") && (hasReactWebFrontend || hasNuxtFrontend)) { + if ( + addons.includes("tauri") && + (hasReactWebFrontend || hasNuxtFrontend || hasSvelteFrontend) + ) { await setupTauri(config); } if (addons.includes("biome")) { @@ -38,10 +42,17 @@ export async function setupAddons(config: ProjectConfig) { } } -export function getWebAppDir( +function getWebAppDir( projectDir: string, frontends: ProjectFrontend[], ): string { + if ( + frontends.some((f) => + ["react-router", "tanstack-router", "nuxt", "svelte"].includes(f), + ) + ) { + return path.join(projectDir, "apps/web"); + } return path.join(projectDir, "apps/web"); } diff --git a/apps/cli/src/helpers/api-setup.ts b/apps/cli/src/helpers/api-setup.ts index 98ebda1..67b6f0d 100644 --- a/apps/cli/src/helpers/api-setup.ts +++ b/apps/cli/src/helpers/api-setup.ts @@ -13,6 +13,7 @@ export async function setupApi(config: ProjectConfig): Promise { ["tanstack-router", "react-router", "tanstack-start", "next"].includes(f), ); const hasNuxtWeb = frontend.includes("nuxt"); + const hasSvelteWeb = frontend.includes("svelte"); if (api === "orpc") { await addPackageDependency({ @@ -61,6 +62,13 @@ export async function setupApi(config: ProjectConfig): Promise { projectDir: webDir, }); } + } else if (hasSvelteWeb) { + if (api === "orpc") { + await addPackageDependency({ + dependencies: ["@orpc/svelte-query", "@orpc/client", "@orpc/server"], + projectDir: webDir, + }); + } } } diff --git a/apps/cli/src/helpers/auth-setup.ts b/apps/cli/src/helpers/auth-setup.ts index bc263ed..cf0281d 100644 --- a/apps/cli/src/helpers/auth-setup.ts +++ b/apps/cli/src/helpers/auth-setup.ts @@ -32,6 +32,7 @@ export async function setupAuth(config: ProjectConfig): Promise { "tanstack-start", "next", "nuxt", + "svelte", ].includes(f), ); diff --git a/apps/cli/src/helpers/create-readme.ts b/apps/cli/src/helpers/create-readme.ts index 2bf5073..f76c86e 100644 --- a/apps/cli/src/helpers/create-readme.ts +++ b/apps/cli/src/helpers/create-readme.ts @@ -39,20 +39,34 @@ function generateReadmeContent(options: ProjectConfig): string { const hasNative = frontend.includes("native"); const hasNext = frontend.includes("next"); const hasTanstackStart = frontend.includes("tanstack-start"); + const hasSvelte = frontend.includes("svelte"); + const hasNuxt = frontend.includes("nuxt"); const packageManagerRunCmd = packageManager === "npm" ? "npm run" : packageManager; let webPort = "3001"; - if (hasReactRouter) { + if (hasReactRouter || hasSvelte) { webPort = "5173"; - } else if (hasNext) { - webPort = "3000"; } return `# ${projectName} -This project was created with [Better-T-Stack](https://github.com/AmanVarshney01/create-better-t-stack), a modern TypeScript stack that combines React, ${hasTanstackRouter ? "TanStack Router" : hasReactRouter ? "React Router" : hasNext ? "Next.js" : hasTanstackStart ? "TanStack Start" : ""}, ${backend[0].toUpperCase() + backend.slice(1)}, tRPC, and more. +This project was created with [Better-T-Stack](https://github.com/AmanVarshney01/create-better-t-stack), a modern TypeScript stack that combines React, ${ + hasTanstackRouter + ? "TanStack Router" + : hasReactRouter + ? "React Router" + : hasNext + ? "Next.js" + : hasTanstackStart + ? "TanStack Start" + : hasSvelte + ? "SvelteKit" + : hasNuxt + ? "Nuxt" + : "" + }, ${backend[0].toUpperCase() + backend.slice(1)}, tRPC, and more. ## Features @@ -75,7 +89,12 @@ ${packageManagerRunCmd} dev \`\`\` ${ - hasTanstackRouter || hasReactRouter || hasNext || hasTanstackStart + hasTanstackRouter || + hasReactRouter || + hasNext || + hasTanstackStart || + hasSvelte || + hasNuxt ? `Open [http://localhost:${webPort}](http://localhost:${webPort}) in your browser to see the web application.` : "" } @@ -93,12 +112,53 @@ ${ \`\`\` ${projectName}/ ├── apps/ -${hasTanstackRouter || hasReactRouter || hasNext || hasTanstackStart ? `│ ├── web/ # Frontend application (${hasTanstackRouter ? "React + TanStack Router" : hasReactRouter ? "React + React Router" : hasNext ? "Next.js" : "React + TanStack Start"})\n` : ""}${hasNative ? "│ ├── native/ # Mobile application (React Native, Expo)\n" : ""}${addons.includes("starlight") ? "│ ├── docs/ # Documentation site (Astro Starlight)\n" : ""}│ └── server/ # Backend API (${backend[0].toUpperCase() + backend.slice(1)}, tRPC) +${ + hasTanstackRouter || + hasReactRouter || + hasNext || + hasTanstackStart || + hasSvelte || + hasNuxt + ? `│ ├── web/ # Frontend application (${ + hasTanstackRouter + ? "React + TanStack Router" + : hasReactRouter + ? "React + React Router" + : hasNext + ? "Next.js" + : hasTanstackStart + ? "React + TanStack Start" + : hasSvelte + ? "SvelteKit" + : hasNuxt + ? "Nuxt" + : "" + })\n` + : "" +}${ + hasNative + ? "│ ├── native/ # Mobile application (React Native, Expo)\n" + : "" +}${ + addons.includes("starlight") + ? "│ ├── docs/ # Documentation site (Astro Starlight)\n" + : "" +}│ └── server/ # Backend API (${ + backend[0].toUpperCase() + backend.slice(1) + }, tRPC) \`\`\` ## Available Scripts -${generateScriptsList(packageManagerRunCmd, database, orm, auth, hasNative, addons, backend)} +${generateScriptsList( + packageManagerRunCmd, + database, + orm, + auth, + hasNative, + addons, + backend, +)} `; } @@ -116,6 +176,8 @@ function generateFeaturesList( const hasNative = frontend.includes("native"); const hasNext = frontend.includes("next"); const hasTanstackStart = frontend.includes("tanstack-start"); + const hasSvelte = frontend.includes("svelte"); + const hasNuxt = frontend.includes("nuxt"); const addonsList = [ "- **TypeScript** - For type safety and improved developer experience", @@ -133,6 +195,10 @@ function generateFeaturesList( addonsList.push( "- **TanStack Start** - SSR framework with TanStack Router", ); + } else if (hasSvelte) { + addonsList.push("- **SvelteKit** - Web framework for building Svelte apps"); + } else if (hasNuxt) { + addonsList.push("- **Nuxt** - The Intuitive Vue Framework"); } if (hasNative) { @@ -162,8 +228,18 @@ function generateFeaturesList( if (database !== "none") { addonsList.push( - `- **${orm === "drizzle" ? "Drizzle" : "Prisma"}** - TypeScript-first ORM`, - `- **${database === "sqlite" ? "SQLite/Turso" : database === "postgres" ? "PostgreSQL" : database === "mysql" ? "MySQL" : "MongoDB"}** - Database engine`, + `- **${ + orm === "drizzle" ? "Drizzle" : "Prisma" + }** - TypeScript-first ORM`, + `- **${ + database === "sqlite" + ? "SQLite/Turso" + : database === "postgres" + ? "PostgreSQL" + : database === "mysql" + ? "MySQL" + : "MongoDB" + }** - Database engine`, ); } @@ -203,7 +279,9 @@ function generateDatabaseSetup( let setup = "## Database Setup\n\n"; if (database === "sqlite") { - setup += `This project uses SQLite${orm === "drizzle" ? " with Drizzle ORM" : " with Prisma"}. + setup += `This project uses SQLite${ + orm === "drizzle" ? " with Drizzle ORM" : " with Prisma" + }. 1. Start the local SQLite database: \`\`\`bash @@ -213,13 +291,17 @@ cd apps/server && ${packageManagerRunCmd} db:local 2. Update your \`.env\` file in the \`apps/server\` directory with the appropriate connection details if needed. `; } else if (database === "postgres") { - setup += `This project uses PostgreSQL${orm === "drizzle" ? " with Drizzle ORM" : " with Prisma"}. + setup += `This project uses PostgreSQL${ + orm === "drizzle" ? " with Drizzle ORM" : " with Prisma" + }. 1. Make sure you have a PostgreSQL database set up. 2. Update your \`apps/server/.env\` file with your PostgreSQL connection details. `; } else if (database === "mysql") { - setup += `This project uses MySQL${orm === "drizzle" ? " with Drizzle ORM" : " with Prisma"}. + setup += `This project uses MySQL${ + orm === "drizzle" ? " with Drizzle ORM" : " with Prisma" + }. 1. Make sure you have a MySQL database set up. 2. Update your \`apps/server/.env\` file with your MySQL connection details. diff --git a/apps/cli/src/helpers/env-setup.ts b/apps/cli/src/helpers/env-setup.ts index 9a203bb..cb45cec 100644 --- a/apps/cli/src/helpers/env-setup.ts +++ b/apps/cli/src/helpers/env-setup.ts @@ -55,15 +55,17 @@ export async function setupEnvironmentVariables( const hasTanStackStart = options.frontend.includes("tanstack-start"); const hasNextJs = options.frontend.includes("next"); const hasNuxt = options.frontend.includes("nuxt"); + const hasSvelte = options.frontend.includes("svelte"); const hasWebFrontend = hasReactRouter || hasTanStackRouter || hasTanStackStart || hasNextJs || - hasNuxt; + hasNuxt || + hasSvelte; let corsOrigin = "http://localhost:3001"; - if (hasReactRouter) { + if (hasReactRouter || hasSvelte) { corsOrigin = "http://localhost:5173"; } else if (hasTanStackRouter || hasTanStackStart || hasNextJs || hasNuxt) { corsOrigin = "http://localhost:3001"; @@ -128,6 +130,8 @@ export async function setupEnvironmentVariables( envVarName = "NEXT_PUBLIC_SERVER_URL"; } else if (hasNuxt) { envVarName = "NUXT_PUBLIC_SERVER_URL"; + } else if (hasSvelte) { + envVarName = "PUBLIC_SERVER_URL"; } const clientVars: EnvVariable[] = [ diff --git a/apps/cli/src/helpers/examples-setup.ts b/apps/cli/src/helpers/examples-setup.ts index 0b3bc97..e8f1517 100644 --- a/apps/cli/src/helpers/examples-setup.ts +++ b/apps/cli/src/helpers/examples-setup.ts @@ -14,11 +14,14 @@ export async function setupExamples(config: ProjectConfig): Promise { const clientDirExists = await fs.pathExists(clientDir); const hasNuxt = frontend.includes("nuxt"); + const hasSvelte = frontend.includes("svelte"); if (clientDirExists) { const dependencies: AvailableDependencies[] = ["ai"]; if (hasNuxt) { dependencies.push("@ai-sdk/vue"); + } else if (hasSvelte) { + dependencies.push("@ai-sdk/svelte"); } await addPackageDependency({ dependencies, diff --git a/apps/cli/src/helpers/post-installation.ts b/apps/cli/src/helpers/post-installation.ts index 0fee131..9ddf648 100644 --- a/apps/cli/src/helpers/post-installation.ts +++ b/apps/cli/src/helpers/post-installation.ts @@ -39,7 +39,7 @@ export function displayPostInstallInstructions( const pwaInstructions = addons?.includes("pwa") && (frontend?.includes("react-router") || - frontend?.includes("tanstack-router")) // Exclude Nuxt from PWA instructions + frontend?.includes("tanstack-router")) ? getPwaInstructions() : ""; const starlightInstructions = addons?.includes("starlight") @@ -51,7 +51,7 @@ export function displayPostInstallInstructions( "react-router", "next", "tanstack-start", - "nuxt", // Include Nuxt here + "nuxt", ].includes(f), ); const hasNative = frontend?.includes("native"); @@ -65,13 +65,13 @@ export function displayPostInstallInstructions( const hasTanstackRouter = frontend?.includes("tanstack-router"); const hasTanstackStart = frontend?.includes("tanstack-start"); const hasReactRouter = frontend?.includes("react-router"); - const hasNuxt = frontend?.includes("nuxt"); // Add Nuxt check + const hasNuxt = frontend?.includes("nuxt"); const hasWebFrontend = - hasTanstackRouter || hasReactRouter || hasTanstackStart || hasNuxt; // Include Nuxt + hasTanstackRouter || hasReactRouter || hasTanstackStart || hasNuxt; const hasNativeFrontend = frontend?.includes("native"); const hasFrontend = hasWebFrontend || hasNativeFrontend; - const webPort = hasReactRouter ? "5173" : "3001"; // Nuxt uses 3001, same as others + const webPort = hasReactRouter ? "5173" : "3001"; const tazeCommand = getPackageExecutionCommand(packageManager, "taze -r"); consola.box( diff --git a/apps/cli/src/helpers/tauri-setup.ts b/apps/cli/src/helpers/tauri-setup.ts index bb4844d..82a8e01 100644 --- a/apps/cli/src/helpers/tauri-setup.ts +++ b/apps/cli/src/helpers/tauri-setup.ts @@ -41,14 +41,22 @@ export async function setupTauri(config: ProjectConfig): Promise { await fs.writeJson(clientPackageJsonPath, packageJson, { spaces: 2 }); } + const hasTanstackRouter = frontend.includes("tanstack-router"); const hasReactRouter = frontend.includes("react-router"); const hasNuxt = frontend.includes("nuxt"); + const hasSvelte = frontend.includes("svelte"); const devUrl = hasReactRouter ? "http://localhost:5173" - : "http://localhost:3001"; + : hasSvelte + ? "http://localhost:5173" + : "http://localhost:3001"; - const frontendDist = hasNuxt ? "../.output/public" : "../dist"; + const frontendDist = hasNuxt + ? "../.output/public" + : hasSvelte + ? "../build" + : "../dist"; const tauriArgs = [ "init", diff --git a/apps/cli/src/helpers/template-manager.ts b/apps/cli/src/helpers/template-manager.ts index 30b5292..2254085 100644 --- a/apps/cli/src/helpers/template-manager.ts +++ b/apps/cli/src/helpers/template-manager.ts @@ -31,6 +31,9 @@ async function processAndCopyFiles( if (path.basename(relativeSrcPath) === "_gitignore") { relativeDestPath = path.join(path.dirname(relativeSrcPath), ".gitignore"); } + if (path.basename(relativeSrcPath) === "_npmrc") { + relativeDestPath = path.join(path.dirname(relativeSrcPath), ".npmrc"); + } const destPath = path.join(destDir, relativeDestPath); @@ -64,9 +67,10 @@ export async function setupFrontendTemplates( ["tanstack-router", "react-router", "tanstack-start", "next"].includes(f), ); const hasNuxtWeb = context.frontend.includes("nuxt"); + const hasSvelteWeb = context.frontend.includes("svelte"); const hasNative = context.frontend.includes("native"); - if (hasReactWeb || hasNuxtWeb) { + if (hasReactWeb || hasNuxtWeb || hasSvelteWeb) { const webAppDir = path.join(projectDir, "apps/web"); await fs.ensureDir(webAppDir); @@ -116,6 +120,25 @@ export async function setupFrontendTemplates( if (await fs.pathExists(apiWebNuxtDir)) { await processAndCopyFiles("**/*", apiWebNuxtDir, webAppDir, context); } + } else if (hasSvelteWeb) { + const svelteBaseDir = path.join(PKG_ROOT, "templates/frontend/svelte"); + if (await fs.pathExists(svelteBaseDir)) { + await processAndCopyFiles("**/*", svelteBaseDir, webAppDir, context); + } + if (context.api === "orpc") { + const apiWebSvelteDir = path.join( + PKG_ROOT, + `templates/api/${context.api}/web/svelte`, + ); + if (await fs.pathExists(apiWebSvelteDir)) { + await processAndCopyFiles( + "**/*", + apiWebSvelteDir, + webAppDir, + context, + ); + } + } } } @@ -255,6 +278,7 @@ export async function setupAuthTemplate( ["tanstack-router", "react-router", "tanstack-start", "next"].includes(f), ); const hasNuxtWeb = context.frontend.includes("nuxt"); + const hasSvelteWeb = context.frontend.includes("svelte"); const hasNative = context.frontend.includes("native"); if (serverAppDirExists) { @@ -310,7 +334,7 @@ export async function setupAuthTemplate( } } - if ((hasReactWeb || hasNuxtWeb) && webAppDirExists) { + if ((hasReactWeb || hasNuxtWeb || hasSvelteWeb) && webAppDirExists) { if (hasReactWeb) { const authWebBaseSrc = path.join( PKG_ROOT, @@ -343,6 +367,21 @@ export async function setupAuthTemplate( if (await fs.pathExists(authWebNuxtSrc)) { await processAndCopyFiles("**/*", authWebNuxtSrc, webAppDir, context); } + } else if (hasSvelteWeb) { + if (context.api === "orpc") { + const authWebSvelteSrc = path.join( + PKG_ROOT, + "templates/auth/web/svelte", + ); + if (await fs.pathExists(authWebSvelteSrc)) { + await processAndCopyFiles( + "**/*", + authWebSvelteSrc, + webAppDir, + context, + ); + } + } } } @@ -403,6 +442,7 @@ export async function setupExamplesTemplate( ["tanstack-router", "react-router", "tanstack-start", "next"].includes(f), ); const hasNuxtWeb = context.frontend.includes("nuxt"); + const hasSvelteWeb = context.frontend.includes("svelte"); for (const example of context.examples) { if (example === "none") continue; @@ -476,7 +516,6 @@ export async function setupExamplesTemplate( } } } else if (hasNuxtWeb && webAppDirExists) { - // Only copy Nuxt examples if the API is oRPC (as tRPC is not supported) if (context.api === "orpc") { const exampleWebNuxtSrc = path.join(exampleBaseDir, "web/nuxt"); if (await fs.pathExists(exampleWebNuxtSrc)) { @@ -488,14 +527,22 @@ export async function setupExamplesTemplate( false, ); } else { - consola.info( - pc.gray( - `Skipping Nuxt web template for example '${example}' (template not found).`, - ), - ); } } - // If API is tRPC, skip Nuxt examples silently as CLI validation prevents this combo. + } else if (hasSvelteWeb && webAppDirExists) { + if (context.api === "orpc") { + const exampleWebSvelteSrc = path.join(exampleBaseDir, "web/svelte"); + if (await fs.pathExists(exampleWebSvelteSrc)) { + await processAndCopyFiles( + "**/*", + exampleWebSvelteSrc, + webAppDir, + context, + false, + ); + } else { + } + } } } } diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index 199636a..9ab0e60 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -77,6 +77,7 @@ async function main() { "next", "nuxt", "native", + "svelte", "none", ], }) @@ -270,11 +271,12 @@ function processAndValidateFlags( f === "react-router" || f === "tanstack-start" || f === "next" || - f === "nuxt", + f === "nuxt" || + f === "svelte", ); if (webFrontends.length > 1) { consola.fatal( - "Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt", + "Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt, svelte", ); process.exit(1); } @@ -446,17 +448,21 @@ function processAndValidateFlags( } } - const includesNative = effectiveFrontend?.includes("native"); const includesNuxt = effectiveFrontend?.includes("nuxt"); + const includesSvelte = effectiveFrontend?.includes("svelte"); - if (includesNuxt && effectiveApi === "trpc") { + if ((includesNuxt || includesSvelte) && effectiveApi === "trpc") { consola.fatal( - `tRPC API is not supported with 'nuxt' frontend. Please use --api orpc or remove 'nuxt' from --frontend.`, + `tRPC API is not supported with '${ + includesNuxt ? "nuxt" : "svelte" + }' frontend. Please use --api orpc or remove '${ + includesNuxt ? "nuxt" : "svelte" + }' from --frontend.`, ); process.exit(1); } if ( - includesNuxt && + (includesNuxt || includesSvelte) && effectiveApi !== "orpc" && (!options.api || (options.yes && options.api !== "trpc")) ) { @@ -474,7 +480,10 @@ function processAndValidateFlags( f === "react-router" || (f === "nuxt" && config.addons?.includes("tauri") && - !config.addons?.includes("pwa")), // Nuxt compatible with Tauri, not PWA + !config.addons?.includes("pwa")) || + (f === "svelte" && + config.addons?.includes("tauri") && + !config.addons?.includes("pwa")), ); if (hasWebSpecificAddons && !hasCompatibleWebFrontend) { @@ -486,7 +495,7 @@ function processAndValidateFlags( config.addons.includes("tauri") ) { incompatibleAddon = - "PWA and Tauri addons require tanstack-router, react-router, or Nuxt (Tauri only)."; + "PWA and Tauri addons require tanstack-router, react-router, or Nuxt/Svelte (Tauri only)."; } consola.fatal( `${incompatibleAddon} Cannot use these addons with your frontend selection.`, @@ -517,12 +526,13 @@ function processAndValidateFlags( "tanstack-start", "next", "nuxt", + "svelte", ].includes(f), ); if (config.examples.length > 0 && !hasWebFrontendForExamples) { consola.fatal( - "Examples require a web frontend (tanstack-router, react-router, tanstack-start, next, or nuxt).", + "Examples require a web frontend (tanstack-router, react-router, tanstack-start, next, nuxt, or svelte).", ); process.exit(1); } @@ -538,5 +548,5 @@ main().catch((err) => { } else { console.error(err); } - process.exit(1); // Ensure exit on error + process.exit(1); }); diff --git a/apps/cli/src/prompts/addons.ts b/apps/cli/src/prompts/addons.ts index fdc972e..5fb3a7e 100644 --- a/apps/cli/src/prompts/addons.ts +++ b/apps/cli/src/prompts/addons.ts @@ -3,65 +3,75 @@ import pc from "picocolors"; import { DEFAULT_CONFIG } from "../constants"; import type { ProjectAddons, ProjectFrontend } from "../types"; +type AddonOption = { + value: ProjectAddons; + label: string; + hint: string; +}; + export async function getAddonsChoice( addons?: ProjectAddons[], frontends?: ProjectFrontend[], ): Promise { if (addons !== undefined) return addons; - const hasCompatibleWebFrontend = + const hasCompatiblePwaFrontend = frontends?.includes("react-router") || frontends?.includes("tanstack-router"); - const addonOptions = [ + const hasCompatibleTauriFrontend = + frontends?.includes("react-router") || + frontends?.includes("tanstack-router") || + frontends?.includes("nuxt") || + frontends?.includes("svelte"); + + const allPossibleOptions: AddonOption[] = [ { - value: "turborepo" as const, + value: "turborepo", label: "Turborepo (Recommended)", hint: "Optimize builds for monorepos", }, { - value: "starlight" as const, + value: "starlight", label: "Starlight", hint: "Add Astro Starlight documentation site", }, { - value: "biome" as const, + value: "biome", label: "Biome", hint: "Add Biome for linting and formatting", }, { - value: "husky" as const, + value: "husky", label: "Husky", hint: "Add Git hooks with Husky, lint-staged (requires Biome)", }, - ]; - - const webAddonOptions = [ { - value: "pwa" as const, + value: "pwa", label: "PWA (Progressive Web App)", hint: "Make your app installable and work offline", }, { - value: "tauri" as const, + value: "tauri", label: "Tauri Desktop App", hint: "Build native desktop apps from your web frontend", }, ]; - const options = hasCompatibleWebFrontend - ? [...addonOptions, ...webAddonOptions] - : addonOptions; + const options = allPossibleOptions.filter((option) => { + if (option.value === "pwa") return hasCompatiblePwaFrontend; + if (option.value === "tauri") return hasCompatibleTauriFrontend; + return true; + }); - const initialValues = DEFAULT_CONFIG.addons.filter( - (addon) => - hasCompatibleWebFrontend || (addon !== "pwa" && addon !== "tauri"), + const initialValues = DEFAULT_CONFIG.addons.filter((addonValue) => + options.some((opt) => opt.value === addonValue), ); - const response = await multiselect({ + const response = await multiselect({ message: "Select addons", - options, - initialValues, + options: options, + initialValues: initialValues, required: false, }); diff --git a/apps/cli/src/prompts/api.ts b/apps/cli/src/prompts/api.ts index 5fdc94b..9949040 100644 --- a/apps/cli/src/prompts/api.ts +++ b/apps/cli/src/prompts/api.ts @@ -10,6 +10,7 @@ export async function getApiChoice( if (Api) return Api; const includesNuxt = frontend?.includes("nuxt"); + const includesSvelte = frontend?.includes("svelte"); let apiOptions = [ { @@ -24,12 +25,14 @@ export async function getApiChoice( }, ]; - if (includesNuxt) { + if (includesNuxt || includesSvelte) { apiOptions = [ { value: "orpc" as const, label: "oRPC", - hint: "End-to-end type-safe APIs (Required for Nuxt frontend)", + hint: `End-to-end type-safe APIs (Required for ${ + includesNuxt ? "Nuxt" : "Svelte" + } frontend)`, }, ]; } @@ -37,7 +40,7 @@ export async function getApiChoice( const apiType = await select({ message: "Select API type", options: apiOptions, - initialValue: includesNuxt ? "orpc" : DEFAULT_CONFIG.api, + initialValue: includesNuxt || includesSvelte ? "orpc" : DEFAULT_CONFIG.api, }); if (isCancel(apiType)) { @@ -45,7 +48,7 @@ export async function getApiChoice( process.exit(0); } - if (includesNuxt && apiType !== "orpc") { + if ((includesNuxt || includesSvelte) && apiType !== "orpc") { return "orpc"; } diff --git a/apps/cli/src/prompts/examples.ts b/apps/cli/src/prompts/examples.ts index a77013e..faa6c42 100644 --- a/apps/cli/src/prompts/examples.ts +++ b/apps/cli/src/prompts/examples.ts @@ -22,8 +22,9 @@ export async function getExamplesChoice( frontends?.includes("react-router") || frontends?.includes("tanstack-router") || frontends?.includes("tanstack-start") || - frontends?.includes("next") || // Added next - frontends?.includes("nuxt"); // Added nuxt + frontends?.includes("next") || + frontends?.includes("nuxt") || + frontends?.includes("svelte"); if (!hasWebFrontend) return []; @@ -36,7 +37,6 @@ export async function getExamplesChoice( }, ]; - // AI example is available for hono, express, next backends, and Nuxt (if backend is not elysia) if (backend !== "elysia") { options.push({ value: "ai" as const, diff --git a/apps/cli/src/prompts/frontend-option.ts b/apps/cli/src/prompts/frontend-option.ts index 246e3cc..ac30662 100644 --- a/apps/cli/src/prompts/frontend-option.ts +++ b/apps/cli/src/prompts/frontend-option.ts @@ -14,7 +14,7 @@ export async function getFrontendChoice( { value: "web", label: "Web", - hint: "React or Vue Web Application", + hint: "React, Vue or Svelte Web Application", }, { value: "native", @@ -29,7 +29,8 @@ export async function getFrontendChoice( f === "react-router" || f === "tanstack-start" || f === "next" || - f === "nuxt", + f === "nuxt" || + f === "svelte", ) ? ["web"] : [], @@ -66,6 +67,11 @@ export async function getFrontendChoice( label: "Nuxt", hint: "The Progressive Web Framework for Vue.js", }, + { + value: "svelte", + label: "Svelte", + hint: "web development for the rest of us", + }, { value: "tanstack-start", label: "TanStack Start (beta)", @@ -78,7 +84,9 @@ export async function getFrontendChoice( f === "tanstack-router" || f === "react-router" || f === "tanstack-start" || - f === "next", + f === "next" || + f === "nuxt" || + f === "svelte", ) || "tanstack-router", }); diff --git a/apps/cli/src/types.ts b/apps/cli/src/types.ts index 4efe104..8ab1d73 100644 --- a/apps/cli/src/types.ts +++ b/apps/cli/src/types.ts @@ -24,6 +24,7 @@ export type ProjectFrontend = | "next" | "nuxt" | "native" + | "svelte" | "none"; export type ProjectDBSetup = | "turso" diff --git a/apps/cli/templates/api/orpc/web/svelte/src/lib/orpc.ts.hbs b/apps/cli/templates/api/orpc/web/svelte/src/lib/orpc.ts.hbs new file mode 100644 index 0000000..03dec41 --- /dev/null +++ b/apps/cli/templates/api/orpc/web/svelte/src/lib/orpc.ts.hbs @@ -0,0 +1,31 @@ +import { PUBLIC_SERVER_URL } from "$env/static/public"; +import { createORPCClient } from "@orpc/client"; +import { RPCLink } from "@orpc/client/fetch"; +import type { RouterClient } from "@orpc/server"; +import { createORPCSvelteQueryUtils } from "@orpc/svelte-query"; +import { QueryCache, QueryClient } from "@tanstack/svelte-query"; +import type { appRouter } from "../../../server/src/routers/index"; + +export const queryClient = new QueryClient({ + queryCache: new QueryCache({ + onError: (error) => { + console.error(`Error: ${error.message}`); + }, + }), +}); + +export const link = new RPCLink({ + url: `${PUBLIC_SERVER_URL}/rpc`, + {{#if auth}} + fetch(url, options) { + return fetch(url, { + ...options, + credentials: "include", + }); + }, + {{/if}} +}); + +export const client: RouterClient = createORPCClient(link); + +export const orpc = createORPCSvelteQueryUtils(client); diff --git a/apps/cli/templates/auth/web/svelte/src/components/SignInForm.svelte b/apps/cli/templates/auth/web/svelte/src/components/SignInForm.svelte new file mode 100644 index 0000000..4bf6474 --- /dev/null +++ b/apps/cli/templates/auth/web/svelte/src/components/SignInForm.svelte @@ -0,0 +1,108 @@ + + +
+

Welcome Back

+ +
{ + e.preventDefault(); + e.stopPropagation(); + form.handleSubmit(); + }} + > + + {#snippet children(field)} +
+ + { + const target = e.target as HTMLInputElement + field.handleChange(target.value) + }} /> + {#if field.state.meta.isTouched} + {#each field.state.meta.errors as error} + + {/each} + {/if} +
+ {/snippet} +
+ + + {#snippet children(field)} +
+ + { + const target = e.target as HTMLInputElement + field.handleChange(target.value) + }} + /> + {#if field.state.meta.isTouched} + {#each field.state.meta.errors as error} + + {/each} + {/if} +
+ {/snippet} +
+ + ({ canSubmit: state.canSubmit, isSubmitting: state.isSubmitting })}> + {#snippet children(state)} + + {/snippet} + +
+ +
+ +
+
diff --git a/apps/cli/templates/auth/web/svelte/src/components/SignUpForm.svelte b/apps/cli/templates/auth/web/svelte/src/components/SignUpForm.svelte new file mode 100644 index 0000000..58ea799 --- /dev/null +++ b/apps/cli/templates/auth/web/svelte/src/components/SignUpForm.svelte @@ -0,0 +1,142 @@ + + +
+

Create Account

+ +
{ + e.preventDefault(); + e.stopPropagation(); + form.handleSubmit(); + }} + > + + {#snippet children(field)} +
+ + { + const target = e.target as HTMLInputElement + field.handleChange(target.value) + }} + /> + {#if field.state.meta.isTouched} + {#each field.state.meta.errors as error} + + {/each} + {/if} +
+ {/snippet} +
+ + + {#snippet children(field)} +
+ + { + const target = e.target as HTMLInputElement + field.handleChange(target.value) + }} + /> + {#if field.state.meta.isTouched} + {#each field.state.meta.errors as error} + + {/each} + {/if} +
+ {/snippet} +
+ + + {#snippet children(field)} +
+ + { + const target = e.target as HTMLInputElement + field.handleChange(target.value) + }} + /> + {#if field.state.meta.errors} + {#each field.state.meta.errors as error} + + {/each} + {/if} +
+ {/snippet} +
+ + ({ canSubmit: state.canSubmit, isSubmitting: state.isSubmitting })}> + {#snippet children(state)} + + {/snippet} + +
+ +
+ +
+
diff --git a/apps/cli/templates/auth/web/svelte/src/components/UserMenu.svelte b/apps/cli/templates/auth/web/svelte/src/components/UserMenu.svelte new file mode 100644 index 0000000..98e64bc --- /dev/null +++ b/apps/cli/templates/auth/web/svelte/src/components/UserMenu.svelte @@ -0,0 +1,54 @@ + + +
+ {#if $sessionQuery.isPending} +
+ {:else if $sessionQuery.data?.user} + {@const user = $sessionQuery.data.user} +
+ + +
+ {:else} +
+ +
+ {/if} +
diff --git a/apps/cli/templates/auth/web/svelte/src/lib/auth-client.ts b/apps/cli/templates/auth/web/svelte/src/lib/auth-client.ts new file mode 100644 index 0000000..7955006 --- /dev/null +++ b/apps/cli/templates/auth/web/svelte/src/lib/auth-client.ts @@ -0,0 +1,6 @@ +import { PUBLIC_SERVER_URL } from "$env/static/public"; +import { createAuthClient } from "better-auth/svelte"; + +export const authClient = createAuthClient({ + baseURL: PUBLIC_SERVER_URL, +}); diff --git a/apps/cli/templates/auth/web/svelte/src/routes/dashboard/+page.svelte b/apps/cli/templates/auth/web/svelte/src/routes/dashboard/+page.svelte new file mode 100644 index 0000000..349021f --- /dev/null +++ b/apps/cli/templates/auth/web/svelte/src/routes/dashboard/+page.svelte @@ -0,0 +1,31 @@ + + +{#if $sessionQuery.isPending} +
Loading...
+{:else if !$sessionQuery.data} + +{:else} +
+

Dashboard

+

Welcome {$sessionQuery.data.user.name}

+

privateData: {$privateDataQuery.data?.message}

+
+{/if} diff --git a/apps/cli/templates/auth/web/svelte/src/routes/login/+page.svelte b/apps/cli/templates/auth/web/svelte/src/routes/login/+page.svelte new file mode 100644 index 0000000..8fe4218 --- /dev/null +++ b/apps/cli/templates/auth/web/svelte/src/routes/login/+page.svelte @@ -0,0 +1,12 @@ + + +{#if showSignIn} + showSignIn = false} /> +{:else} + showSignIn = true} /> +{/if} diff --git a/apps/cli/templates/examples/ai/web/nuxt/app/pages/ai.vue b/apps/cli/templates/examples/ai/web/nuxt/app/pages/ai.vue index 51f8edf..39b1bcf 100644 --- a/apps/cli/templates/examples/ai/web/nuxt/app/pages/ai.vue +++ b/apps/cli/templates/examples/ai/web/nuxt/app/pages/ai.vue @@ -1,6 +1,6 @@ + +
+
+ {#if chat.messages.length === 0} +
Ask the AI anything to get started!
+ {/if} + + {#each chat.messages as message (message.id)} +
+

+ {message.role === 'user' ? 'You' : 'AI Assistant'} +

+
+ {#each message.parts as part, partIndex (partIndex)} + {#if part.type === 'text'} + {part.text} + {:else if part.type === 'tool-invocation'} +
{JSON.stringify(part.toolInvocation, null, 2)}
+ {/if} + {/each} +
+
+ {/each} +
+
+ +
+ { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + chat.handleSubmit(e); + } + }} + /> + +
+
diff --git a/apps/cli/templates/examples/todo/web/svelte/src/routes/todos/+page.svelte b/apps/cli/templates/examples/todo/web/svelte/src/routes/todos/+page.svelte new file mode 100644 index 0000000..22023bb --- /dev/null +++ b/apps/cli/templates/examples/todo/web/svelte/src/routes/todos/+page.svelte @@ -0,0 +1,150 @@ + + +
+

Todos

+ +
+ + +
+ + {#if isLoadingTodos} +

Loading...

+ {:else if !hasTodos} +

No todos yet.

+ {:else} +
    + {#each todos as todo (todo.id)} + {@const isToggling = $toggleMutation.isPending && $toggleMutation.variables?.id === todo.id} + {@const isDeleting = $deleteMutation.isPending && $deleteMutation.variables?.id === todo.id} + {@const isDisabled = isToggling || isDeleting} +
  • +
    + handleToggleTodo(todo.id, todo.completed)} + disabled={isDisabled} + /> + +
    + +
  • + {/each} +
+ {/if} + + {#if $todosQuery.isError} +

+ Error loading: {$todosQuery.error?.message ?? 'Unknown error'} +

+ {/if} + {#if $addMutation.isError} +

+ Error adding: {$addMutation.error?.message ?? 'Unknown error'} +

+ {/if} + {#if $toggleMutation.isError} +

+ Error updating: {$toggleMutation.error?.message ?? 'Unknown error'} +

+ {/if} + {#if $deleteMutation.isError} +

+ Error deleting: {$deleteMutation.error?.message ?? 'Unknown error'} +

+ {/if} +
diff --git a/apps/cli/templates/frontend/svelte/_gitignore b/apps/cli/templates/frontend/svelte/_gitignore new file mode 100644 index 0000000..3b462cb --- /dev/null +++ b/apps/cli/templates/frontend/svelte/_gitignore @@ -0,0 +1,23 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/apps/cli/templates/frontend/svelte/_npmrc b/apps/cli/templates/frontend/svelte/_npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/apps/cli/templates/frontend/svelte/_npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/apps/cli/templates/frontend/svelte/package.json b/apps/cli/templates/frontend/svelte/package.json new file mode 100644 index 0000000..00c16aa --- /dev/null +++ b/apps/cli/templates/frontend/svelte/package.json @@ -0,0 +1,31 @@ +{ + "name": "web", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^6.0.0", + "@sveltejs/kit": "^2.20.7", + "@sveltejs/vite-plugin-svelte": "^5.0.3", + "@tailwindcss/vite": "^4.1.4", + "svelte": "^5.28.2", + "svelte-check": "^4.1.6", + "tailwindcss": "^4.1.4", + "typescript": "^5.8.3", + "@tanstack/svelte-query-devtools": "^5.74.6", + "vite": "^6.3.3" + }, + "dependencies": { + "@tanstack/svelte-form": "^1.7.0", + "@tanstack/svelte-query": "^5.74.4", + "zod": "^3.24.3" + } +} diff --git a/apps/cli/templates/frontend/svelte/src/app.css b/apps/cli/templates/frontend/svelte/src/app.css new file mode 100644 index 0000000..7abc514 --- /dev/null +++ b/apps/cli/templates/frontend/svelte/src/app.css @@ -0,0 +1,5 @@ +@import 'tailwindcss'; + +body { + @apply bg-neutral-950 text-neutral-100; +} diff --git a/apps/cli/templates/frontend/svelte/src/app.d.ts b/apps/cli/templates/frontend/svelte/src/app.d.ts new file mode 100644 index 0000000..da08e6d --- /dev/null +++ b/apps/cli/templates/frontend/svelte/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/apps/cli/templates/frontend/svelte/src/app.html b/apps/cli/templates/frontend/svelte/src/app.html new file mode 100644 index 0000000..77a5ff5 --- /dev/null +++ b/apps/cli/templates/frontend/svelte/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/apps/cli/templates/frontend/svelte/src/components/Header.svelte.hbs b/apps/cli/templates/frontend/svelte/src/components/Header.svelte.hbs new file mode 100644 index 0000000..20e684a --- /dev/null +++ b/apps/cli/templates/frontend/svelte/src/components/Header.svelte.hbs @@ -0,0 +1,40 @@ + + +
+
+ +
+ {{#if auth}} + + {{/if}} +
+
+
+
diff --git a/apps/cli/templates/frontend/svelte/src/lib/index.ts b/apps/cli/templates/frontend/svelte/src/lib/index.ts new file mode 100644 index 0000000..856f2b6 --- /dev/null +++ b/apps/cli/templates/frontend/svelte/src/lib/index.ts @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/apps/cli/templates/frontend/svelte/src/routes/+layout.svelte.hbs b/apps/cli/templates/frontend/svelte/src/routes/+layout.svelte.hbs new file mode 100644 index 0000000..e45c81d --- /dev/null +++ b/apps/cli/templates/frontend/svelte/src/routes/+layout.svelte.hbs @@ -0,0 +1,19 @@ + + + +
+
+
+ {@render children()} +
+
+ +
diff --git a/apps/cli/templates/frontend/svelte/src/routes/+page.svelte.hbs b/apps/cli/templates/frontend/svelte/src/routes/+page.svelte.hbs new file mode 100644 index 0000000..54b2f27 --- /dev/null +++ b/apps/cli/templates/frontend/svelte/src/routes/+page.svelte.hbs @@ -0,0 +1,44 @@ + + +
+
{TITLE_TEXT}
+
+
+

API Status

+
+
+ + {$healthCheck.isLoading + ? "Checking..." + : $healthCheck.data + ? "Connected" + : "Disconnected"} + +
+
+
+
diff --git a/apps/cli/templates/frontend/svelte/static/favicon.png b/apps/cli/templates/frontend/svelte/static/favicon.png new file mode 100644 index 0000000..825b9e6 Binary files /dev/null and b/apps/cli/templates/frontend/svelte/static/favicon.png differ diff --git a/apps/cli/templates/frontend/svelte/svelte.config.js b/apps/cli/templates/frontend/svelte/svelte.config.js new file mode 100644 index 0000000..1295460 --- /dev/null +++ b/apps/cli/templates/frontend/svelte/svelte.config.js @@ -0,0 +1,18 @@ +import adapter from '@sveltejs/adapter-auto'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + // Consult https://svelte.dev/docs/kit/integrations + // for more information about preprocessors + preprocess: vitePreprocess(), + + kit: { + // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. + // If your environment is not supported, or you settled on a specific environment, switch out the adapter. + // See https://svelte.dev/docs/kit/adapters for more information about adapters. + adapter: adapter() + } +}; + +export default config; diff --git a/apps/cli/templates/frontend/svelte/tsconfig.json b/apps/cli/templates/frontend/svelte/tsconfig.json new file mode 100644 index 0000000..0b2d886 --- /dev/null +++ b/apps/cli/templates/frontend/svelte/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes + // from the referenced tsconfig.json - TypeScript does not merge them in +} diff --git a/apps/cli/templates/frontend/svelte/vite.config.ts b/apps/cli/templates/frontend/svelte/vite.config.ts new file mode 100644 index 0000000..2d35c4f --- /dev/null +++ b/apps/cli/templates/frontend/svelte/vite.config.ts @@ -0,0 +1,7 @@ +import tailwindcss from '@tailwindcss/vite'; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [tailwindcss(), sveltekit()] +}); diff --git a/apps/web/public/icon/svelte.svg b/apps/web/public/icon/svelte.svg new file mode 100644 index 0000000..2c0cd42 --- /dev/null +++ b/apps/web/public/icon/svelte.svg @@ -0,0 +1 @@ + diff --git a/apps/web/src/app/(home)/_components/StackArchitech.tsx b/apps/web/src/app/(home)/_components/StackArchitech.tsx index 68aeda3..c47fb8c 100644 --- a/apps/web/src/app/(home)/_components/StackArchitech.tsx +++ b/apps/web/src/app/(home)/_components/StackArchitech.tsx @@ -78,14 +78,23 @@ const CATEGORY_ORDER: Array = [ const hasWebFrontend = (frontend: string[]) => frontend.some((f) => - ["tanstack-router", "react-router", "tanstack-start", "next"].includes(f), + [ + "tanstack-router", + "react-router", + "tanstack-start", + "next", + "nuxt", + "svelte", + ].includes(f), ); const hasPWACompatibleFrontend = (frontend: string[]) => frontend.some((f) => ["tanstack-router", "react-router"].includes(f)); const hasTauriCompatibleFrontend = (frontend: string[]) => - frontend.some((f) => ["tanstack-router", "react-router", "nuxt"].includes(f)); + frontend.some((f) => + ["tanstack-router", "react-router", "nuxt", "svelte"].includes(f), + ); const hasNativeFrontend = (frontend: string[]) => frontend.includes("native"); @@ -216,6 +225,7 @@ const StackArchitect = () => { const isPWACompat = hasPWACompatibleFrontend(nextStack.frontend); const isTauriCompat = hasTauriCompatibleFrontend(nextStack.frontend); const isNuxt = nextStack.frontend.includes("nuxt"); + const isSvelte = nextStack.frontend.includes("svelte"); if (nextStack.database === "none") { if (nextStack.orm !== "none") { @@ -270,7 +280,7 @@ const StackArchitect = () => { changed = true; } - if (isNuxt && nextStack.api === "trpc") { + if ((isNuxt || isSvelte) && nextStack.api === "trpc") { nextStack.api = "orpc"; changed = true; } @@ -446,6 +456,7 @@ const StackArchitect = () => { const isPWACompat = currentHasPWACompatibleFrontend; const isTauriCompat = currentHasTauriCompatibleFrontend; const isNuxt = stack.frontend.includes("nuxt"); + const isSvelte = stack.frontend.includes("svelte"); if (!isPWACompat && stack.addons.includes("pwa")) { notes.frontend.notes.push("PWA addon requires TanStack or React Router."); @@ -466,9 +477,12 @@ const StackArchitect = () => { notes.examples.hasIssue = true; } - if (isNuxt && stack.api === "trpc") { + if ((isNuxt || isSvelte) && stack.api === "trpc") { notes.api.notes.push( "Nuxt requires oRPC. It will be selected automatically.", + `${ + isNuxt ? "Nuxt" : "Svelte" + } requires oRPC. It will be selected automatically.`, ); notes.api.hasIssue = true; notes.frontend.hasIssue = true; @@ -630,6 +644,7 @@ const StackArchitect = () => { "tanstack-start", "next", "nuxt", + "svelte", ]; if (techId === "none") { @@ -718,6 +733,9 @@ const StackArchitect = () => { if (techId === "trpc" && stack.frontend.includes("nuxt")) { return "tRPC is not supported with Nuxt. Use oRPC instead."; } + if (techId === "trpc" && stack.frontend.includes("svelte")) { + return "tRPC is not supported with Svelte. Use oRPC instead."; + } } if (catKey === "orm") { diff --git a/apps/web/src/lib/constant.ts b/apps/web/src/lib/constant.ts index 5e087d5..d25c3e6 100644 --- a/apps/web/src/lib/constant.ts +++ b/apps/web/src/lib/constant.ts @@ -58,6 +58,14 @@ export const TECH_OPTIONS = { color: "from-green-400 to-green-700", default: false, }, + { + id: "svelte", + name: "Svelte", + description: "Cybernetically enhanced web apps", + icon: "/icon/svelte.svg", + color: "from-orange-500 to-orange-700", + default: false, + }, { id: "native", name: "React Native",