diff --git a/apps/cli/src/helpers/core/api-setup.ts b/apps/cli/src/helpers/core/api-setup.ts index f685ac39c..c489311db 100644 --- a/apps/cli/src/helpers/core/api-setup.ts +++ b/apps/cli/src/helpers/core/api-setup.ts @@ -25,6 +25,7 @@ function getFrontendType(frontend: Frontend[]): { hasNuxtWeb: boolean; hasSvelteWeb: boolean; hasSolidWeb: boolean; + hasAstroWeb: boolean; hasNative: boolean; } { const reactBasedFrontends = [ @@ -40,6 +41,7 @@ function getFrontendType(frontend: Frontend[]): { hasNuxtWeb: frontend.includes("nuxt"), hasSvelteWeb: frontend.includes("svelte"), hasSolidWeb: frontend.includes("solid"), + hasAstroWeb: frontend.includes("astro"), hasNative: frontend.some((f) => nativeFrontends.includes(f)), }; } @@ -108,6 +110,15 @@ function getApiDependencies( "@tanstack/solid-router-devtools", ], }; + } else if (frontendType.hasAstroWeb && api === "orpc") { + deps.web = { + dependencies: [ + "@orpc/tanstack-query", + "@orpc/client", + "@tanstack/react-query", + ], + devDependencies: ["@tanstack/react-query-devtools"], + }; } if (api === "trpc") { diff --git a/apps/cli/src/helpers/core/auth-setup.ts b/apps/cli/src/helpers/core/auth-setup.ts index f6246ce20..f973e9af0 100644 --- a/apps/cli/src/helpers/core/auth-setup.ts +++ b/apps/cli/src/helpers/core/auth-setup.ts @@ -116,6 +116,7 @@ export async function setupAuth(config: ProjectConfig) { "tanstack-start", "next", "nuxt", + "astro", "svelte", "solid", ].includes(f), diff --git a/apps/cli/src/helpers/core/create-readme.ts b/apps/cli/src/helpers/core/create-readme.ts index fa31d4585..81818b44b 100644 --- a/apps/cli/src/helpers/core/create-readme.ts +++ b/apps/cli/src/helpers/core/create-readme.ts @@ -174,6 +174,7 @@ function generateStackDescription( const hasTanstackStart = frontend.includes("tanstack-start"); const hasSvelte = frontend.includes("svelte"); const hasNuxt = frontend.includes("nuxt"); + const hasAstro = frontend.includes("astro"); const hasSolid = frontend.includes("solid"); const hasFrontendNone = frontend.length === 0 || frontend.includes("none"); @@ -190,6 +191,8 @@ function generateStackDescription( parts.push("SvelteKit"); } else if (hasNuxt) { parts.push("Nuxt"); + } else if (hasAstro) { + parts.push("Astro"); } else if (hasSolid) { parts.push("SolidJS"); } diff --git a/apps/cli/src/helpers/core/env-setup.ts b/apps/cli/src/helpers/core/env-setup.ts index 02febdc19..1d79af848 100644 --- a/apps/cli/src/helpers/core/env-setup.ts +++ b/apps/cli/src/helpers/core/env-setup.ts @@ -106,6 +106,7 @@ export async function setupEnvironmentVariables(config: ProjectConfig) { const hasTanStackStart = frontend.includes("tanstack-start"); const hasNextJs = frontend.includes("next"); const hasNuxt = frontend.includes("nuxt"); + const hasAstro = frontend.includes("astro"); const hasSvelte = frontend.includes("svelte"); const hasSolid = frontend.includes("solid"); const hasWebFrontend = @@ -114,6 +115,7 @@ export async function setupEnvironmentVariables(config: ProjectConfig) { hasTanStackStart || hasNextJs || hasNuxt || + hasAstro || hasSolid || hasSvelte; @@ -281,12 +283,16 @@ export async function setupEnvironmentVariables(config: ProjectConfig) { if (!(await fs.pathExists(serverDir))) { return; } + const envPath = path.join(serverDir, ".env"); let corsOrigin = "http://localhost:3001"; if (hasReactRouter || hasSvelte) { corsOrigin = "http://localhost:5173"; } + if (hasAstro) { + corsOrigin = "http://localhost:4321"; + } let databaseUrl: string | null = null; diff --git a/apps/cli/src/helpers/core/payments-setup.ts b/apps/cli/src/helpers/core/payments-setup.ts index 5557b826d..438e0d0a2 100644 --- a/apps/cli/src/helpers/core/payments-setup.ts +++ b/apps/cli/src/helpers/core/payments-setup.ts @@ -34,6 +34,7 @@ export async function setupPayments(config: ProjectConfig) { "tanstack-start", "next", "nuxt", + "astro", "svelte", "solid", ].includes(f), diff --git a/apps/cli/src/helpers/core/post-installation.ts b/apps/cli/src/helpers/core/post-installation.ts index 5c6742e26..e11b0def5 100644 --- a/apps/cli/src/helpers/core/post-installation.ts +++ b/apps/cli/src/helpers/core/post-installation.ts @@ -94,6 +94,7 @@ export async function displayPostInstallInstructions( "next", "tanstack-start", "nuxt", + "astro", "svelte", "solid", ].includes(f), diff --git a/apps/cli/src/helpers/core/template-manager.ts b/apps/cli/src/helpers/core/template-manager.ts index f8c3e2c33..0fe61c9e7 100644 --- a/apps/cli/src/helpers/core/template-manager.ts +++ b/apps/cli/src/helpers/core/template-manager.ts @@ -67,6 +67,7 @@ export async function setupFrontendTemplates( ["tanstack-router", "react-router", "tanstack-start", "next"].includes(f), ); const hasNuxtWeb = context.frontend.includes("nuxt"); + const hasAstroWeb = context.frontend.includes("astro"); const hasSvelteWeb = context.frontend.includes("svelte"); const hasSolidWeb = context.frontend.includes("solid"); const hasNativeWind = context.frontend.includes("native-nativewind"); @@ -74,7 +75,7 @@ export async function setupFrontendTemplates( const _hasNative = hasNativeWind || hasUnistyles; const isConvex = context.backend === "convex"; - if (hasReactWeb || hasNuxtWeb || hasSvelteWeb || hasSolidWeb) { + if (hasReactWeb || hasNuxtWeb || hasAstroWeb || hasSvelteWeb || hasSolidWeb) { const webAppDir = path.join(projectDir, "apps/web"); await fs.ensureDir(webAppDir); @@ -140,6 +141,23 @@ export async function setupFrontendTemplates( } else { } } + } else if (hasAstroWeb) { + const astroBaseDir = path.join(PKG_ROOT, "templates/frontend/astro"); + if (await fs.pathExists(astroBaseDir)) { + await processAndCopyFiles("**/*", astroBaseDir, webAppDir, context); + } else { + } + + if (!isConvex && context.api === "orpc") { + const apiWebAstroDir = path.join( + PKG_ROOT, + `templates/api/${context.api}/web/astro`, + ); + if (await fs.pathExists(apiWebAstroDir)) { + await processAndCopyFiles("**/*", apiWebAstroDir, webAppDir, context); + } else { + } + } } else if (hasSvelteWeb) { const svelteBaseDir = path.join(PKG_ROOT, "templates/frontend/svelte"); if (await fs.pathExists(svelteBaseDir)) { diff --git a/apps/cli/src/prompts/frontend.ts b/apps/cli/src/prompts/frontend.ts index f4079c857..91739dd58 100644 --- a/apps/cli/src/prompts/frontend.ts +++ b/apps/cli/src/prompts/frontend.ts @@ -55,6 +55,11 @@ export async function getFrontendChoice( label: "Nuxt", hint: "The Progressive Web Framework for Vue.js", }, + { + value: "astro" as const, + label: "Astro", + hint: "All-in-one web framework for content-driven websites", + }, { value: "svelte" as const, label: "Svelte", diff --git a/apps/cli/src/types.ts b/apps/cli/src/types.ts index a6f7fcfb8..ba9f98369 100644 --- a/apps/cli/src/types.ts +++ b/apps/cli/src/types.ts @@ -27,6 +27,7 @@ export const FrontendSchema = z "tanstack-start", "next", "nuxt", + "astro", "native-nativewind", "native-unistyles", "svelte", diff --git a/apps/cli/src/utils/compatibility-rules.ts b/apps/cli/src/utils/compatibility-rules.ts index 473f02c2a..9d13ecae4 100644 --- a/apps/cli/src/utils/compatibility-rules.ts +++ b/apps/cli/src/utils/compatibility-rules.ts @@ -33,7 +33,7 @@ export function ensureSingleWebAndNative(frontends: Frontend[]) { const { web, native } = splitFrontends(frontends); if (web.length > 1) { exitWithError( - "Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt, svelte, solid", + "Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt, astro, svelte, solid", ); } if (native.length > 1) { diff --git a/apps/cli/src/utils/compatibility.ts b/apps/cli/src/utils/compatibility.ts index 3f488d4c5..0b3f277f2 100644 --- a/apps/cli/src/utils/compatibility.ts +++ b/apps/cli/src/utils/compatibility.ts @@ -6,6 +6,7 @@ export const WEB_FRAMEWORKS: readonly Frontend[] = [ "tanstack-start", "next", "nuxt", + "astro", "svelte", "solid", ] as const; diff --git a/apps/cli/templates/api/orpc/web/astro/src/components/orpc-status.tsx.hbs b/apps/cli/templates/api/orpc/web/astro/src/components/orpc-status.tsx.hbs new file mode 100644 index 000000000..13a7e83a1 --- /dev/null +++ b/apps/cli/templates/api/orpc/web/astro/src/components/orpc-status.tsx.hbs @@ -0,0 +1,39 @@ +import { orpc, queryClient } from '../lib/orpc'; +import { useQuery, QueryClientProvider } from '@tanstack/react-query'; + +function ApiStatus() { + const healthCheck = useQuery(orpc.healthCheck.queryOptions()); + + return ( +
+

API Status

+
+
+ + {healthCheck.isLoading + ? "Checking..." + : healthCheck.data + ? "Connected" + : "Disconnected"} + +
+ {healthCheck.data && ( +
+ Response: {JSON.stringify(healthCheck.data)} +
+ )} +
+ ); +} + +export function OrpcApiStatus() { + return ( + + + + ); +} \ No newline at end of file diff --git a/apps/cli/templates/api/orpc/web/astro/src/lib/orpc.ts.hbs b/apps/cli/templates/api/orpc/web/astro/src/lib/orpc.ts.hbs new file mode 100644 index 000000000..8adc4d352 --- /dev/null +++ b/apps/cli/templates/api/orpc/web/astro/src/lib/orpc.ts.hbs @@ -0,0 +1,35 @@ +import { createORPCClient, onError } from "@orpc/client"; +import { BatchLinkPlugin } from "@orpc/client/plugins"; +import { RPCLink } from "@orpc/client/fetch"; +import { createTanstackQueryUtils } from "@orpc/tanstack-query"; +import { QueryCache, QueryClient } from "@tanstack/react-query"; +import type { AppRouterClient } 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: `${import.meta.env.VITE_SERVER_URL}/rpc`, + plugins: [ + new BatchLinkPlugin({ + groups: [{ + condition: () => true, + context: {}, + }], + }), + ], + interceptors: [ + onError((error) => { + console.error(`RPC Error: ${error.message}`, error) + }) + ], +}); + +export const client: AppRouterClient = createORPCClient(link); + +export const orpc = createTanstackQueryUtils(client); \ No newline at end of file diff --git a/apps/cli/templates/frontend/astro/_gitignore b/apps/cli/templates/frontend/astro/_gitignore new file mode 100644 index 000000000..a0cee654d --- /dev/null +++ b/apps/cli/templates/frontend/astro/_gitignore @@ -0,0 +1,24 @@ +# build output +dist/ +# generated types +.astro/ + +# dependencies +node_modules/ + +# logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + + +# environment variables +.env +.env.production + +# macOS-specific files +.DS_Store + +# jetbrains setting folder +.idea/ \ No newline at end of file diff --git a/apps/cli/templates/frontend/astro/astro.config.mjs.hbs b/apps/cli/templates/frontend/astro/astro.config.mjs.hbs new file mode 100644 index 000000000..fd30411b9 --- /dev/null +++ b/apps/cli/templates/frontend/astro/astro.config.mjs.hbs @@ -0,0 +1,15 @@ +import { defineConfig } from 'astro/config'; +import tailwindcss from '@tailwindcss/vite'; +{{#if (eq api "orpc")}} +import react from '@astrojs/react'; +{{/if}} + +export default defineConfig({ + vite: { + plugins: [tailwindcss()] + }, + {{#if (eq api "orpc")}} + integrations: [react()], + {{/if}} + output: 'static' +}); \ No newline at end of file diff --git a/apps/cli/templates/frontend/astro/package.json.hbs b/apps/cli/templates/frontend/astro/package.json.hbs new file mode 100644 index 000000000..a4c15effb --- /dev/null +++ b/apps/cli/templates/frontend/astro/package.json.hbs @@ -0,0 +1,37 @@ +{ + "name": "web", + "type": "module", + "version": "0.1.0", + "private": true, + "license": "MIT", + "engines": { + "node": ">= 22" + }, + "scripts": { + "dev": "astro dev", + "start": "astro dev", + "build": "astro build", + "preview": "astro preview", + "astro": "astro" + }, + "dependencies": { + "astro": "^5.2.4", + "@tailwindcss/vite": "^4.1.13", + "tailwindcss": "^4.1.13", + "typescript": "^5.9.2", + "zod": "^4.1.5"{{#if (eq api "orpc")}}, + "@astrojs/react": "^4.1.3", + "@types/react": "^19.0.18", + "@types/react-dom": "^19.0.18", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "@orpc/tanstack-query": "^1.9.0", + "@orpc/client": "^1.9.0", + "@tanstack/react-query": "^5.85.5"{{/if}} + }, + "devDependencies": { + "@astrojs/check": "^0.9.4", + {{#if (eq api "orpc")}} + "@tanstack/react-query-devtools": "^5.85.5"{{/if}} + } +} \ No newline at end of file diff --git a/apps/cli/templates/frontend/astro/public/favicon.svg b/apps/cli/templates/frontend/astro/public/favicon.svg new file mode 100644 index 000000000..f157bd1c5 --- /dev/null +++ b/apps/cli/templates/frontend/astro/public/favicon.svg @@ -0,0 +1,9 @@ + + + + diff --git a/apps/cli/templates/frontend/astro/public/robots.txt b/apps/cli/templates/frontend/astro/public/robots.txt new file mode 100644 index 000000000..ca2c7ce06 --- /dev/null +++ b/apps/cli/templates/frontend/astro/public/robots.txt @@ -0,0 +1,5 @@ +# Example: Allow all bots to scan and index your site. +# Full syntax: https://developers.google.com/search/docs/advanced/robots/create-robots-txt + +User-agent: * +Allow: / diff --git a/apps/cli/templates/frontend/astro/src/layouts/Layout.astro.hbs b/apps/cli/templates/frontend/astro/src/layouts/Layout.astro.hbs new file mode 100644 index 000000000..ff6a1231e --- /dev/null +++ b/apps/cli/templates/frontend/astro/src/layouts/Layout.astro.hbs @@ -0,0 +1,26 @@ +--- +import '../styles/global.css'; + + +interface Props { + title?: string | undefined; + description?: string | undefined; +} + +const { title, description = "Built with Better-T-Stack"} = Astro.props; + +--- + + + + + + + + + {title} + + + + + \ No newline at end of file diff --git a/apps/cli/templates/frontend/astro/src/pages/index.astro.hbs b/apps/cli/templates/frontend/astro/src/pages/index.astro.hbs new file mode 100644 index 000000000..1854353dc --- /dev/null +++ b/apps/cli/templates/frontend/astro/src/pages/index.astro.hbs @@ -0,0 +1,38 @@ +--- +{{#if (eq backend "convex")}} +// TODO: Add Convex integration for Astro +{{else}} +{{#if (eq api "orpc")}} +import { OrpcApiStatus } from '../components/orpc-status'; +{{/if}} +{{/if}} + +import Layout from '../layouts/Layout.astro'; + +const TITLE_TEXT = ` + ██████╗ ███████╗████████╗████████╗███████╗██████╗ + ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗ + ██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝ + ██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗ + ██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║ + ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ + + ████████╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗ + ╚══██╔══╝ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝ + ██║ ███████╗ ██║ ███████║██║ █████╔╝ + ██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗ + ██║ ███████║ ██║ ██║ ██║╚██████╗██║ ██╗ + ╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ + `; +--- + + +
+
{TITLE_TEXT}
+
+ {{#if (eq api "orpc")}} + + {{/if}} +
+
+
\ No newline at end of file diff --git a/apps/cli/templates/frontend/astro/src/styles/global.css b/apps/cli/templates/frontend/astro/src/styles/global.css new file mode 100644 index 000000000..d0fb0df46 --- /dev/null +++ b/apps/cli/templates/frontend/astro/src/styles/global.css @@ -0,0 +1,5 @@ +@import 'tailwindcss'; + +body { + @apply bg-neutral-950 text-neutral-100; +} \ No newline at end of file diff --git a/apps/cli/templates/frontend/astro/tsconfig.json.hbs b/apps/cli/templates/frontend/astro/tsconfig.json.hbs new file mode 100644 index 000000000..a6ced2335 --- /dev/null +++ b/apps/cli/templates/frontend/astro/tsconfig.json.hbs @@ -0,0 +1,22 @@ +{ + "extends": "astro/tsconfigs/strict", + "include": [".astro/types.d.ts", "**/*"], + "exclude": ["dist"], + {{#if (eq api "orpc")}} + "references": [ + { "path": "../server" } + ],{{/if}} + "compilerOptions":{ + {{#if (eq api "orpc")}} + "jsx": "react-jsx", + "jsxImportSource": "react", + {{/if}} + "paths": { + "@components/*": ["./src/components/*"], + "@layouts/*": ["./src/layouts/*"], + "@utils/*": ["./src/utils/*"], + "@styles/*": ["./src/styles/*"], + "@assets/*": ["./src/assets/*"] + } + } +} \ No newline at end of file diff --git a/apps/cli/test/frontend.test.ts b/apps/cli/test/frontend.test.ts index d3a352859..11867873e 100644 --- a/apps/cli/test/frontend.test.ts +++ b/apps/cli/test/frontend.test.ts @@ -18,6 +18,7 @@ describe("Frontend Configurations", () => { "native-unistyles", "svelte", "solid", + "astro" ] satisfies ReadonlyArray< | "tanstack-router" | "react-router" @@ -28,6 +29,7 @@ describe("Frontend Configurations", () => { | "native-unistyles" | "svelte" | "solid" + | "astro" >; for (const frontend of singleFrontends) { diff --git a/apps/web/src/lib/constant.ts b/apps/web/src/lib/constant.ts index 7fcbcc094..33e81c79c 100644 --- a/apps/web/src/lib/constant.ts +++ b/apps/web/src/lib/constant.ts @@ -80,6 +80,14 @@ export const TECH_OPTIONS: Record< color: "from-green-400 to-green-700", default: false, }, + { + id: "astro", + name: "Astro", + description: "All-in-one web framework for content-driven websites", + icon: `${ICON_BASE_URL}/astro.svg`, + color: "from-indigo-400 to-indigo-700", + default: false, + }, { id: "svelte", name: "Svelte",