diff --git a/internal/cli/registry.go b/internal/cli/registry.go
index 300ca9d..b745bc5 100644
--- a/internal/cli/registry.go
+++ b/internal/cli/registry.go
@@ -8,6 +8,7 @@ import (
"github.com/b-jonathan/taco/internal/stacks/firebase"
"github.com/b-jonathan/taco/internal/stacks/mongodb"
"github.com/b-jonathan/taco/internal/stacks/nextjs"
+ "github.com/b-jonathan/taco/internal/stacks/vite"
)
type Stack = stacks.Stack
@@ -15,6 +16,7 @@ type Stack = stacks.Stack
var Registry = map[string]Stack{
"express": express.New(),
"nextjs": nextjs.New(),
+ "vite": vite.New(),
"mongodb": mongodb.New(),
"firebase": firebase.New(), // TODO: implement Firebase stack
"none": nil,
diff --git a/internal/cli/root.go b/internal/cli/root.go
index 5f00fe5..d526e80 100644
--- a/internal/cli/root.go
+++ b/internal/cli/root.go
@@ -167,7 +167,7 @@ func initCmd() *cobra.Command {
//TODO: We're gonna have to refactor this into a "dependency-style" selection, so only db's supported by chosen backend are seen
- stack["frontend"], _ = prompt.CreateSurveySelect("Choose a Frontend Stack:\n", []string{"NextJS", "None"}, prompt.AskOpts{})
+ stack["frontend"], _ = prompt.CreateSurveySelect("Choose a Frontend Stack:\n", []string{"NextJS", "Vite", "None"}, prompt.AskOpts{})
stack["frontend"] = strings.ToLower(stack["frontend"])
frontend, err := GetFactory(stack["frontend"])
if err != nil {
@@ -187,11 +187,17 @@ func initCmd() *cobra.Command {
return err
}
- stack["auth"], _ = prompt.CreateSurveySelect("Choose an Auth Stack:\n", []string{"Firebase", "None"}, prompt.AskOpts{})
- stack["auth"] = strings.ToLower(stack["auth"])
- auth, err := GetFactory(stack["auth"])
- if err != nil {
- return err
+ // Firebase only supports Next.js currently
+ var auth stacks.Stack
+ if stack["frontend"] == "nextjs" {
+ stack["auth"], _ = prompt.CreateSurveySelect("Choose an Auth Stack:\n", []string{"Firebase", "None"}, prompt.AskOpts{})
+ stack["auth"] = strings.ToLower(stack["auth"])
+ auth, err = GetFactory(stack["auth"])
+ if err != nil {
+ return err
+ }
+ } else {
+ stack["auth"] = "none"
}
opts := &stacks.Options{
diff --git a/internal/stacks/templates/embed.go b/internal/stacks/templates/embed.go
index 861d8e5..57c83f8 100644
--- a/internal/stacks/templates/embed.go
+++ b/internal/stacks/templates/embed.go
@@ -2,5 +2,5 @@ package templates
import "embed"
-//go:embed express/* firebase/* mongodb/* nextjs/*
+//go:embed express/* firebase/* mongodb/* nextjs/* vite/*
var FS embed.FS
diff --git a/internal/stacks/templates/vite/.prettierrc.json.tmpl b/internal/stacks/templates/vite/.prettierrc.json.tmpl
new file mode 100644
index 0000000..ea769fc
--- /dev/null
+++ b/internal/stacks/templates/vite/.prettierrc.json.tmpl
@@ -0,0 +1,7 @@
+{
+"tabWidth": 2,
+"semi": true,
+"singleQuote": false,
+"trailingComma": "all",
+"plugins": ["prettier-plugin-tailwindcss"]
+}
diff --git a/internal/stacks/templates/vite/eslint.config.mjs.tmpl b/internal/stacks/templates/vite/eslint.config.mjs.tmpl
new file mode 100644
index 0000000..802c5ca
--- /dev/null
+++ b/internal/stacks/templates/vite/eslint.config.mjs.tmpl
@@ -0,0 +1,29 @@
+// eslint.config.mjs
+/* eslint-disable */
+import js from '@eslint/js';
+import globals from 'globals';
+import ts from 'typescript-eslint';
+import reactHooks from 'eslint-plugin-react-hooks';
+import reactRefresh from 'eslint-plugin-react-refresh';
+
+export default [
+ { ignores: ['node_modules/**', '**/dist/**', '**/build/**', '**/coverage/**', '**/.cache/**'] },
+ js.configs.recommended,
+ ...ts.configs.recommendedTypeChecked,
+ {
+ files: ['src/**/*.{ts,tsx,js,jsx}'],
+ languageOptions: {
+ globals: { ...globals.browser, ...globals.node },
+ parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname }
+ },
+ plugins: {
+ 'react-hooks': reactHooks,
+ 'react-refresh': reactRefresh,
+ },
+ rules: {
+ 'react-hooks/rules-of-hooks': 'error',
+ 'react-hooks/exhaustive-deps': 'warn',
+ 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
+ }
+ }
+];
diff --git a/internal/stacks/templates/vite/src/App.tsx.tmpl b/internal/stacks/templates/vite/src/App.tsx.tmpl
new file mode 100644
index 0000000..9318e28
--- /dev/null
+++ b/internal/stacks/templates/vite/src/App.tsx.tmpl
@@ -0,0 +1,20 @@
+import { BrowserRouter, Routes, Route, Link } from "react-router";
+import Home from "./pages/Home";
+import About from "./pages/About";
+
+function App() {
+ return (
+
+
+
+ } />
+ } />
+
+
+ );
+}
+
+export default App;
+
diff --git a/internal/stacks/templates/vite/src/index.css.tmpl b/internal/stacks/templates/vite/src/index.css.tmpl
new file mode 100644
index 0000000..f1d8c73
--- /dev/null
+++ b/internal/stacks/templates/vite/src/index.css.tmpl
@@ -0,0 +1 @@
+@import "tailwindcss";
diff --git a/internal/stacks/templates/vite/src/pages/About.tsx.tmpl b/internal/stacks/templates/vite/src/pages/About.tsx.tmpl
new file mode 100644
index 0000000..8cbe101
--- /dev/null
+++ b/internal/stacks/templates/vite/src/pages/About.tsx.tmpl
@@ -0,0 +1,6 @@
+function About() {
+ return
About page
;
+}
+
+export default About;
+
diff --git a/internal/stacks/templates/vite/src/pages/Home.tsx.tmpl b/internal/stacks/templates/vite/src/pages/Home.tsx.tmpl
new file mode 100644
index 0000000..b771345
--- /dev/null
+++ b/internal/stacks/templates/vite/src/pages/Home.tsx.tmpl
@@ -0,0 +1,17 @@
+import { useEffect, useState } from "react";
+
+function Home() {
+ const [message, setMessage] = useState("loading...");
+
+ useEffect(() => {
+ fetch(import.meta.env.VITE_BACKEND_URL || "http://localhost:4000/")
+ .then((res) => res.text())
+ .then(setMessage)
+ .catch((err) => setMessage("error: " + err.message));
+ }, []);
+
+ return {message}
;
+}
+
+export default Home;
+
diff --git a/internal/stacks/templates/vite/vite.config.ts.tmpl b/internal/stacks/templates/vite/vite.config.ts.tmpl
new file mode 100644
index 0000000..c4069b7
--- /dev/null
+++ b/internal/stacks/templates/vite/vite.config.ts.tmpl
@@ -0,0 +1,8 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+import tailwindcss from '@tailwindcss/vite'
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react(), tailwindcss()],
+})
diff --git a/internal/stacks/vite/vite.go b/internal/stacks/vite/vite.go
new file mode 100644
index 0000000..679b2de
--- /dev/null
+++ b/internal/stacks/vite/vite.go
@@ -0,0 +1,138 @@
+package vite
+
+import (
+ "context"
+ "fmt"
+ "path/filepath"
+ "strings"
+
+ "github.com/b-jonathan/taco/internal/execx"
+ "github.com/b-jonathan/taco/internal/fsutil"
+ "github.com/b-jonathan/taco/internal/nodepkg"
+ "github.com/b-jonathan/taco/internal/stacks"
+ "github.com/spf13/afero"
+)
+
+type Stack = stacks.Stack
+type Options = stacks.Options
+
+type vite struct{}
+
+func New() Stack { return &vite{} }
+
+func (vite) Type() string { return "frontend" }
+
+func (vite) Name() string { return "vite" }
+
+func (vite) Init(ctx context.Context, opts *Options) error {
+ if err := fsutil.Fs.MkdirAll(opts.ProjectRoot, 0o755); err != nil {
+ return fmt.Errorf("mkdir: %w", err)
+ }
+
+ // 1) Scaffold Vite with React + TypeScript template
+ viteFlags := []string{
+ "--yes",
+ "create-vite@latest",
+ "frontend",
+ "--template", "react-ts",
+ }
+
+ if err := execx.RunCmd(ctx, opts.ProjectRoot, "npx "+strings.Join(viteFlags, " ")); err != nil {
+ return fmt.Errorf("create-vite: %w", err)
+ }
+
+ frontendDir := filepath.Join(opts.ProjectRoot, "frontend")
+
+ // 2) Install base dependencies
+ if err := execx.RunCmd(ctx, frontendDir, "npm install"); err != nil {
+ return fmt.Errorf("npm install: %w", err)
+ }
+
+ // 3) Install React Router
+ if err := execx.RunCmd(ctx, frontendDir, "npm install react-router"); err != nil {
+ return fmt.Errorf("npm install react-router: %w", err)
+ }
+
+ // 4) Install Tailwind CSS and its dependencies
+ tailwindDeps := []string{
+ "tailwindcss",
+ "@tailwindcss/vite",
+ }
+ if err := execx.RunCmd(ctx, frontendDir, "npm install -D "+strings.Join(tailwindDeps, " ")); err != nil {
+ return fmt.Errorf("npm install tailwind deps: %w", err)
+ }
+
+ // 5) Install ESLint + Prettier
+ devDeps := []string{
+ "eslint",
+ "@eslint/js",
+ "globals",
+ "typescript",
+ "typescript-eslint",
+ "eslint-plugin-react-hooks",
+ "eslint-plugin-react-refresh",
+ "eslint-config-prettier",
+ "prettier",
+ "prettier-plugin-tailwindcss",
+ }
+ if err := execx.RunCmd(ctx, frontendDir, "npm install -D "+strings.Join(devDeps, " ")); err != nil {
+ return fmt.Errorf("npm install dev deps: %w", err)
+ }
+
+ return nil
+}
+
+func (vite) Generate(ctx context.Context, opts *Options) error {
+ frontendDir := filepath.Join(opts.ProjectRoot, "frontend")
+
+ templateDir := "vite"
+ outputDir := filepath.Join(frontendDir)
+
+ if err := fsutil.GenerateFromTemplateDir(templateDir, outputDir); err != nil {
+ return fmt.Errorf("generate vite templates: %w", err)
+ }
+
+ packageParams := nodepkg.InitPackageParams{
+ Name: "frontend",
+ Scripts: map[string]string{
+ "lint-check": "eslint . && prettier --check .",
+ "lint-fix": "eslint . --fix && prettier --write .",
+ },
+ }
+
+ if err := nodepkg.InitPackage(frontendDir, packageParams); err != nil {
+ return fmt.Errorf("init vite package.json: %w", err)
+ }
+
+ return nil
+}
+
+func (vite) Post(ctx context.Context, opts *Options) error {
+ frontendDir := filepath.Join(opts.ProjectRoot, "frontend")
+
+ // Create .env file with VITE_BACKEND_URL
+ envPath := filepath.Join(frontendDir, ".env")
+ if err := fsutil.EnsureFile(envPath); err != nil {
+ return fmt.Errorf("ensure .env: %w", err)
+ }
+
+ dir := filepath.Dir(envPath)
+ if err := fsutil.Fs.MkdirAll(dir, 0o755); err != nil {
+ return fmt.Errorf("mkdir %s: %w", dir, err)
+ }
+ content := `VITE_BACKEND_URL=http://localhost:4000`
+ if err := afero.WriteFile(fsutil.Fs, envPath, []byte(content), 0o644); err != nil {
+ return fmt.Errorf("write %s: %w", envPath, err)
+ }
+
+ // Update root .gitignore
+ gitignorePath := filepath.Join(opts.ProjectRoot, ".gitignore")
+ if err := fsutil.EnsureFile(gitignorePath); err != nil {
+ return fmt.Errorf("ensure gitignore file: %w", err)
+ }
+
+ _ = fsutil.AppendUniqueLines(gitignorePath,
+ []string{"frontend/node_modules/", "frontend/dist/", "frontend/.env*"})
+
+ return nil
+}