Public Access
feat: scaffold API, client, or monorepo projects with Nuxt/Vue client support
- 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
This commit is contained in:
1 parent
fe48b7caee
commit
29f94539e0
7 files changed
+724
-99
No files matched your search
+321
@@ -0,0 +1,321 @@
|
||||
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)
|
||||
}
|
||||
Reference in new issue
Block a user