From 230037c5f5abba127afb9162d87f88bf5d124225 Mon Sep 17 00:00:00 2001 From: FernandoJVideira <03.pleaser-minster@icloud.com> Date: Wed, 16 Sep 2026 13:22:37 +0100 Subject: [PATCH] Make generated Vue monorepos buildable with docker compose Running `draft new monorepo` with the Vue client and then `docker compose build client` failed three separate ways, each found by actually doing it: - No Dockerfile: the Vue overlay never shipped one. The template now has an nginx-based image (project-template#4) and the client-vue sync job embeds it plus its nginx.conf.template, with the pnpm pin rewritten to the local version like the Nuxt overlay. - Stale root lockfile: the Vue scaffold wrote a pnpm-workspace.yaml into apps/client (to approve vue-demi's build script), which made it its own workspace, so the installs updated a nested lockfile and the root one kept describing the template's Nuxt client - pnpm install --frozen-lockfile then refused it. The file is only written when the client isn't already inside a workspace (standalone `draft new client`). - Missing component: the client-vue job listed components by name, so the AuthCard.vue that Login/Signup import was never embedded. It now embeds src/components/ as a whole. Co-Authored-By: Claude Fable 5.1 --- README.md | 2 +- assets/testdata/template | 2 +- cmd/client.go | 35 +++++++++++++++++++++++++++++------ tools/sync_templates.go | 5 +++-- 4 files changed, 34 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index dcdb51f..0681609 100644 --- a/README.md +++ b/README.md @@ -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 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 diff --git a/assets/testdata/template b/assets/testdata/template index 70067c9..4143dea 160000 --- a/assets/testdata/template +++ b/assets/testdata/template @@ -1 +1 @@ -Subproject commit 70067c968a24f7ffb84c09391980022aeffebd93 +Subproject commit 4143dea0b3e4865f19c771ba3be1598ba371c003 diff --git a/cmd/client.go b/cmd/client.go index 2443118..f048a27 100644 --- a/cmd/client.go +++ b/cmd/client.go @@ -153,6 +153,19 @@ func patchNuxtConfig(projectDir, serviceName string) error { 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 { parentDir := filepath.Dir(projectDir) leafName := filepath.Base(projectDir) @@ -173,12 +186,19 @@ func scaffoldClientVue(projectDir, serviceName string) error { 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) + // 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( + 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...") @@ -210,6 +230,9 @@ func scaffoldClientVue(projectDir, serviceName string) error { replacements := map[string]string{ "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. + "pnpm@11.22.0": "pnpm@" + version, } if err := copyEmbeddedDir("embedded/client-vue", projectDir, replacements); err != nil { return fmt.Errorf("error copying client overlay: %w", err) diff --git a/tools/sync_templates.go b/tools/sync_templates.go index 81c91fa..1eb7c96 100644 --- a/tools/sync_templates.go +++ b/tools/sync_templates.go @@ -52,11 +52,12 @@ var jobs = []syncJob{ "src/stores/", "src/composables/", "src/lib/api-client.ts", - "src/components/AppBrand.vue", - "src/components/AppLayout.vue", + "src/components/", "src/views/", "src/shared/", ".env.example", + "Dockerfile", + "nginx.conf.template", }, }, }