Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions internal/cli/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ 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

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,
Expand Down
18 changes: 12 additions & 6 deletions internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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{
Expand Down
2 changes: 1 addition & 1 deletion internal/stacks/templates/embed.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
7 changes: 7 additions & 0 deletions internal/stacks/templates/vite/.prettierrc.json.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"tabWidth": 2,
"semi": true,
"singleQuote": false,
"trailingComma": "all",
"plugins": ["prettier-plugin-tailwindcss"]
}
29 changes: 29 additions & 0 deletions internal/stacks/templates/vite/eslint.config.mjs.tmpl
Original file line number Diff line number Diff line change
@@ -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 }],
}
}
];
20 changes: 20 additions & 0 deletions internal/stacks/templates/vite/src/App.tsx.tmpl
Original file line number Diff line number Diff line change
@@ -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 (
<BrowserRouter>
<nav>
<Link to="/">Home</Link> | <Link to="/about">About</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</BrowserRouter>
);
}

export default App;

1 change: 1 addition & 0 deletions internal/stacks/templates/vite/src/index.css.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import "tailwindcss";
6 changes: 6 additions & 0 deletions internal/stacks/templates/vite/src/pages/About.tsx.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
function About() {
return <div>About page</div>;
}

export default About;

17 changes: 17 additions & 0 deletions internal/stacks/templates/vite/src/pages/Home.tsx.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useEffect, useState } from "react";

function Home() {
const [message, setMessage] = useState<string>("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 <div>{message}</div>;
}

export default Home;

8 changes: 8 additions & 0 deletions internal/stacks/templates/vite/vite.config.ts.tmpl
Original file line number Diff line number Diff line change
@@ -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()],
})
138 changes: 138 additions & 0 deletions internal/stacks/vite/vite.go
Original file line number Diff line number Diff line change
@@ -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{} }

Check failure on line 21 in internal/stacks/vite/vite.go

View workflow job for this annotation

GitHub Actions / osv-scan

cannot use &vite{} (value of type *vite) as stacks.Stack value in return statement: *vite does not implement stacks.Stack (missing method Rollback)

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
}
Loading