From 4f89b8bc15cca4ba95ee0601ee3efa91d7377be2 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Mon, 5 May 2025 09:51:33 +0530 Subject: [PATCH] add solid --- .changeset/chubby-baths-take.md | 5 + apps/cli/src/constants.ts | 4 + apps/cli/src/helpers/addons-setup.ts | 14 +- apps/cli/src/helpers/api-setup.ts | 34 +++ apps/cli/src/helpers/create-readme.ts | 18 +- apps/cli/src/helpers/env-setup.ts | 2 + apps/cli/src/helpers/post-installation.ts | 3 +- apps/cli/src/helpers/tauri-setup.ts | 6 +- apps/cli/src/helpers/template-manager.ts | 202 ++++++++++++------ apps/cli/src/index.ts | 54 +++-- apps/cli/src/prompts/addons.ts | 6 +- apps/cli/src/prompts/api.ts | 12 +- apps/cli/src/prompts/backend-framework.ts | 77 ++++--- apps/cli/src/prompts/config-prompts.ts | 6 +- apps/cli/src/prompts/examples.ts | 3 +- apps/cli/src/prompts/frontend-option.ts | 104 +++++---- apps/cli/src/types.ts | 1 + .../api/orpc/web/solid/src/utils/orpc.ts.hbs | 30 +++ .../web/solid/src/components/sign-in-form.tsx | 132 ++++++++++++ .../web/solid/src/components/sign-up-form.tsx | 158 ++++++++++++++ .../web/solid/src/components/user-menu.tsx | 54 +++++ .../auth/web/solid/src/lib/auth-client.ts | 5 + .../auth/web/solid/src/routes/dashboard.tsx | 38 ++++ .../auth/web/solid/src/routes/login.tsx | 23 ++ .../prisma/sqlite/prisma/schema/schema.prisma | 2 +- .../todo/web/solid/src/routes/todos.tsx | 132 ++++++++++++ .../react/react-router/vite.config.ts.hbs | 10 +- .../react/tanstack-router/vite.config.ts.hbs | 8 +- apps/cli/templates/frontend/solid/_gitignore | 7 + apps/cli/templates/frontend/solid/index.html | 13 ++ .../cli/templates/frontend/solid/package.json | 33 +++ .../frontend/solid/public/robots.txt | 3 + .../solid/src/components/header.tsx.hbs | 38 ++++ .../frontend/solid/src/components/loader.tsx | 9 + .../templates/frontend/solid/src/main.tsx.hbs | 32 +++ .../frontend/solid/src/routes/__root.tsx.hbs | 21 ++ .../frontend/solid/src/routes/index.tsx.hbs | 65 ++++++ .../frontend/solid/src/routes/todos.tsx | 132 ++++++++++++ .../templates/frontend/solid/src/styles.css | 5 + .../templates/frontend/solid/tsconfig.json | 29 +++ .../frontend/solid/vite.config.js.hbs | 39 ++++ 41 files changed, 1362 insertions(+), 207 deletions(-) create mode 100644 .changeset/chubby-baths-take.md create mode 100644 apps/cli/templates/api/orpc/web/solid/src/utils/orpc.ts.hbs create mode 100644 apps/cli/templates/auth/web/solid/src/components/sign-in-form.tsx create mode 100644 apps/cli/templates/auth/web/solid/src/components/sign-up-form.tsx create mode 100644 apps/cli/templates/auth/web/solid/src/components/user-menu.tsx create mode 100644 apps/cli/templates/auth/web/solid/src/lib/auth-client.ts create mode 100644 apps/cli/templates/auth/web/solid/src/routes/dashboard.tsx create mode 100644 apps/cli/templates/auth/web/solid/src/routes/login.tsx create mode 100644 apps/cli/templates/examples/todo/web/solid/src/routes/todos.tsx create mode 100644 apps/cli/templates/frontend/solid/_gitignore create mode 100644 apps/cli/templates/frontend/solid/index.html create mode 100644 apps/cli/templates/frontend/solid/package.json create mode 100644 apps/cli/templates/frontend/solid/public/robots.txt create mode 100644 apps/cli/templates/frontend/solid/src/components/header.tsx.hbs create mode 100644 apps/cli/templates/frontend/solid/src/components/loader.tsx create mode 100644 apps/cli/templates/frontend/solid/src/main.tsx.hbs create mode 100644 apps/cli/templates/frontend/solid/src/routes/__root.tsx.hbs create mode 100644 apps/cli/templates/frontend/solid/src/routes/index.tsx.hbs create mode 100644 apps/cli/templates/frontend/solid/src/routes/todos.tsx create mode 100644 apps/cli/templates/frontend/solid/src/styles.css create mode 100644 apps/cli/templates/frontend/solid/tsconfig.json create mode 100644 apps/cli/templates/frontend/solid/vite.config.js.hbs diff --git a/.changeset/chubby-baths-take.md b/.changeset/chubby-baths-take.md new file mode 100644 index 0000000..0df7e45 --- /dev/null +++ b/.changeset/chubby-baths-take.md @@ -0,0 +1,5 @@ +--- +"create-better-t-stack": minor +--- + +add solid diff --git a/apps/cli/src/constants.ts b/apps/cli/src/constants.ts index 64e0a67..c55f001 100644 --- a/apps/cli/src/constants.ts +++ b/apps/cli/src/constants.ts @@ -84,6 +84,7 @@ export const dependencyVersionMap = { "@orpc/server": "^1.1.1", "@orpc/client": "^1.1.1", "@orpc/react-query": "^1.1.1", + "@orpc/solid-query": "^1.1.1", "@orpc/vue-query": "^1.1.1", "@orpc/svelte-query": "^1.1.1", @@ -98,6 +99,9 @@ export const dependencyVersionMap = { "@tanstack/svelte-query": "^5.74.4", "@tanstack/react-query-devtools": "^5.69.0", "@tanstack/react-query": "^5.69.0", + + "@tanstack/solid-query": "^5.75.0", + "@tanstack/solid-query-devtools": "^5.75.0", } as const; export type AvailableDependencies = keyof typeof dependencyVersionMap; diff --git a/apps/cli/src/helpers/addons-setup.ts b/apps/cli/src/helpers/addons-setup.ts index 3fbca77..3cd4b46 100644 --- a/apps/cli/src/helpers/addons-setup.ts +++ b/apps/cli/src/helpers/addons-setup.ts @@ -14,6 +14,7 @@ export async function setupAddons(config: ProjectConfig) { frontend.includes("react-router") || frontend.includes("tanstack-router"); const hasNuxtFrontend = frontend.includes("nuxt"); const hasSvelteFrontend = frontend.includes("svelte"); + const hasSolidFrontend = frontend.includes("solid"); if (addons.includes("turborepo")) { await addPackageDependency({ @@ -22,12 +23,15 @@ export async function setupAddons(config: ProjectConfig) { }); } - if (addons.includes("pwa") && hasReactWebFrontend) { + if (addons.includes("pwa") && (hasReactWebFrontend || hasSolidFrontend)) { await setupPwa(projectDir, frontend); } if ( addons.includes("tauri") && - (hasReactWebFrontend || hasNuxtFrontend || hasSvelteFrontend) + (hasReactWebFrontend || + hasNuxtFrontend || + hasSvelteFrontend || + hasSolidFrontend) ) { await setupTauri(config); } @@ -48,7 +52,9 @@ function getWebAppDir( ): string { if ( frontends.some((f) => - ["react-router", "tanstack-router", "nuxt", "svelte"].includes(f), + ["react-router", "tanstack-router", "nuxt", "svelte", "solid"].includes( + f, + ), ) ) { return path.join(projectDir, "apps/web"); @@ -102,7 +108,7 @@ async function setupHusky(projectDir: string) { async function setupPwa(projectDir: string, frontends: ProjectFrontend[]) { const isCompatibleFrontend = frontends.some((f) => - ["react-router", "tanstack-router"].includes(f), + ["react-router", "tanstack-router", "solid"].includes(f), ); if (!isCompatibleFrontend) return; diff --git a/apps/cli/src/helpers/api-setup.ts b/apps/cli/src/helpers/api-setup.ts index 8a440a6..212f8a2 100644 --- a/apps/cli/src/helpers/api-setup.ts +++ b/apps/cli/src/helpers/api-setup.ts @@ -18,6 +18,7 @@ export async function setupApi(config: ProjectConfig): Promise { ); const hasNuxtWeb = frontend.includes("nuxt"); const hasSvelteWeb = frontend.includes("svelte"); + const hasSolidWeb = frontend.includes("solid"); if (!isConvex && api !== "none") { const serverDir = path.join(projectDir, "apps/server"); @@ -85,6 +86,18 @@ export async function setupApi(config: ProjectConfig): Promise { projectDir: webDir, }); } + } else if (hasSolidWeb) { + if (api === "orpc") { + await addPackageDependency({ + dependencies: [ + "@orpc/solid-query", + "@orpc/client", + "@orpc/server", + "@tanstack/solid-query", + ], + projectDir: webDir, + }); + } } } @@ -114,6 +127,7 @@ export async function setupApi(config: ProjectConfig): Promise { "next", "native", ]; + const needsSolidQuery = frontend.includes("solid"); const needsReactQuery = frontend.some((f) => reactBasedFrontends.includes(f)); if (needsReactQuery && !isConvex) { @@ -155,6 +169,26 @@ export async function setupApi(config: ProjectConfig): Promise { } } + if (needsSolidQuery && !isConvex) { + const solidQueryDeps: AvailableDependencies[] = ["@tanstack/solid-query"]; + const solidQueryDevDeps: AvailableDependencies[] = [ + "@tanstack/solid-query-devtools", + ]; + + if (webDirExists) { + const webPkgJsonPath = path.join(webDir, "package.json"); + if (await fs.pathExists(webPkgJsonPath)) { + try { + await addPackageDependency({ + dependencies: solidQueryDeps, + devDependencies: solidQueryDevDeps, + projectDir: webDir, + }); + } catch (error) {} + } + } + } + if (isConvex) { if (webDirExists) { const webPkgJsonPath = path.join(webDir, "package.json"); diff --git a/apps/cli/src/helpers/create-readme.ts b/apps/cli/src/helpers/create-readme.ts index f76c86e..00201c1 100644 --- a/apps/cli/src/helpers/create-readme.ts +++ b/apps/cli/src/helpers/create-readme.ts @@ -40,6 +40,7 @@ function generateReadmeContent(options: ProjectConfig): string { const hasNext = frontend.includes("next"); const hasTanstackStart = frontend.includes("tanstack-start"); const hasSvelte = frontend.includes("svelte"); + const hasSolid = frontend.includes("solid"); const hasNuxt = frontend.includes("nuxt"); const packageManagerRunCmd = @@ -65,7 +66,9 @@ This project was created with [Better-T-Stack](https://github.com/AmanVarshney01 ? "SvelteKit" : hasNuxt ? "Nuxt" - : "" + : hasSolid + ? "SolidJS" + : "" }, ${backend[0].toUpperCase() + backend.slice(1)}, tRPC, and more. ## Features @@ -94,7 +97,8 @@ ${ hasNext || hasTanstackStart || hasSvelte || - hasNuxt + hasNuxt || + hasSolid ? `Open [http://localhost:${webPort}](http://localhost:${webPort}) in your browser to see the web application.` : "" } @@ -118,7 +122,8 @@ ${ hasNext || hasTanstackStart || hasSvelte || - hasNuxt + hasNuxt || + hasSolid ? `│ ├── web/ # Frontend application (${ hasTanstackRouter ? "React + TanStack Router" @@ -132,7 +137,9 @@ ${ ? "SvelteKit" : hasNuxt ? "Nuxt" - : "" + : hasSolid + ? "SolidJS" + : "" })\n` : "" }${ @@ -178,6 +185,7 @@ function generateFeaturesList( const hasTanstackStart = frontend.includes("tanstack-start"); const hasSvelte = frontend.includes("svelte"); const hasNuxt = frontend.includes("nuxt"); + const hasSolid = frontend.includes("solid"); const addonsList = [ "- **TypeScript** - For type safety and improved developer experience", @@ -199,6 +207,8 @@ function generateFeaturesList( addonsList.push("- **SvelteKit** - Web framework for building Svelte apps"); } else if (hasNuxt) { addonsList.push("- **Nuxt** - The Intuitive Vue Framework"); + } else if (hasSolid) { + addonsList.push("- **SolidJS** - Simple and performant reactivity"); } if (hasNative) { diff --git a/apps/cli/src/helpers/env-setup.ts b/apps/cli/src/helpers/env-setup.ts index 3478108..3d50f81 100644 --- a/apps/cli/src/helpers/env-setup.ts +++ b/apps/cli/src/helpers/env-setup.ts @@ -74,12 +74,14 @@ export async function setupEnvironmentVariables( const hasNextJs = frontend.includes("next"); const hasNuxt = frontend.includes("nuxt"); const hasSvelte = frontend.includes("svelte"); + const hasSolid = frontend.includes("solid"); const hasWebFrontend = hasReactRouter || hasTanStackRouter || hasTanStackStart || hasNextJs || hasNuxt || + hasSolid || hasSvelte; if (hasWebFrontend) { diff --git a/apps/cli/src/helpers/post-installation.ts b/apps/cli/src/helpers/post-installation.ts index 45acc3e..3c0aff9 100644 --- a/apps/cli/src/helpers/post-installation.ts +++ b/apps/cli/src/helpers/post-installation.ts @@ -63,6 +63,7 @@ export function displayPostInstallInstructions( "tanstack-start", "nuxt", "svelte", + "solid", ].includes(f), ); const hasNative = frontend?.includes("native"); @@ -75,7 +76,7 @@ export function displayPostInstallInstructions( !isConvex && database !== "none" && orm === "none" ? getNoOrmWarning() : ""; const hasReactRouter = frontend?.includes("react-router"); - const hasSvelte = frontend?.includes("svelte"); + const hasSvelte = frontend?.includes("svelte"); // Keep separate for port logic const webPort = hasReactRouter || hasSvelte ? "5173" : "3001"; const tazeCommand = getPackageExecutionCommand(packageManager, "taze -r"); diff --git a/apps/cli/src/helpers/tauri-setup.ts b/apps/cli/src/helpers/tauri-setup.ts index 42e7a6f..93eaecb 100644 --- a/apps/cli/src/helpers/tauri-setup.ts +++ b/apps/cli/src/helpers/tauri-setup.ts @@ -45,10 +45,10 @@ export async function setupTauri(config: ProjectConfig): Promise { const hasReactRouter = frontend.includes("react-router"); const hasNuxt = frontend.includes("nuxt"); const hasSvelte = frontend.includes("svelte"); + const hasSolid = frontend.includes("solid"); - const devUrl = hasReactRouter - ? "http://localhost:5173" - : hasSvelte + const devUrl = + hasReactRouter || hasSvelte ? "http://localhost:5173" : "http://localhost:3001"; diff --git a/apps/cli/src/helpers/template-manager.ts b/apps/cli/src/helpers/template-manager.ts index 81fbf5d..450afe9 100644 --- a/apps/cli/src/helpers/template-manager.ts +++ b/apps/cli/src/helpers/template-manager.ts @@ -1,7 +1,6 @@ import path from "node:path"; import fs from "fs-extra"; import { globby } from "globby"; -import pc from "picocolors"; import { PKG_ROOT } from "../constants"; import type { ProjectConfig } from "../types"; import { processTemplate } from "../utils/template-processor"; @@ -70,10 +69,11 @@ export async function setupFrontendTemplates( ); const hasNuxtWeb = context.frontend.includes("nuxt"); const hasSvelteWeb = context.frontend.includes("svelte"); + const hasSolidWeb = context.frontend.includes("solid"); const hasNative = context.frontend.includes("native"); const isConvex = context.backend === "convex"; - if (hasReactWeb || hasNuxtWeb || hasSvelteWeb) { + if (hasReactWeb || hasNuxtWeb || hasSvelteWeb || hasSolidWeb) { const webAppDir = path.join(projectDir, "apps/web"); await fs.ensureDir(webAppDir); @@ -105,6 +105,7 @@ export async function setupFrontendTemplates( ); } else { } + if (!isConvex && context.api !== "none") { const apiWebBaseDir = path.join( PKG_ROOT, @@ -127,7 +128,8 @@ export async function setupFrontendTemplates( await processAndCopyFiles("**/*", nuxtBaseDir, webAppDir, context); } else { } - if (!isConvex && context.api !== "none") { + + if (!isConvex && context.api === "orpc") { const apiWebNuxtDir = path.join( PKG_ROOT, `templates/api/${context.api}/web/nuxt`, @@ -143,6 +145,7 @@ export async function setupFrontendTemplates( await processAndCopyFiles("**/*", svelteBaseDir, webAppDir, context); } else { } + if (!isConvex && context.api === "orpc") { const apiWebSvelteDir = path.join( PKG_ROOT, @@ -158,6 +161,23 @@ export async function setupFrontendTemplates( } else { } } + } else if (hasSolidWeb) { + const solidBaseDir = path.join(PKG_ROOT, "templates/frontend/solid"); + if (await fs.pathExists(solidBaseDir)) { + await processAndCopyFiles("**/*", solidBaseDir, webAppDir, context); + } else { + } + + if (!isConvex && context.api === "orpc") { + const apiWebSolidDir = path.join( + PKG_ROOT, + `templates/api/${context.api}/web/solid`, + ); + if (await fs.pathExists(apiWebSolidDir)) { + await processAndCopyFiles("**/*", apiWebSolidDir, webAppDir, context); + } else { + } + } } } @@ -323,6 +343,7 @@ export async function setupAuthTemplate( ); const hasNuxtWeb = context.frontend.includes("nuxt"); const hasSvelteWeb = context.frontend.includes("svelte"); + const hasSolidWeb = context.frontend.includes("solid"); const hasNative = context.frontend.includes("native"); if (serverAppDirExists) { @@ -380,7 +401,10 @@ export async function setupAuthTemplate( } } - if ((hasReactWeb || hasNuxtWeb || hasSvelteWeb) && webAppDirExists) { + if ( + (hasReactWeb || hasNuxtWeb || hasSvelteWeb || hasSolidWeb) && + webAppDirExists + ) { if (hasReactWeb) { const authWebBaseSrc = path.join( PKG_ROOT, @@ -390,6 +414,7 @@ export async function setupAuthTemplate( await processAndCopyFiles("**/*", authWebBaseSrc, webAppDir, context); } else { } + const reactFramework = context.frontend.find((f) => ["tanstack-router", "react-router", "tanstack-start", "next"].includes( f, @@ -432,6 +457,19 @@ export async function setupAuthTemplate( } else { } } + } else if (hasSolidWeb) { + if (context.api === "orpc") { + const authWebSolidSrc = path.join(PKG_ROOT, "templates/auth/web/solid"); + if (await fs.pathExists(authWebSolidSrc)) { + await processAndCopyFiles( + "**/*", + authWebSolidSrc, + webAppDir, + context, + ); + } else { + } + } } } @@ -459,6 +497,7 @@ export async function setupAddonsTemplate( if (addon === "pwa") { addonSrcDir = path.join(PKG_ROOT, "templates/addons/pwa/apps/web"); addonDestDir = path.join(projectDir, "apps/web"); + if (!(await fs.pathExists(addonDestDir))) { continue; } @@ -475,6 +514,14 @@ export async function setupExamplesTemplate( projectDir: string, context: ProjectConfig, ): Promise { + if ( + !context.examples || + context.examples.length === 0 || + context.examples[0] === "none" + ) { + return; + } + const serverAppDir = path.join(projectDir, "apps/server"); const webAppDir = path.join(projectDir, "apps/web"); @@ -486,84 +533,85 @@ export async function setupExamplesTemplate( ); const hasNuxtWeb = context.frontend.includes("nuxt"); const hasSvelteWeb = context.frontend.includes("svelte"); + const hasSolidWeb = context.frontend.includes("solid"); for (const example of context.examples) { - if ( - !context.examples || - context.examples.length === 0 || - context.examples[0] === "none" - ) - continue; - if (example === "none") continue; const exampleBaseDir = path.join(PKG_ROOT, `templates/examples/${example}`); - if (example === "ai" && context.backend === "next" && serverAppDirExists) { - const aiNextServerSrc = path.join(exampleBaseDir, "server/next"); - - if (await fs.pathExists(aiNextServerSrc)) { - await processAndCopyFiles( - "**/*", - aiNextServerSrc, - serverAppDir, - context, - false, - ); - } - } - - if (serverAppDirExists) { + if (serverAppDirExists && context.backend !== "convex") { const exampleServerSrc = path.join(exampleBaseDir, "server"); - if (await fs.pathExists(exampleServerSrc)) { - if (context.backend !== "convex") { - if (context.orm !== "none" && context.database !== "none") { - const exampleOrmBaseSrc = path.join( - exampleServerSrc, - context.orm, - "base", - ); - if (await fs.pathExists(exampleOrmBaseSrc)) { - await processAndCopyFiles( - "**/*", - exampleOrmBaseSrc, - serverAppDir, - context, - false, - ); - } - const exampleDbSchemaSrc = path.join( - exampleServerSrc, - context.orm, - context.database, - ); - if (await fs.pathExists(exampleDbSchemaSrc)) { - await processAndCopyFiles( - "**/*", - exampleDbSchemaSrc, - serverAppDir, - context, - false, - ); - } - } - const generalServerFiles = await globby(["*.ts", "*.hbs"], { - cwd: exampleServerSrc, - onlyFiles: true, - deep: 1, - ignore: [`${context.orm}/**`], - }); - for (const file of generalServerFiles) { - const srcPath = path.join(exampleServerSrc, file); - const destPath = path.join(serverAppDir, file.replace(".hbs", "")); - if (srcPath.endsWith(".hbs")) { - await processTemplate(srcPath, destPath, context); - } else { + if (example === "ai" && context.backend === "next") { + const aiNextServerSrc = path.join(exampleServerSrc, "next"); + if (await fs.pathExists(aiNextServerSrc)) { + await processAndCopyFiles( + "**/*", + aiNextServerSrc, + serverAppDir, + context, + false, + ); + } + } + + if (context.orm !== "none" && context.database !== "none") { + const exampleOrmBaseSrc = path.join( + exampleServerSrc, + context.orm, + "base", + ); + if (await fs.pathExists(exampleOrmBaseSrc)) { + await processAndCopyFiles( + "**/*", + exampleOrmBaseSrc, + serverAppDir, + context, + false, + ); + } + + const exampleDbSchemaSrc = path.join( + exampleServerSrc, + context.orm, + context.database, + ); + if (await fs.pathExists(exampleDbSchemaSrc)) { + await processAndCopyFiles( + "**/*", + exampleDbSchemaSrc, + serverAppDir, + context, + false, + ); + } + } + + const ignorePatterns = [`${context.orm}/**`]; + if (example === "ai" && context.backend === "next") { + ignorePatterns.push("next/**"); + } + + const generalServerFiles = await globby(["**/*.ts", "**/*.hbs"], { + cwd: exampleServerSrc, + onlyFiles: true, + deep: 1, + ignore: ignorePatterns, + }); + + for (const file of generalServerFiles) { + const srcPath = path.join(exampleServerSrc, file); + const destPath = path.join(serverAppDir, file.replace(".hbs", "")); + try { + if (srcPath.endsWith(".hbs")) { + await processTemplate(srcPath, destPath, context); + } else { + if (!(await fs.pathExists(destPath))) { await fs.copy(srcPath, destPath, { overwrite: false }); } } - } + } catch (error) {} } } @@ -620,6 +668,18 @@ export async function setupExamplesTemplate( ); } else { } + } else if (hasSolidWeb) { + const exampleWebSolidSrc = path.join(exampleBaseDir, "web/solid"); + if (await fs.pathExists(exampleWebSolidSrc)) { + await processAndCopyFiles( + "**/*", + exampleWebSolidSrc, + webAppDir, + context, + false, + ); + } else { + } } } } diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index 913b52a..b687261 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -78,6 +78,7 @@ async function main() { "nuxt", "native", "svelte", + "solid", "none", ], }) @@ -326,11 +327,12 @@ function processAndValidateFlags( f === "tanstack-start" || f === "next" || f === "nuxt" || - f === "svelte", + f === "svelte" || + f === "solid", ); if (webFrontends.length > 1) { consola.fatal( - "Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt, svelte", + "Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt, svelte, solid", ); process.exit(1); } @@ -395,6 +397,20 @@ function processAndValidateFlags( process.exit(1); } + if (providedFlags.has("frontend") && options.frontend) { + const incompatibleFrontends = options.frontend.filter( + (f) => f === "nuxt" || f === "solid", + ); + if (incompatibleFrontends.length > 0) { + consola.fatal( + `The following frontends are not compatible with '--backend convex': ${incompatibleFrontends.join( + ", ", + )}. Please choose a different frontend or backend.`, + ); + process.exit(1); + } + } + config.auth = false; config.database = "none"; config.orm = "none"; @@ -529,19 +545,24 @@ function processAndValidateFlags( const includesNuxt = effectiveFrontend?.includes("nuxt"); const includesSvelte = effectiveFrontend?.includes("svelte"); + const includesSolid = effectiveFrontend?.includes("solid"); - if ((includesNuxt || includesSvelte) && effectiveApi === "trpc") { + if ( + (includesNuxt || includesSvelte || includesSolid) && + effectiveApi === "trpc" + ) { consola.fatal( `tRPC API is not supported with '${ - includesNuxt ? "nuxt" : "svelte" + includesNuxt ? "nuxt" : includesSvelte ? "svelte" : "solid" }' frontend. Please use --api orpc or remove '${ - includesNuxt ? "nuxt" : "svelte" + includesNuxt ? "nuxt" : includesSvelte ? "svelte" : "solid" }' from --frontend.`, ); process.exit(1); } + if ( - (includesNuxt || includesSvelte) && + (includesNuxt || includesSvelte || includesSolid) && effectiveApi !== "orpc" && (!options.api || (options.yes && options.api !== "trpc")) ) { @@ -559,6 +580,7 @@ function processAndValidateFlags( (f) => f === "tanstack-router" || f === "react-router" || + f === "solid" || (f === "nuxt" && config.addons?.includes("tauri") && !config.addons?.includes("pwa")) || @@ -576,7 +598,7 @@ function processAndValidateFlags( config.addons.includes("tauri") ) { incompatibleAddon = - "PWA and Tauri addons require tanstack-router, react-router, or Nuxt/Svelte (Tauri only)."; + "PWA requires tanstack-router/react-router/solid. Tauri requires tanstack-router/react-router/Nuxt/Svelte/Solid."; } consola.fatal( `${incompatibleAddon} Cannot use these addons with your frontend selection.`, @@ -632,18 +654,12 @@ function processAndValidateFlags( process.exit(1); } - const hasWebFrontendForExamples = effectiveFrontend?.some((f) => - [ - "tanstack-router", - "react-router", - "tanstack-start", - "next", - "nuxt", - "svelte", - ].includes(f), - ); - const noFrontendSelected = - !effectiveFrontend || effectiveFrontend.length === 0; + if (config.examples.includes("ai") && includesSolid) { + consola.fatal( + "The 'ai' example is not compatible with the Solid frontend.", + ); + process.exit(1); + } } } diff --git a/apps/cli/src/prompts/addons.ts b/apps/cli/src/prompts/addons.ts index 5fb3a7e..a951faf 100644 --- a/apps/cli/src/prompts/addons.ts +++ b/apps/cli/src/prompts/addons.ts @@ -17,13 +17,15 @@ export async function getAddonsChoice( const hasCompatiblePwaFrontend = frontends?.includes("react-router") || - frontends?.includes("tanstack-router"); + frontends?.includes("tanstack-router") || + frontends?.includes("solid"); const hasCompatibleTauriFrontend = frontends?.includes("react-router") || frontends?.includes("tanstack-router") || frontends?.includes("nuxt") || - frontends?.includes("svelte"); + frontends?.includes("svelte") || + frontends?.includes("solid"); const allPossibleOptions: AddonOption[] = [ { diff --git a/apps/cli/src/prompts/api.ts b/apps/cli/src/prompts/api.ts index 1c38e24..5087de3 100644 --- a/apps/cli/src/prompts/api.ts +++ b/apps/cli/src/prompts/api.ts @@ -16,6 +16,7 @@ export async function getApiChoice( const includesNuxt = frontend?.includes("nuxt"); const includesSvelte = frontend?.includes("svelte"); + const includesSolid = frontend?.includes("solid"); let apiOptions = [ { @@ -30,13 +31,13 @@ export async function getApiChoice( }, ]; - if (includesNuxt || includesSvelte) { + if (includesNuxt || includesSvelte || includesSolid) { apiOptions = [ { value: "orpc" as const, label: "oRPC", hint: `End-to-end type-safe APIs (Required for ${ - includesNuxt ? "Nuxt" : "Svelte" + includesNuxt ? "Nuxt" : includesSvelte ? "Svelte" : "Solid" } frontend)`, }, ]; @@ -45,7 +46,10 @@ export async function getApiChoice( const apiType = await select({ message: "Select API type", options: apiOptions, - initialValue: includesNuxt || includesSvelte ? "orpc" : DEFAULT_CONFIG.api, + initialValue: + includesNuxt || includesSvelte || includesSolid + ? "orpc" + : DEFAULT_CONFIG.api, }); if (isCancel(apiType)) { @@ -53,7 +57,7 @@ export async function getApiChoice( process.exit(0); } - if ((includesNuxt || includesSvelte) && apiType !== "orpc") { + if ((includesNuxt || includesSvelte || includesSolid) && apiType !== "orpc") { return "orpc"; } diff --git a/apps/cli/src/prompts/backend-framework.ts b/apps/cli/src/prompts/backend-framework.ts index 6d0381f..8ae4f79 100644 --- a/apps/cli/src/prompts/backend-framework.ts +++ b/apps/cli/src/prompts/backend-framework.ts @@ -1,43 +1,62 @@ import { cancel, isCancel, select } from "@clack/prompts"; import pc from "picocolors"; import { DEFAULT_CONFIG } from "../constants"; -import type { ProjectBackend } from "../types"; +import type { ProjectBackend, ProjectFrontend } from "../types"; export async function getBackendFrameworkChoice( backendFramework?: ProjectBackend, + frontends?: ProjectFrontend[], ): Promise { if (backendFramework !== undefined) return backendFramework; + const hasIncompatibleFrontend = frontends?.some( + (f) => f === "nuxt" || f === "solid", + ); + + const backendOptions: Array<{ + value: ProjectBackend; + label: string; + hint: string; + }> = [ + { + value: "hono" as const, + label: "Hono", + hint: "Lightweight, ultrafast web framework", + }, + { + value: "next" as const, + label: "Next.js", + hint: "Full-stack framework with API routes", + }, + { + value: "express" as const, + label: "Express", + hint: "Fast, unopinionated, minimalist web framework for Node.js", + }, + { + value: "elysia" as const, + label: "Elysia", + hint: "Ergonomic web framework for building backend servers", + }, + ]; + + if (!hasIncompatibleFrontend) { + backendOptions.push({ + value: "convex" as const, + label: "Convex", + hint: "Reactive backend-as-a-service platform", + }); + } + + let initialValue = DEFAULT_CONFIG.backend; + if (hasIncompatibleFrontend && initialValue === "convex") { + initialValue = "hono"; + } + const response = await select({ message: "Select backend framework", - options: [ - { - value: "hono", - label: "Hono", - hint: "Lightweight, ultrafast web framework", - }, - { - value: "next", - label: "Next.js", - hint: "Full-stack framework with API routes", - }, - { - value: "express", - label: "Express", - hint: "Fast, unopinionated, minimalist web framework for Node.js", - }, - { - value: "elysia", - label: "Elysia", - hint: "Ergonomic web framework for building backend servers", - }, - { - value: "convex", - label: "Convex", - hint: "Reactive backend-as-a-service platform", - }, - ], - initialValue: DEFAULT_CONFIG.backend, + options: backendOptions, + initialValue, }); if (isCancel(response)) { diff --git a/apps/cli/src/prompts/config-prompts.ts b/apps/cli/src/prompts/config-prompts.ts index 701ba4a..cbbcbbd 100644 --- a/apps/cli/src/prompts/config-prompts.ts +++ b/apps/cli/src/prompts/config-prompts.ts @@ -53,8 +53,10 @@ export async function gatherConfig( projectName: async () => { return getProjectName(flags.projectName); }, - frontend: () => getFrontendChoice(flags.frontend), - backend: () => getBackendFrameworkChoice(flags.backend), + frontend: ({ results }) => + getFrontendChoice(flags.frontend, flags.backend), + backend: ({ results }) => + getBackendFrameworkChoice(flags.backend, results.frontend), runtime: ({ results }) => getRuntimeChoice(flags.runtime, results.backend), database: ({ results }) => diff --git a/apps/cli/src/prompts/examples.ts b/apps/cli/src/prompts/examples.ts index 716ff97..f9fdbf3 100644 --- a/apps/cli/src/prompts/examples.ts +++ b/apps/cli/src/prompts/examples.ts @@ -37,6 +37,7 @@ export async function getExamplesChoice( "next", "nuxt", "svelte", + "solid", ].includes(f), ) ?? false; const noFrontendSelected = !frontends || frontends.length === 0; @@ -52,7 +53,7 @@ export async function getExamplesChoice( }, ]; - if (backend !== "elysia") { + if (backend !== "elysia" && !frontends?.includes("solid")) { options.push({ value: "ai" as const, label: "AI Chat", diff --git a/apps/cli/src/prompts/frontend-option.ts b/apps/cli/src/prompts/frontend-option.ts index ac30662..e7adebf 100644 --- a/apps/cli/src/prompts/frontend-option.ts +++ b/apps/cli/src/prompts/frontend-option.ts @@ -1,10 +1,11 @@ import { cancel, isCancel, multiselect, select } from "@clack/prompts"; import pc from "picocolors"; import { DEFAULT_CONFIG } from "../constants"; -import type { ProjectFrontend } from "../types"; +import type { ProjectBackend, ProjectFrontend } from "../types"; export async function getFrontendChoice( frontendOptions?: ProjectFrontend[], + backend?: ProjectBackend, ): Promise { if (frontendOptions !== undefined) return frontendOptions; @@ -23,17 +24,7 @@ export async function getFrontendChoice( }, ], required: false, - initialValues: DEFAULT_CONFIG.frontend.some( - (f) => - f === "tanstack-router" || - f === "react-router" || - f === "tanstack-start" || - f === "next" || - f === "nuxt" || - f === "svelte", - ) - ? ["web"] - : [], + initialValues: ["web"], }); if (isCancel(frontendTypes)) { @@ -44,50 +35,55 @@ export async function getFrontendChoice( const result: ProjectFrontend[] = []; if (frontendTypes.includes("web")) { + const allWebOptions = [ + { + value: "tanstack-router" as const, + label: "TanStack Router", + hint: "Modern and scalable routing for React Applications", + }, + { + value: "react-router" as const, + label: "React Router", + hint: "A user‑obsessed, standards‑focused, multi‑strategy router", + }, + { + value: "next" as const, + label: "Next.js", + hint: "The React Framework for the Web", + }, + { + value: "nuxt" as const, + label: "Nuxt", + hint: "The Progressive Web Framework for Vue.js", + }, + { + value: "svelte" as const, + label: "Svelte", + hint: "web development for the rest of us", + }, + { + value: "solid" as const, + label: "Solid", + hint: "Simple and performant reactivity for building user interfaces", + }, + { + value: "tanstack-start" as const, + label: "TanStack Start (beta)", + hint: "SSR, Server Functions, API Routes and more with TanStack Router", + }, + ]; + + const webOptions = allWebOptions.filter((option) => { + if (backend === "convex") { + return option.value !== "nuxt" && option.value !== "solid"; + } + return true; + }); + const webFramework = await select({ message: "Choose frontend framework", - options: [ - { - value: "tanstack-router", - label: "TanStack Router", - hint: "Modern and scalable routing for React Applications", - }, - { - value: "react-router", - label: "React Router", - hint: "A user‑obsessed, standards‑focused, multi‑strategy router", - }, - { - value: "next", - label: "Next.js", - hint: "The React Framework for the Web", - }, - { - value: "nuxt", - 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)", - hint: "SSR, Server Functions, API Routes and more with TanStack Router", - }, - ], - initialValue: - DEFAULT_CONFIG.frontend.find( - (f) => - f === "tanstack-router" || - f === "react-router" || - f === "tanstack-start" || - f === "next" || - f === "nuxt" || - f === "svelte", - ) || "tanstack-router", + options: webOptions, + initialValue: DEFAULT_CONFIG.frontend[0], }); if (isCancel(webFramework)) { diff --git a/apps/cli/src/types.ts b/apps/cli/src/types.ts index 1ce2547..6521982 100644 --- a/apps/cli/src/types.ts +++ b/apps/cli/src/types.ts @@ -25,6 +25,7 @@ export type ProjectFrontend = | "nuxt" | "native" | "svelte" + | "solid" | "none"; export type ProjectDBSetup = | "turso" diff --git a/apps/cli/templates/api/orpc/web/solid/src/utils/orpc.ts.hbs b/apps/cli/templates/api/orpc/web/solid/src/utils/orpc.ts.hbs new file mode 100644 index 0000000..8243d04 --- /dev/null +++ b/apps/cli/templates/api/orpc/web/solid/src/utils/orpc.ts.hbs @@ -0,0 +1,30 @@ +import { createORPCClient } from "@orpc/client"; +import { RPCLink } from "@orpc/client/fetch"; +import { createORPCSolidQueryUtils } from "@orpc/solid-query"; +import { QueryCache, QueryClient } from "@tanstack/solid-query"; +import type { appRouter } from "../../../server/src/routers/index"; +import type { RouterClient } from "@orpc/server"; + +export const queryClient = new QueryClient({ + queryCache: new QueryCache({ + onError: (error) => { + console.error(`Error: ${error.message}`); + }, + }), +}); + +export const link = new RPCLink({ + url: `${import.meta.env.VITE_SERVER_URL}/rpc`, + {{#if auth}} + fetch(url, options) { + return fetch(url, { + ...options, + credentials: "include", + }); + }, + {{/if}} +}); + +export const client: RouterClient = createORPCClient(link); + +export const orpc = createORPCSolidQueryUtils(client); diff --git a/apps/cli/templates/auth/web/solid/src/components/sign-in-form.tsx b/apps/cli/templates/auth/web/solid/src/components/sign-in-form.tsx new file mode 100644 index 0000000..e8d5311 --- /dev/null +++ b/apps/cli/templates/auth/web/solid/src/components/sign-in-form.tsx @@ -0,0 +1,132 @@ +import { authClient } from "@/lib/auth-client"; +import { createForm } from "@tanstack/solid-form"; +import { useNavigate } from "@tanstack/solid-router"; +import { z } from "zod"; +import { For } from "solid-js"; + +export default function SignInForm({ + onSwitchToSignUp, +}: { + onSwitchToSignUp: () => void; +}) { + const navigate = useNavigate({ + from: "/", + }); + + const form = createForm(() => ({ + defaultValues: { + email: "", + password: "", + }, + onSubmit: async ({ value }) => { + await authClient.signIn.email( + { + email: value.email, + password: value.password, + }, + { + onSuccess: () => { + navigate({ + to: "/dashboard", + }); + console.log("Sign in successful"); + }, + onError: (error) => { + console.error(error.error.message); + }, + }, + ); + }, + validators: { + onSubmit: z.object({ + email: z.string().email("Invalid email address"), + password: z.string().min(8, "Password must be at least 8 characters"), + }), + }, + })); + + return ( +
+

Welcome Back

+ +
{ + e.preventDefault(); + e.stopPropagation(); + void form.handleSubmit(); + }} + class="space-y-4" + > +
+ + {(field) => ( +
+ + field().handleChange(e.currentTarget.value)} // Use onInput and currentTarget + class="w-full rounded border p-2" // Example basic styling + /> + + {(error) => ( +

{error?.message}

+ )} +
+
+ )} +
+
+ +
+ + {(field) => ( +
+ + field().handleChange(e.currentTarget.value)} + class="w-full rounded border p-2" + /> + + {(error) => ( +

{error?.message}

+ )} +
+
+ )} +
+
+ + + {(state) => ( + + )} + +
+ +
+ +
+
+ ); +} diff --git a/apps/cli/templates/auth/web/solid/src/components/sign-up-form.tsx b/apps/cli/templates/auth/web/solid/src/components/sign-up-form.tsx new file mode 100644 index 0000000..aac8e69 --- /dev/null +++ b/apps/cli/templates/auth/web/solid/src/components/sign-up-form.tsx @@ -0,0 +1,158 @@ +import { authClient } from "@/lib/auth-client"; +import { createForm } from "@tanstack/solid-form"; +import { useNavigate } from "@tanstack/solid-router"; +import { z } from "zod"; +import { For } from "solid-js"; + +export default function SignUpForm({ + onSwitchToSignIn, +}: { + onSwitchToSignIn: () => void; +}) { + const navigate = useNavigate({ + from: "/", + }); + + const form = createForm(() => ({ + defaultValues: { + email: "", + password: "", + name: "", + }, + onSubmit: async ({ value }) => { + await authClient.signUp.email( + { + email: value.email, + password: value.password, + name: value.name, + }, + { + onSuccess: () => { + navigate({ + to: "/dashboard", + }); + console.log("Sign up successful"); + }, + onError: (error) => { + console.error(error.error.message); + }, + }, + ); + }, + validators: { + onSubmit: z.object({ + name: z.string().min(2, "Name must be at least 2 characters"), + email: z.string().email("Invalid email address"), + password: z.string().min(8, "Password must be at least 8 characters"), + }), + }, + })); + + return ( +
+

Create Account

+ +
{ + e.preventDefault(); + e.stopPropagation(); + void form.handleSubmit(); + }} + class="space-y-4" + > +
+ + {(field) => ( +
+ + field().handleChange(e.currentTarget.value)} + class="w-full rounded border p-2" + /> + + {(error) => ( +

{error?.message}

+ )} +
+
+ )} +
+
+ +
+ + {(field) => ( +
+ + field().handleChange(e.currentTarget.value)} + class="w-full rounded border p-2" + /> + + {(error) => ( +

{error?.message}

+ )} +
+
+ )} +
+
+ +
+ + {(field) => ( +
+ + field().handleChange(e.currentTarget.value)} + class="w-full rounded border p-2" + /> + + {(error) => ( +

{error?.message}

+ )} +
+
+ )} +
+
+ + + {(state) => ( + + )} + +
+ +
+ +
+
+ ); +} diff --git a/apps/cli/templates/auth/web/solid/src/components/user-menu.tsx b/apps/cli/templates/auth/web/solid/src/components/user-menu.tsx new file mode 100644 index 0000000..6845cbd --- /dev/null +++ b/apps/cli/templates/auth/web/solid/src/components/user-menu.tsx @@ -0,0 +1,54 @@ +import { authClient } from "@/lib/auth-client"; +import { useNavigate, Link } from "@tanstack/solid-router"; +import { createSignal, Show } from "solid-js"; + +export default function UserMenu() { + const navigate = useNavigate(); + const session = authClient.useSession(); + const [isMenuOpen, setIsMenuOpen] = createSignal(false); + + return ( +
+ +
+ + + + + Sign In + + + + + + + +
+
{session().data?.user.email}
+ +
+
+
+
+ ); +} diff --git a/apps/cli/templates/auth/web/solid/src/lib/auth-client.ts b/apps/cli/templates/auth/web/solid/src/lib/auth-client.ts new file mode 100644 index 0000000..73bac9b --- /dev/null +++ b/apps/cli/templates/auth/web/solid/src/lib/auth-client.ts @@ -0,0 +1,5 @@ +import { createAuthClient } from "better-auth/solid"; + +export const authClient = createAuthClient({ + baseURL: import.meta.env.VITE_SERVER_URL, +}); diff --git a/apps/cli/templates/auth/web/solid/src/routes/dashboard.tsx b/apps/cli/templates/auth/web/solid/src/routes/dashboard.tsx new file mode 100644 index 0000000..c4b888f --- /dev/null +++ b/apps/cli/templates/auth/web/solid/src/routes/dashboard.tsx @@ -0,0 +1,38 @@ +import { authClient } from "@/lib/auth-client"; +import { orpc } from "@/utils/orpc"; +import { useQuery } from "@tanstack/solid-query"; +import { createFileRoute } from "@tanstack/solid-router"; +import { createEffect, Show } from "solid-js"; + +export const Route = createFileRoute("/dashboard")({ + component: RouteComponent, +}); + +function RouteComponent() { + const session = authClient.useSession(); + const navigate = Route.useNavigate(); + + const privateData = useQuery(() => orpc.privateData.queryOptions()); + + createEffect(() => { + if (!session().data && !session().isPending) { + navigate({ + to: "/login", + }); + } + }); + + return ( +
+ +
Loading...
+
+ + +

Dashboard

+

Welcome {session().data?.user.name}

+

privateData: {privateData.data?.message}

+
+
+ ); +} diff --git a/apps/cli/templates/auth/web/solid/src/routes/login.tsx b/apps/cli/templates/auth/web/solid/src/routes/login.tsx new file mode 100644 index 0000000..49b8cc0 --- /dev/null +++ b/apps/cli/templates/auth/web/solid/src/routes/login.tsx @@ -0,0 +1,23 @@ +import SignInForm from "@/components/sign-in-form"; +import SignUpForm from "@/components/sign-up-form"; +import { createFileRoute } from "@tanstack/solid-router"; +import { createSignal, Match, Switch } from "solid-js"; + +export const Route = createFileRoute("/login")({ + component: RouteComponent, +}); + +function RouteComponent() { + const [showSignIn, setShowSignIn] = createSignal(false); + + return ( + + + setShowSignIn(false)} /> + + + setShowSignIn(true)} /> + + + ); +} diff --git a/apps/cli/templates/db/prisma/sqlite/prisma/schema/schema.prisma b/apps/cli/templates/db/prisma/sqlite/prisma/schema/schema.prisma index e12db8b..2121d4b 100644 --- a/apps/cli/templates/db/prisma/sqlite/prisma/schema/schema.prisma +++ b/apps/cli/templates/db/prisma/sqlite/prisma/schema/schema.prisma @@ -6,5 +6,5 @@ generator client { datasource db { provider = "sqlite" - url = "file:../local.db" + url = env("DATABASE_URL") } diff --git a/apps/cli/templates/examples/todo/web/solid/src/routes/todos.tsx b/apps/cli/templates/examples/todo/web/solid/src/routes/todos.tsx new file mode 100644 index 0000000..ff800c0 --- /dev/null +++ b/apps/cli/templates/examples/todo/web/solid/src/routes/todos.tsx @@ -0,0 +1,132 @@ +import { createFileRoute } from "@tanstack/solid-router"; +import { Loader2, Trash2 } from "lucide-solid"; +import { createSignal, For, Show } from "solid-js"; +import { orpc } from "@/utils/orpc"; +import { useQuery, useMutation } from "@tanstack/solid-query"; + +export const Route = createFileRoute("/todos")({ + component: TodosRoute, +}); + +function TodosRoute() { + const [newTodoText, setNewTodoText] = createSignal(""); + + const todos = useQuery(() => orpc.todo.getAll.queryOptions()); + + const createMutation = useMutation(() => + orpc.todo.create.mutationOptions({ + onSuccess: () => { + todos.refetch(); + setNewTodoText(""); + }, + }), + ); + + const toggleMutation = useMutation(() => + orpc.todo.toggle.mutationOptions({ + onSuccess: () => todos.refetch(), + }), + ); + + const deleteMutation = useMutation(() => + orpc.todo.delete.mutationOptions({ + onSuccess: () => todos.refetch(), + }), + ); + + const handleAddTodo = (e: Event) => { + e.preventDefault(); + if (newTodoText().trim()) { + createMutation.mutate({ text: newTodoText() }); + } + }; + + const handleToggleTodo = (id: number, completed: boolean) => { + toggleMutation.mutate({ id, completed: !completed }); + }; + + const handleDeleteTodo = (id: number) => { + deleteMutation.mutate({ id }); + }; + + return ( +
+
+
+

Todo List

+

Manage your tasks efficiently

+
+
+
+ setNewTodoText(e.currentTarget.value)} + placeholder="Add a new task..." + disabled={createMutation.isPending} + class="w-full rounded-md border p-2 text-sm" + /> + +
+ + +
+ +
+
+ + +

No todos yet. Add one above!

+
+ + +
    + + {(todo) => ( +
  • +
    + + handleToggleTodo(todo.id, todo.completed) + } + id={`todo-${todo.id}`} + class="h-4 w-4" + /> + +
    + +
  • + )} +
    +
+
+
+
+
+ ); +} diff --git a/apps/cli/templates/frontend/react/react-router/vite.config.ts.hbs b/apps/cli/templates/frontend/react/react-router/vite.config.ts.hbs index 4347486..9f15eaa 100644 --- a/apps/cli/templates/frontend/react/react-router/vite.config.ts.hbs +++ b/apps/cli/templates/frontend/react/react-router/vite.config.ts.hbs @@ -1,4 +1,3 @@ -{{! Import VitePWA only if 'pwa' addon is selected }} {{#if (includes addons "pwa")}} import { VitePWA } from "vite-plugin-pwa"; {{/if}} @@ -12,24 +11,21 @@ export default defineConfig({ tailwindcss(), reactRouter(), tsconfigPaths(), - {{! Add VitePWA plugin config only if 'pwa' addon is selected }} {{#if (includes addons "pwa")}} VitePWA({ registerType: "autoUpdate", manifest: { - // Use context variables for better naming name: "{{projectName}}", short_name: "{{projectName}}", description: "{{projectName}} - PWA Application", theme_color: "#0c0c0c", - // Add more manifest options as needed }, pwaAssets: { - disabled: false, // Set to false to enable asset generation - config: true, // Use pwa-assets.config.ts + disabled: false, + config: true, }, devOptions: { - enabled: true, // Enable PWA features in dev mode + enabled: true, }, }), {{/if}} diff --git a/apps/cli/templates/frontend/react/tanstack-router/vite.config.ts.hbs b/apps/cli/templates/frontend/react/tanstack-router/vite.config.ts.hbs index 5b2a08d..24eadde 100644 --- a/apps/cli/templates/frontend/react/tanstack-router/vite.config.ts.hbs +++ b/apps/cli/templates/frontend/react/tanstack-router/vite.config.ts.hbs @@ -16,19 +16,17 @@ export default defineConfig({ VitePWA({ registerType: "autoUpdate", manifest: { - // Use context variables for better naming name: "{{projectName}}", short_name: "{{projectName}}", description: "{{projectName}} - PWA Application", theme_color: "#0c0c0c", - // Add more manifest options as needed }, pwaAssets: { - disabled: false, // Set to false to enable asset generation - config: true, // Use pwa-assets.config.ts + disabled: false, + config: true, }, devOptions: { - enabled: true, // Enable PWA features in dev mode + enabled: true, }, }), {{/if}} diff --git a/apps/cli/templates/frontend/solid/_gitignore b/apps/cli/templates/frontend/solid/_gitignore new file mode 100644 index 0000000..2749f5b --- /dev/null +++ b/apps/cli/templates/frontend/solid/_gitignore @@ -0,0 +1,7 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local +.env +.env.* diff --git a/apps/cli/templates/frontend/solid/index.html b/apps/cli/templates/frontend/solid/index.html new file mode 100644 index 0000000..762e95e --- /dev/null +++ b/apps/cli/templates/frontend/solid/index.html @@ -0,0 +1,13 @@ + + + + + + + + + +
+ + + diff --git a/apps/cli/templates/frontend/solid/package.json b/apps/cli/templates/frontend/solid/package.json new file mode 100644 index 0000000..6bd462f --- /dev/null +++ b/apps/cli/templates/frontend/solid/package.json @@ -0,0 +1,33 @@ +{ + "name": "web", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port 3001", + "build": "vite build && tsc", + "serve": "vite preview", + "test": "vitest run" + }, + "dependencies": { + "@orpc/client": "^1.1.1", + "@orpc/server": "^1.1.1", + "@orpc/solid-query": "^1.1.1", + "@tailwindcss/vite": "^4.0.6", + "@tanstack/router-plugin": "^1.109.2", + "@tanstack/solid-form": "^1.9.0", + "@tanstack/solid-query": "^5.75.0", + "@tanstack/solid-query-devtools": "^5.75.0", + "@tanstack/solid-router": "^1.110.0", + "@tanstack/solid-router-devtools": "^1.109.2", + "better-auth": "^1.2.7", + "lucide-solid": "^0.507.0", + "solid-js": "^1.9.4", + "tailwindcss": "^4.0.6", + "zod": "^3.24.3" + }, + "devDependencies": { + "typescript": "^5.7.2", + "vite": "^6.0.11", + "vite-plugin-solid": "^2.11.2" + } +} diff --git a/apps/cli/templates/frontend/solid/public/robots.txt b/apps/cli/templates/frontend/solid/public/robots.txt new file mode 100644 index 0000000..e9e57dc --- /dev/null +++ b/apps/cli/templates/frontend/solid/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/apps/cli/templates/frontend/solid/src/components/header.tsx.hbs b/apps/cli/templates/frontend/solid/src/components/header.tsx.hbs new file mode 100644 index 0000000..684a37d --- /dev/null +++ b/apps/cli/templates/frontend/solid/src/components/header.tsx.hbs @@ -0,0 +1,38 @@ +import { Link } from "@tanstack/solid-router"; +{{#if auth}} +import UserMenu from "./user-menu"; +{{/if}} +import { For } from "solid-js"; + +export default function Header() { + const links = [ + { to: "/", label: "Home" }, + {{#if auth}} + { to: "/dashboard", label: "Dashboard" }, + {{/if}} + {{#if (includes examples "todo")}} + { to: "/todos", label: "Todos" }, + {{/if}} + {{#if (includes examples "ai")}} + { to: "/ai", label: "AI Chat" }, + {{/if}} + ]; + + return ( +
+
+ +
+ {{#if auth}} + + {{/if}} +
+
+
+
+ ); +} diff --git a/apps/cli/templates/frontend/solid/src/components/loader.tsx b/apps/cli/templates/frontend/solid/src/components/loader.tsx new file mode 100644 index 0000000..6107505 --- /dev/null +++ b/apps/cli/templates/frontend/solid/src/components/loader.tsx @@ -0,0 +1,9 @@ +import { Loader2 } from "lucide-solid"; + +export default function Loader() { + return ( +
+ +
+ ); +} diff --git a/apps/cli/templates/frontend/solid/src/main.tsx.hbs b/apps/cli/templates/frontend/solid/src/main.tsx.hbs new file mode 100644 index 0000000..39daf83 --- /dev/null +++ b/apps/cli/templates/frontend/solid/src/main.tsx.hbs @@ -0,0 +1,32 @@ +import { RouterProvider, createRouter } from "@tanstack/solid-router"; +import { render } from "solid-js/web"; +import { routeTree } from "./routeTree.gen"; +import "./styles.css"; +import { QueryClientProvider } from "@tanstack/solid-query"; +import { queryClient } from "./utils/orpc"; + +const router = createRouter({ + routeTree, + defaultPreload: "intent", + scrollRestoration: true, + defaultPreloadStaleTime: 0, +}); + +declare module "@tanstack/solid-router" { + interface Register { + router: typeof router; + } +} + +function App() { + return ( + + + + ); +} + +const rootElement = document.getElementById("app"); +if (rootElement) { + render(() => , rootElement); +} diff --git a/apps/cli/templates/frontend/solid/src/routes/__root.tsx.hbs b/apps/cli/templates/frontend/solid/src/routes/__root.tsx.hbs new file mode 100644 index 0000000..8e6a35d --- /dev/null +++ b/apps/cli/templates/frontend/solid/src/routes/__root.tsx.hbs @@ -0,0 +1,21 @@ +import Header from "@/components/header"; +import { Outlet, createRootRouteWithContext } from "@tanstack/solid-router"; +import { TanStackRouterDevtools } from "@tanstack/solid-router-devtools"; +import { SolidQueryDevtools } from "@tanstack/solid-query-devtools"; + +export const Route = createRootRouteWithContext()({ + component: RootComponent, +}); + +function RootComponent() { + return ( + <> +
+
+ +
+ + + + ); +} diff --git a/apps/cli/templates/frontend/solid/src/routes/index.tsx.hbs b/apps/cli/templates/frontend/solid/src/routes/index.tsx.hbs new file mode 100644 index 0000000..2f97106 --- /dev/null +++ b/apps/cli/templates/frontend/solid/src/routes/index.tsx.hbs @@ -0,0 +1,65 @@ +import { createFileRoute } from "@tanstack/solid-router"; +import { useQuery } from "@tanstack/solid-query"; +import { orpc } from "../utils/orpc"; +import { Match, Switch } from "solid-js"; + +export const Route = createFileRoute("/")({ + component: App, +}); + +const TITLE_TEXT = ` + ██████╗ ███████╗████████╗████████╗███████╗██████╗ + ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗ + ██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝ + ██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗ + ██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║ + ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ + + ████████╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗ + ╚══██╔══╝ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝ + ██║ ███████╗ ██║ ███████║██║ █████╔╝ + ██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗ + ██║ ███████║ ██║ ██║ ██║╚██████╗██║ ██╗ + ╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ + `; + +function App() { + const healthCheck = useQuery(() => orpc.healthCheck.queryOptions()); + + return ( +
+
{TITLE_TEXT}
+
+
+

API Status

+ + +
+
{" "} + Checking... +
+ + +
+
+ Disconnected +
+ + +
+
+ + {healthCheck.data + ? "Connected" + : "Disconnected (Success but no data)"} + +
+ + +
+
+
+ ); +} diff --git a/apps/cli/templates/frontend/solid/src/routes/todos.tsx b/apps/cli/templates/frontend/solid/src/routes/todos.tsx new file mode 100644 index 0000000..ff800c0 --- /dev/null +++ b/apps/cli/templates/frontend/solid/src/routes/todos.tsx @@ -0,0 +1,132 @@ +import { createFileRoute } from "@tanstack/solid-router"; +import { Loader2, Trash2 } from "lucide-solid"; +import { createSignal, For, Show } from "solid-js"; +import { orpc } from "@/utils/orpc"; +import { useQuery, useMutation } from "@tanstack/solid-query"; + +export const Route = createFileRoute("/todos")({ + component: TodosRoute, +}); + +function TodosRoute() { + const [newTodoText, setNewTodoText] = createSignal(""); + + const todos = useQuery(() => orpc.todo.getAll.queryOptions()); + + const createMutation = useMutation(() => + orpc.todo.create.mutationOptions({ + onSuccess: () => { + todos.refetch(); + setNewTodoText(""); + }, + }), + ); + + const toggleMutation = useMutation(() => + orpc.todo.toggle.mutationOptions({ + onSuccess: () => todos.refetch(), + }), + ); + + const deleteMutation = useMutation(() => + orpc.todo.delete.mutationOptions({ + onSuccess: () => todos.refetch(), + }), + ); + + const handleAddTodo = (e: Event) => { + e.preventDefault(); + if (newTodoText().trim()) { + createMutation.mutate({ text: newTodoText() }); + } + }; + + const handleToggleTodo = (id: number, completed: boolean) => { + toggleMutation.mutate({ id, completed: !completed }); + }; + + const handleDeleteTodo = (id: number) => { + deleteMutation.mutate({ id }); + }; + + return ( +
+
+
+

Todo List

+

Manage your tasks efficiently

+
+
+
+ setNewTodoText(e.currentTarget.value)} + placeholder="Add a new task..." + disabled={createMutation.isPending} + class="w-full rounded-md border p-2 text-sm" + /> + +
+ + +
+ +
+
+ + +

No todos yet. Add one above!

+
+ + +
    + + {(todo) => ( +
  • +
    + + handleToggleTodo(todo.id, todo.completed) + } + id={`todo-${todo.id}`} + class="h-4 w-4" + /> + +
    + +
  • + )} +
    +
+
+
+
+
+ ); +} diff --git a/apps/cli/templates/frontend/solid/src/styles.css b/apps/cli/templates/frontend/solid/src/styles.css new file mode 100644 index 0000000..1f51fbb --- /dev/null +++ b/apps/cli/templates/frontend/solid/src/styles.css @@ -0,0 +1,5 @@ +@import "tailwindcss"; + +body { + @apply bg-neutral-950 text-neutral-100; +} diff --git a/apps/cli/templates/frontend/solid/tsconfig.json b/apps/cli/templates/frontend/solid/tsconfig.json new file mode 100644 index 0000000..a805e10 --- /dev/null +++ b/apps/cli/templates/frontend/solid/tsconfig.json @@ -0,0 +1,29 @@ +{ + "include": ["**/*.ts", "**/*.tsx"], + "compilerOptions": { + "target": "ES2022", + "jsx": "preserve", + "jsxImportSource": "solid-js", + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["vite/client"], + + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + "skipLibCheck": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + + "rootDirs": ["."], + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/apps/cli/templates/frontend/solid/vite.config.js.hbs b/apps/cli/templates/frontend/solid/vite.config.js.hbs new file mode 100644 index 0000000..c677aa4 --- /dev/null +++ b/apps/cli/templates/frontend/solid/vite.config.js.hbs @@ -0,0 +1,39 @@ +import { defineConfig } from "vite"; +import { TanStackRouterVite } from "@tanstack/router-plugin/vite"; +import solidPlugin from "vite-plugin-solid"; +import tailwindcss from "@tailwindcss/vite"; +import path from "node:path"; +{{#if (includes addons "pwa")}} +import { VitePWA } from "vite-plugin-pwa"; +{{/if}} + +export default defineConfig({ + plugins: [ + TanStackRouterVite({ target: "solid", autoCodeSplitting: true }), + solidPlugin(), + tailwindcss(), + {{#if (includes addons "pwa")}} + VitePWA({ + registerType: "autoUpdate", + manifest: { + name: "{{projectName}}", + short_name: "{{projectName}}", + description: "{{projectName}} - PWA Application", + theme_color: "#0c0c0c", + }, + pwaAssets: { + disabled: false, + config: true, + }, + devOptions: { + enabled: true, + }, + }), + {{/if}} + ], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, +});