Public Access
- draft new now offers API, Client, or Full Stack (monorepo) project shapes - draft new client scaffolds a Nuxt or Vue app via the official nuxi/create-vue tooling, with a Draft-maintained auth/API overlay layered on top - draft new monorepo combines a scaffolded API and client under one root with go.work, pnpm-workspace.yaml, and docker-compose wiring - assets/embedded restructured into api/monorepo-root/client-nuxt/client-vue zones, synced via a generalized, job-based sync tool
322 lines
9.1 KiB
Go
322 lines
9.1 KiB
Go
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
func init() {
|
|
newCmd.AddCommand(clientCmd)
|
|
}
|
|
|
|
var clientCmd = &cobra.Command{
|
|
Use: "client",
|
|
Short: "Create a new Draft Client project",
|
|
Long: `Create a new Draft Client project with the necessary structure and files.`,
|
|
Run: genClient,
|
|
}
|
|
|
|
func genClient(cmd *cobra.Command, args []string) {
|
|
_, projectDir, serviceName, err := promptProjectName()
|
|
if err != nil {
|
|
fmt.Printf("Prompt failed %v\n", err)
|
|
return
|
|
}
|
|
|
|
isVue, resultLabel, err := promptClientFramework()
|
|
if err != nil {
|
|
fmt.Printf("Prompt failed %v\n", err)
|
|
return
|
|
}
|
|
|
|
if err := requirePnpmAndNpx(); err != nil {
|
|
fmt.Printf("error checking for pnpm and npx: %v", err)
|
|
return
|
|
}
|
|
|
|
fmt.Printf("🚀 Scaffolding %s client '%s'...\n", resultLabel, projectDir)
|
|
|
|
if isVue {
|
|
err = scaffoldClientVue(projectDir, serviceName)
|
|
} else {
|
|
err = scaffoldClientNuxt(projectDir, serviceName)
|
|
}
|
|
if err != nil {
|
|
fmt.Printf("error scaffolding client: %v", err)
|
|
return
|
|
}
|
|
|
|
fmt.Println("✅ Done!")
|
|
}
|
|
|
|
func scaffoldClientNuxt(projectDir, serviceName string) error {
|
|
if err := runCommand("", "npx", "nuxi@latest", "init", projectDir,
|
|
"--no-install",
|
|
"--packageManager=pnpm",
|
|
"-t", "ui",
|
|
"--gitInit=false",
|
|
"-M", "@pinia/nuxt",
|
|
); err != nil {
|
|
return fmt.Errorf("error running nuxi init: %w", err)
|
|
}
|
|
|
|
replacements := map[string]string{
|
|
"App Template": serviceName,
|
|
}
|
|
if err := copyEmbeddedDir("embedded/client-nuxt", projectDir, replacements); err != nil {
|
|
return fmt.Errorf("error copying client overlay: %w", err)
|
|
}
|
|
|
|
if err := patchNuxtConfig(projectDir, serviceName); err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Println("📦 Installing dependencies...")
|
|
return runCommand(projectDir, "pnpm", "add", "pinia", "@pinia/nuxt")
|
|
}
|
|
|
|
func patchNuxtConfig(projectDir, serviceName string) error {
|
|
configPath := filepath.Join(projectDir, "nuxt.config.ts")
|
|
content, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
return fmt.Errorf("error reading nuxt.config.ts: %w", err)
|
|
}
|
|
|
|
insertion := fmt.Sprintf(`
|
|
app: {
|
|
head: {
|
|
title: %q,
|
|
meta: [{ name: 'description', content: %q }],
|
|
},
|
|
},
|
|
|
|
runtimeConfig: {
|
|
apiInternalUrl: '',
|
|
public: {
|
|
baseUrl: '',
|
|
},
|
|
},
|
|
`, serviceName, serviceName)
|
|
|
|
newContent := strings.Replace(string(content), "defineNuxtConfig({", "defineNuxtConfig({"+insertion, 1)
|
|
if newContent == string(content) {
|
|
return fmt.Errorf("could not find defineNuxtConfig({ anchor in nuxt.config.ts")
|
|
}
|
|
|
|
return os.WriteFile(configPath, []byte(newContent), 0644)
|
|
}
|
|
|
|
func scaffoldClientVue(projectDir, serviceName string) error {
|
|
parentDir := filepath.Dir(projectDir)
|
|
leafName := filepath.Base(projectDir)
|
|
if err := os.MkdirAll(parentDir, 0755); err != nil {
|
|
return fmt.Errorf("error creating parent directory: %w", err)
|
|
}
|
|
if err := runCommand(parentDir, "pnpm", "create", "vue@latest", leafName,
|
|
"--ts", "--router", "--pinia", "--eslint", "--bare",
|
|
); err != nil {
|
|
return fmt.Errorf("error running create-vue: %w", err)
|
|
}
|
|
|
|
version, err := pnpmVersion()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := patchPackageManagerField(projectDir, version); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := os.WriteFile(
|
|
filepath.Join(projectDir, "pnpm-workspace.yaml"),
|
|
[]byte("allowBuilds:\n vue-demi: true\n"),
|
|
0644,
|
|
); err != nil {
|
|
return fmt.Errorf("error writing pnpm-workspace.yaml: %w", err)
|
|
}
|
|
|
|
fmt.Println("📦 Installing base dependencies...")
|
|
if err := runCommand(projectDir, "pnpm", "install"); err != nil {
|
|
return fmt.Errorf("error running pnpm install: %w", err)
|
|
}
|
|
|
|
if err := runCommand(projectDir, "pnpm", "add", "tailwindcss", "@tailwindcss/vite"); err != nil {
|
|
return fmt.Errorf("error adding tailwindcss: %w", err)
|
|
}
|
|
|
|
assetsDir := filepath.Join(projectDir, "src", "assets")
|
|
if err := os.MkdirAll(assetsDir, 0755); err != nil {
|
|
return fmt.Errorf("error creating src/assets: %w", err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(assetsDir, "main.css"), []byte("@import \"tailwindcss\";\n"), 0644); err != nil {
|
|
return fmt.Errorf("error writing main.css stub: %w", err)
|
|
}
|
|
|
|
if err := patchViteConfigForTailwind(projectDir); err != nil {
|
|
return err
|
|
}
|
|
if err := patchTsconfigAlias(projectDir); err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Println("🎨 Setting up shadcn-vue...")
|
|
if err := runCommand(projectDir, "pnpm", "dlx", "shadcn-vue@latest", "init",
|
|
"-t", "vite", "-b", "neutral", "--icon-library", "lucide", "-y", "-d",
|
|
); err != nil {
|
|
return fmt.Errorf("error running shadcn-vue init: %w", err)
|
|
}
|
|
|
|
if err := runCommand(projectDir, "pnpm", "dlx", "shadcn-vue@latest", "add",
|
|
"button", "input", "label", "select", "-y",
|
|
); err != nil {
|
|
return fmt.Errorf("error adding shadcn-vue components: %w", err)
|
|
}
|
|
|
|
if err := patchMainCssTokens(projectDir); err != nil {
|
|
return err
|
|
}
|
|
if err := patchEslintConfig(projectDir); err != nil {
|
|
return err
|
|
}
|
|
|
|
replacements := map[string]string{
|
|
"App Template": serviceName,
|
|
}
|
|
if err := copyEmbeddedDir("embedded/client-vue", projectDir, replacements); err != nil {
|
|
return fmt.Errorf("error copying client overlay: %w", err)
|
|
}
|
|
|
|
fmt.Println("📦 Installing remaining dependencies...")
|
|
return runCommand(projectDir, "pnpm", "add", "ofetch", "vue-sonner")
|
|
}
|
|
|
|
func patchPackageManagerField(projectDir, version string) error {
|
|
pkgPath := filepath.Join(projectDir, "package.json")
|
|
content, err := os.ReadFile(pkgPath)
|
|
if err != nil {
|
|
return fmt.Errorf("error reading package.json: %w", err)
|
|
}
|
|
|
|
newContent := strings.Replace(string(content), "{",
|
|
fmt.Sprintf("{\n \"packageManager\": \"pnpm@%s\",", version), 1)
|
|
if newContent == string(content) {
|
|
return fmt.Errorf("could not find opening brace in package.json")
|
|
}
|
|
|
|
return os.WriteFile(pkgPath, []byte(newContent), 0644)
|
|
}
|
|
|
|
func patchViteConfigForTailwind(projectDir string) error {
|
|
configPath := filepath.Join(projectDir, "vite.config.ts")
|
|
content, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
return fmt.Errorf("error reading vite.config.ts: %w", err)
|
|
}
|
|
|
|
newContent := strings.Replace(string(content),
|
|
"import vueDevTools from 'vite-plugin-vue-devtools'",
|
|
"import vueDevTools from 'vite-plugin-vue-devtools'\nimport tailwindcss from '@tailwindcss/vite'",
|
|
1,
|
|
)
|
|
newContent = strings.Replace(newContent, "vueDevTools(),", "vueDevTools(),\n tailwindcss(),", 1)
|
|
|
|
if newContent == string(content) {
|
|
return fmt.Errorf("could not find expected anchors in vite.config.ts")
|
|
}
|
|
|
|
return os.WriteFile(configPath, []byte(newContent), 0644)
|
|
}
|
|
|
|
func patchTsconfigAlias(projectDir string) error {
|
|
configPath := filepath.Join(projectDir, "tsconfig.json")
|
|
content, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
return fmt.Errorf("error reading tsconfig.json: %w", err)
|
|
}
|
|
|
|
aliasBlock := ` ],
|
|
"compilerOptions": {
|
|
"baseUrl": ".",
|
|
"paths": {
|
|
"@/*": ["./src/*"]
|
|
}
|
|
}
|
|
}`
|
|
newContent := strings.Replace(string(content), " ]\n}", aliasBlock, 1)
|
|
if newContent == string(content) {
|
|
return fmt.Errorf("could not find expected anchor in tsconfig.json")
|
|
}
|
|
|
|
return os.WriteFile(configPath, []byte(newContent), 0644)
|
|
}
|
|
|
|
func patchMainCssTokens(projectDir string) error {
|
|
cssPath := filepath.Join(projectDir, "src", "assets", "main.css")
|
|
content, err := os.ReadFile(cssPath)
|
|
if err != nil {
|
|
return fmt.Errorf("error reading main.css: %w", err)
|
|
}
|
|
s := string(content)
|
|
|
|
s2 := strings.Replace(s,
|
|
"--color-background: var(--background);",
|
|
"--color-background: var(--background);\n --color-success: var(--success);\n --color-success-foreground: var(--success-foreground);\n --color-warning: var(--warning);\n --color-warning-foreground: var(--warning-foreground);",
|
|
1,
|
|
)
|
|
if s2 == s {
|
|
return fmt.Errorf("could not find @theme anchor in main.css")
|
|
}
|
|
|
|
s3 := strings.Replace(s2,
|
|
"}\n\n.dark {",
|
|
" --success: oklch(0.6 0.15 145);\n --success-foreground: oklch(0.985 0 0);\n --warning: oklch(0.75 0.15 80);\n --warning-foreground: oklch(0.145 0 0);\n}\n\n.dark {",
|
|
1,
|
|
)
|
|
if s3 == s2 {
|
|
return fmt.Errorf("could not find :root close anchor in main.css")
|
|
}
|
|
|
|
s4 := strings.Replace(s3,
|
|
"}\n\n@layer base {",
|
|
" --success: oklch(0.65 0.15 145);\n --success-foreground: oklch(0.145 0 0);\n --warning: oklch(0.8 0.15 80);\n --warning-foreground: oklch(0.145 0 0);\n}\n\n@layer base {",
|
|
1,
|
|
)
|
|
if s4 == s3 {
|
|
return fmt.Errorf("could not find .dark close anchor in main.css")
|
|
}
|
|
|
|
return os.WriteFile(cssPath, []byte(s4), 0644)
|
|
}
|
|
|
|
func patchEslintConfig(projectDir string) error {
|
|
configPath := filepath.Join(projectDir, "eslint.config.ts")
|
|
content, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
return fmt.Errorf("error reading eslint.config.ts: %w", err)
|
|
}
|
|
|
|
override := ` {
|
|
name: 'app/rules-overrides',
|
|
rules: {
|
|
'vue/multi-word-component-names': 'off',
|
|
'@typescript-eslint/no-explicit-any': 'off',
|
|
},
|
|
},
|
|
|
|
...pluginOxlint.buildFromOxlintConfigFile('.oxlintrc.json'),`
|
|
|
|
newContent := strings.Replace(string(content),
|
|
" ...pluginOxlint.buildFromOxlintConfigFile('.oxlintrc.json'),",
|
|
override,
|
|
1,
|
|
)
|
|
if newContent == string(content) {
|
|
return fmt.Errorf("could not find expected anchor in eslint.config.ts")
|
|
}
|
|
|
|
return os.WriteFile(configPath, []byte(newContent), 0644)
|
|
}
|