Make generated Vue monorepos buildable with docker compose #9

Merged
FernandoJVideira merged 1 commits from fix/vue-monorepo-docker into main 2026-09-16 12:27:07 +00:00
4 changed files with 28 additions and 4 deletions

No files matched your search

+1 -1
View File
@@ -85,7 +85,7 @@ draft new client
3. Scaffolds the base app via the official tooling (`nuxi init` for Nuxt, `create-vue` for Vue), so the generated boilerplate, dependency versions, and lockfile are always current rather than pinned by Draft 3. Scaffolds the base app via the official tooling (`nuxi init` for Nuxt, `create-vue` for Vue), so the generated boilerplate, dependency versions, and lockfile are always current rather than pinned by Draft
4. Layers a Draft-maintained overlay on top: an auth store (login/logout/register/token refresh), route guards, API composable, and pages for login/signup/profile/organizations/email verification/invites — all wired up to talk to a Draft-generated API out of the box 4. Layers a Draft-maintained overlay on top: an auth store (login/logout/register/token refresh), route guards, API composable, and pages for login/signup/profile/organizations/email verification/invites — all wired up to talk to a Draft-generated API out of the box
Both frameworks produce a comparable app: [`vue-ui-kit`](https://git.fernandovideira.com/FernandoJVideira/vue-ui-kit) (installed from its own Gitea npm registry - see its README for the one-time `.npmrc` token setup) + Tailwind, the same component library and theme tokens on either framework. The generated Nuxt client's Dockerfile mounts that `~/.npmrc` as a build secret (wired up in the monorepo's `docker-compose.yml`), so container builds resolve the package without baking the token into the image. Both frameworks produce a comparable app: [`vue-ui-kit`](https://git.fernandovideira.com/FernandoJVideira/vue-ui-kit) (installed from its own Gitea npm registry - see its README for the one-time `.npmrc` token setup) + Tailwind, the same component library and theme tokens on either framework. Both clients ship with a Dockerfile that mounts that `~/.npmrc` as a build secret (wired up in the monorepo's `docker-compose.yml`), so container builds resolve the package without baking the token into the image - Nuxt runs its Nitro server, Vue is served by nginx with an SPA fallback and `/api/` proxied to the api container.
### Create a New Monorepo Project ### Create a New Monorepo Project
+23
View File
@@ -153,6 +153,19 @@ func patchNuxtConfig(projectDir, serviceName string) error {
return os.WriteFile(configPath, []byte(finalContent), 0644) return os.WriteFile(configPath, []byte(finalContent), 0644)
} }
func insidePnpmWorkspace(dir string) bool {
for {
if _, err := os.Stat(filepath.Join(dir, "pnpm-workspace.yaml")); err == nil {
return true
}
parent := filepath.Dir(dir)
if parent == dir {
return false
}
dir = parent
}
}
func scaffoldClientVue(projectDir, serviceName string) error { func scaffoldClientVue(projectDir, serviceName string) error {
parentDir := filepath.Dir(projectDir) parentDir := filepath.Dir(projectDir)
leafName := filepath.Base(projectDir) leafName := filepath.Base(projectDir)
@@ -173,6 +186,12 @@ func scaffoldClientVue(projectDir, serviceName string) error {
return err return err
} }
// A standalone client is its own workspace root and needs vue-demi's build
// script approved. Inside a monorepo the root pnpm-workspace.yaml already
// does that - and writing one here would make apps/client a separate
// workspace, so the installs below would update a nested lockfile and
// leave the root one describing the template's client instead of this one.
if !insidePnpmWorkspace(parentDir) {
if err := os.WriteFile( if err := os.WriteFile(
filepath.Join(projectDir, "pnpm-workspace.yaml"), filepath.Join(projectDir, "pnpm-workspace.yaml"),
[]byte("allowBuilds:\n vue-demi: true\n"), []byte("allowBuilds:\n vue-demi: true\n"),
@@ -180,6 +199,7 @@ func scaffoldClientVue(projectDir, serviceName string) error {
); err != nil { ); err != nil {
return fmt.Errorf("error writing pnpm-workspace.yaml: %w", err) return fmt.Errorf("error writing pnpm-workspace.yaml: %w", err)
} }
}
fmt.Println("📦 Installing base dependencies...") fmt.Println("📦 Installing base dependencies...")
if err := runCommand(projectDir, "pnpm", "install"); err != nil { if err := runCommand(projectDir, "pnpm", "install"); err != nil {
@@ -210,6 +230,9 @@ func scaffoldClientVue(projectDir, serviceName string) error {
replacements := map[string]string{ replacements := map[string]string{
"App Template": serviceName, "App Template": serviceName,
// The overlay's Dockerfile pins the pnpm that produced the template's
// lockfile; the generated project's lockfile comes from the local pnpm.
"[email protected]": "pnpm@" + version,
} }
if err := copyEmbeddedDir("embedded/client-vue", projectDir, replacements); err != nil { if err := copyEmbeddedDir("embedded/client-vue", projectDir, replacements); err != nil {
return fmt.Errorf("error copying client overlay: %w", err) return fmt.Errorf("error copying client overlay: %w", err)
+3 -2
View File
@@ -52,11 +52,12 @@ var jobs = []syncJob{
"src/stores/", "src/stores/",
"src/composables/", "src/composables/",
"src/lib/api-client.ts", "src/lib/api-client.ts",
"src/components/AppBrand.vue", "src/components/",
"src/components/AppLayout.vue",
"src/views/", "src/views/",
"src/shared/", "src/shared/",
".env.example", ".env.example",
"Dockerfile",
"nginx.conf.template",
}, },
}, },
} }