11 Commits
Author SHA1 Message Date
FernandoJVideira 8a11de5b0f Merge pull request 'Scaffold generated clients with @lucide/vue' (#11) from chore/lucide-vue into main
Reviewed-on: #11
2026-10-08 22:55:31 +00:00
FernandoJVideiraandClaude Sonnet 5.5 b7fe8f3f4e Scaffold generated clients with @lucide/vue
The template overlay now imports its icons from @lucide/vue (project-template#6),
so the scaffolder has to install that instead of the deprecated
lucide-vue-next: pnpm doesn't hoist the kit's own copy to the app, and a
fresh project would fail to resolve its icon imports. No version is pinned
here - these stay unpinned `pnpm add` calls, so the template remains the
version authority.

Submodule bumped to that commit.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-08 23:54:00 +01:00
FernandoJVideira 0bd1550779 Merge pull request 'Generated clients: exclude vue-ui-kit from Vite pre-bundling, /signup route' (#10) from fix/generated-client-configs into main
Reviewed-on: #10
2026-09-16 12:58:56 +00:00
FernandoJVideiraandClaude Fable 5.1 1dcc135094 Generated clients: exclude vue-ui-kit from Vite pre-bundling, /signup route
Generated projects never got the optimizeDeps exclusion the template's
own configs carry - the Nuxt config is patched from nuxi's output and the
Vue one from create-vue's - so Vite bundled vue-ui-kit's index.ts with
reka-ui inlined while the kit's .vue files imported reka-ui from source.
Two module instances, mismatched provide/inject Symbols: a fresh Vue
client rendered a blank /login ("Injection Symbol(TabsRootContext) not
found") and a fresh Nuxt one crashed SSR on any page with a Dialog. Both
config patches now add the exclusion.

The Nuxt patch also adds the pages:extend hook that gives pages/login.vue
a second /signup record, matching the template's auth card whose tabs now
drive the URL (project-template#5). Submodule bumped to that commit.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-16 13:58:23 +01:00
FernandoJVideira 39678f3ea6 Merge pull request 'Make generated Vue monorepos buildable with docker compose' (#9) from fix/vue-monorepo-docker into main
Reviewed-on: #9
2026-09-16 12:27:07 +00:00
FernandoJVideiraandClaude Fable 5.1 230037c5f5 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 <[email protected]>
2026-09-16 13:22:37 +01:00
FernandoJVideira 1195fdacb5 Merge pull request 'v0.2.0: ship the client Dockerfile with generated Nuxt apps' (#8) from chore/v0.2.0 into main
Reviewed-on: #8
2026-09-16 12:09:31 +00:00
FernandoJVideiraandClaude Fable 5.1 2f1ad93a74 Ship the client Dockerfile with generated Nuxt apps; bump to v0.2.0
A generated monorepo's docker-compose.yml already built apps/client from
apps/client/Dockerfile, but the client overlay only embedded app/ and
public/, so the file never existed and `docker compose build client`
failed on every fresh project. The client-nuxt sync job now embeds the
template's Dockerfile too, and the Nuxt scaffold rewrites its pnpm pin to
the locally detected version - the one that produces the generated
project's lockfile - the same way it already stamps packageManager.

Bumps the submodule to the template commit that mounts ~/.npmrc as a
build secret for the registry token, and the Makefile VERSION to v0.2.0
for the release.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-16 13:08:18 +01:00
FernandoJVideira b3ea1013bc Merge pull request 'Release: vue-ui-kit client scaffolding' (#7) from dev into main
Reviewed-on: #7
2026-09-16 11:57:52 +00:00
FernandoJVideira 4806186ae6 Merge pull request 'Dev' (#5) from dev into main
Reviewed-on: #5
2026-08-25 21:03:41 +00:00
FernandoJVideira dff28b1a4c Merge pull request 'Version 0.1.0' (#3) from dev into main
Reviewed-on: #3
2026-08-25 20:42:49 +00:00
5 changed files with 83 additions and 15 deletions

No files matched your search

+1 -1
View File
@@ -1,5 +1,5 @@
PKG_NAME := git.fernandovideira.com/FernandoJVideira/draft
VERSION := v0.1.0
VERSION := v0.2.0
GO_SOURCES := $(shell find . -name '*.go' -not -path './assets/testdata/*')
.PHONY: version build windows win-arm linux mac prepare-embed clean
+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
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.
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
+76 -9
View File
@@ -68,8 +68,15 @@ func scaffoldClientNuxt(projectDir, serviceName string) error {
return fmt.Errorf("error running nuxi init: %w", err)
}
version, err := pnpmVersion()
if err != nil {
return fmt.Errorf("error detecting pnpm version: %w", err)
}
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.
"[email protected]": "pnpm@" + version,
}
if err := copyEmbeddedDir("embedded/client-nuxt", projectDir, replacements); err != nil {
return fmt.Errorf("error copying client overlay: %w", err)
@@ -81,7 +88,7 @@ func scaffoldClientNuxt(projectDir, serviceName string) error {
fmt.Println("📦 Installing dependencies...")
if err := runCommand(projectDir, "pnpm", "add", "pinia", "@pinia/nuxt",
"@fernandojvideira/vue-ui-kit", "lucide-vue-next",
"@fernandojvideira/vue-ui-kit", "@lucide/vue",
); err != nil {
return fmt.Errorf("error installing dependencies: %w", err)
}
@@ -121,6 +128,25 @@ func patchNuxtConfig(projectDir, serviceName string) error {
css: ['~/assets/css/main.css'],
vite: {
plugins: [tailwindcss()],
// vue-ui-kit ships raw .vue/.ts source. Left to Vite's dependency
// pre-bundler, its index.ts gets bundled with reka-ui inlined while its
// .vue files import reka-ui from source - two module instances, so
// reka-ui's provide/inject contexts (Dialog, Tabs, Select...) can't find
// each other. Excluding it keeps every import on the one reka-ui.
optimizeDeps: {
exclude: ['@fernandojvideira/vue-ui-kit'],
},
},
hooks: {
// /login and /signup are the two tabs of one auth card, served by the
// same page file. A second route record (not an alias - the router
// would treat switching between a route and its alias as a duplicate
// navigation and drop it) lets the tab change the URL while NuxtPage
// keeps the same component instance thanks to the page's fixed key.
'pages:extend'(pages) {
const login = pages.find((page) => page.path === '/login')
if (login) pages.push({ ...login, name: 'signup', path: '/signup' })
},
},
app: {
@@ -146,6 +172,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)
@@ -166,12 +205,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...")
@@ -203,13 +249,16 @@ 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.
"[email protected]": "pnpm@" + version,
}
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", "@fernandojvideira/vue-ui-kit", "lucide-vue-next")
return runCommand(projectDir, "pnpm", "add", "ofetch", "@fernandojvideira/vue-ui-kit", "@lucide/vue")
}
// The library's theme brings its own tokens; --success/--warning are added on
@@ -269,13 +318,31 @@ func patchViteConfigForTailwind(projectDir string) error {
)
newContent = strings.Replace(newContent, "vueDevTools(),", "vueDevTools(),\n tailwindcss(),", 1)
if newContent == string(content) {
// create-vue's config ends with the resolve.alias block; hang the
// optimizeDeps exclusion off its closing brace.
newContent = strings.Replace(newContent,
" '@': fileURLToPath(new URL('./src', import.meta.url)),\n },\n },\n})",
" '@': fileURLToPath(new URL('./src', import.meta.url)),\n },\n },\n"+viteOptimizeDepsExclude+"})",
1,
)
if newContent == string(content) || !strings.Contains(newContent, "optimizeDeps") {
return fmt.Errorf("could not find expected anchors in vite.config.ts")
}
return os.WriteFile(configPath, []byte(newContent), 0644)
}
// vue-ui-kit ships raw .vue/.ts source. Left to Vite's dependency pre-bundler,
// its index.ts gets bundled with reka-ui inlined while its .vue files import
// reka-ui from source - two module instances, so reka-ui's provide/inject
// contexts (Dialog, Tabs, Select...) can't find each other. Excluding it keeps
// every import on the one reka-ui.
const viteOptimizeDepsExclude = ` optimizeDeps: {
exclude: ['@fernandojvideira/vue-ui-kit'],
},
`
func patchTsconfigAlias(projectDir string) error {
configPath := filepath.Join(projectDir, "tsconfig.json")
content, err := os.ReadFile(configPath)
+4 -3
View File
@@ -39,7 +39,7 @@ var jobs = []syncJob{
templateName: "client-nuxt",
sourceDir: "assets/testdata/template/apps/client",
targetDir: "assets/embedded/client-nuxt",
includePrefixes: []string{"app/", "public/"},
includePrefixes: []string{"app/", "public/", "Dockerfile"},
},
{
templateName: "client-vue",
@@ -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",
},
},
}