{"version":3,"file":"fullstack-DUgRUss6.mjs","names":[],"sources":["../src/generators/fullstack.ts"],"sourcesContent":["// `kick new --template fullstack` — a pnpm-workspace monorepo:\n//\n//   my-app/\n//     server/   KickJS API (the standard scaffold, minus its own git/install)\n//     web/      Vite + React frontend typed end-to-end via\n//               @forinda/kickjs-client + the server's generated KickRoutes.Api\n//\n// The type loop: server controllers use return-value handlers → `kick\n// typegen` (run here once, re-run by `kick dev`) emits\n// server/.kickjs/types/kick__routes.ts → web/src/types/kick-routes.d.ts\n// side-effect-imports that file (type-only, erased at runtime) → the web\n// client calls `api.get('/hello')` with the handler's actual response type.\n\nimport { join } from 'node:path'\nimport { execFileSync } from 'node:child_process'\n\nimport { writeFileSafe } from '../utils/fs'\nimport { runCommand } from '../utils/shell'\nimport { initProject, resolveSiblingVersions } from './project'\n\nexport interface InitFullstackOptions {\n  name: string\n  directory: string\n  packageManager?: 'pnpm' | 'npm' | 'yarn' | 'bun'\n  initGit?: boolean\n  installDeps?: boolean\n  schemaLib?: 'zod' | 'valibot' | 'yup'\n  runtime?: 'express' | 'fastify' | 'h3'\n}\n\nexport async function initFullstackProject(options: InitFullstackOptions): Promise<void> {\n  const { name, directory, schemaLib = 'zod', runtime = 'express' } = options\n  // `--pm` arrives as a free CLI string — allowlist before it reaches a\n  // process invocation (execFileSync takes an argv array, no shell, but a\n  // bogus binary name is still a confusing failure).\n  const pmValid = (['pnpm', 'npm', 'yarn', 'bun'] as const).includes(\n    options.packageManager as never,\n  )\n  const packageManager = pmValid\n    ? (options.packageManager as 'pnpm' | 'npm' | 'yarn' | 'bun')\n    : 'pnpm'\n  const dir = directory\n  const log = (msg: string) => console.log(`  ${msg}`)\n  if (options.packageManager !== undefined && !pmValid) {\n    log(\n      `Warning: unknown package manager '${String(options.packageManager)}' — falling back to pnpm.`,\n    )\n  }\n\n  console.log(`\\n  Creating fullstack KickJS workspace: ${name}\\n`)\n\n  // ── server/ — the standard scaffold, deferred install/git ──────────\n  await initProject({\n    name: `${name}-server`,\n    directory: join(dir, 'server'),\n    packageManager,\n    template: 'minimal',\n    schemaLib,\n    runtime,\n    // Root owns install + git so the lockfile/commit cover the workspace.\n    initGit: false,\n    installDeps: false,\n    // Production single-origin serving. `web/` builds to `web/dist`, and\n    // nothing served it before — `pnpm build` produced a frontend the API had\n    // no way to hand out, so every deploy needed a hand-wired static host or\n    // a second process. The adapter is inert until `../web/dist` exists, so\n    // `dev` (Vite serves the client and proxies /api here) is untouched.\n    spaClientDir: '../web/dist',\n  })\n\n  // ── web/ — Vite + React, typed client ──────────────────────────────\n  const versions = await resolveSiblingVersions()\n  const clientVersion = versions['@forinda/kickjs-client'] ?? '^0.1.0'\n\n  await writeFileSafe(join(dir, 'web/package.json'), webPackageJson(name, clientVersion))\n  await writeFileSafe(join(dir, 'web/vite.config.ts'), webViteConfig())\n  await writeFileSafe(join(dir, 'web/tsconfig.json'), webTsConfig())\n  await writeFileSafe(join(dir, 'web/index.html'), webIndexHtml(name))\n  await writeFileSafe(join(dir, 'web/src/main.tsx'), webMain())\n  await writeFileSafe(join(dir, 'web/src/App.tsx'), webApp())\n  await writeFileSafe(join(dir, 'web/src/api.ts'), webApi())\n  await writeFileSafe(join(dir, 'web/src/types/kick-routes.d.ts'), webRouteTypes())\n\n  // ── workspace root ──────────────────────────────────────────────────\n  await writeFileSafe(join(dir, 'package.json'), rootPackageJson(name, packageManager))\n  // Only pnpm reads this file. npm / yarn / bun declare workspaces via the\n  // `workspaces` field in the root package.json instead — see rootPackageJson.\n  if (packageManager === 'pnpm') {\n    await writeFileSafe(join(dir, 'pnpm-workspace.yaml'), `packages:\\n  - server\\n  - web\\n`)\n  }\n  await writeFileSafe(join(dir, '.gitignore'), rootGitignore())\n  await writeFileSafe(join(dir, 'README.md'), rootReadme(name, packageManager))\n\n  // Workspace-root agent docs (CLAUDE.md + .agents/) flavored for the\n  // fullstack layout — the server/ subdir keeps its own generated set.\n  const { generateAgentDocs } = await import('./agent-docs')\n  await generateAgentDocs({\n    outDir: dir,\n    name,\n    pm: packageManager,\n    template: 'fullstack',\n    only: 'all',\n    force: true,\n  })\n\n  // ── install (root — covers both workspace packages) ────────────────\n  if (options.installDeps) {\n    console.log(`\\n  Installing workspace dependencies with ${packageManager}...\\n`)\n    try {\n      // `runCommand`, not bare execFileSync — on Windows every package\n      // manager is a `.cmd` shim that execFileSync cannot spawn.\n      runCommand(packageManager, ['install'], { cwd: dir })\n    } catch {\n      console.log(`\\n  Warning: ${packageManager} install failed. Run it manually.`)\n    }\n  }\n\n  // ── typegen (server) so web's KickRoutes.Api resolves immediately ──\n  try {\n    const { runTypegen } = await import('../typegen')\n    await runTypegen({ cwd: join(dir, 'server'), allowDuplicates: true, silent: true })\n  } catch {\n    // Non-fatal — `kick dev` in server/ retries on boot.\n  }\n\n  // ── git (root) ──────────────────────────────────────────────────────\n  if (options.initGit) {\n    try {\n      execFileSync('git', ['init'], { cwd: dir, stdio: 'pipe' })\n      execFileSync('git', ['branch', '-M', 'main'], { cwd: dir, stdio: 'pipe' })\n      execFileSync('git', ['add', '-A'], { cwd: dir, stdio: 'pipe' })\n      execFileSync('git', ['commit', '-m', 'chore: initial commit from kick new (fullstack)'], {\n        cwd: dir,\n        stdio: 'pipe',\n      })\n      log('Git repository initialized')\n    } catch {\n      log('Warning: git init failed (git may not be installed)')\n    }\n  }\n\n  console.log('\\n  Fullstack workspace scaffolded!')\n  console.log()\n  log('Next steps:')\n  log(`  cd ${name}`)\n  if (!options.installDeps) log(`  ${packageManager} install`)\n  if (packageManager === 'pnpm') {\n    log('  pnpm dev            # runs server (kick dev) + web (vite) together')\n  } else {\n    log(`  ${packageManager} run dev:server   # terminal 1`)\n    log(`  ${packageManager} run dev:web      # terminal 2`)\n  }\n  log('')\n  log('The web app calls the API through @forinda/kickjs-client —')\n  log(\"edit server/src/modules/hello and watch web/src/App.tsx's types follow.\")\n  log('')\n}\n\n// ── web templates ─────────────────────────────────────────────────────\n\nfunction webPackageJson(name: string, clientVersion: string): string {\n  return `${JSON.stringify(\n    {\n      name: `${name}-web`,\n      private: true,\n      version: '0.0.0',\n      type: 'module',\n      scripts: {\n        dev: 'vite',\n        build: 'tsc --noEmit && vite build',\n        preview: 'vite preview',\n        typecheck: 'tsc --noEmit',\n      },\n      dependencies: {\n        '@forinda/kickjs-client': clientVersion,\n        react: '^19.0.0',\n        'react-dom': '^19.0.0',\n      },\n      devDependencies: {\n        '@types/react': '^19.0.0',\n        '@types/react-dom': '^19.0.0',\n        '@vitejs/plugin-react': '^5.0.0',\n        typescript: '^5.9.0',\n        vite: '^7.0.0',\n      },\n    },\n    null,\n    2,\n  )}\\n`\n}\n\nfunction webViteConfig(): string {\n  return `import { defineConfig } from 'vite'\nimport react from '@vitejs/plugin-react'\n\nexport default defineConfig({\n  plugins: [react()],\n  server: {\n    // The KickJS server (kick dev) listens on 3000; the client's baseUrl is\n    // the relative '/api/v1', so the browser hits Vite and Vite forwards.\n    proxy: {\n      '/api': 'http://localhost:3000',\n    },\n  },\n})\n`\n}\n\nfunction webTsConfig(): string {\n  return `${JSON.stringify(\n    {\n      compilerOptions: {\n        target: 'ES2022',\n        lib: ['ES2022', 'DOM', 'DOM.Iterable'],\n        module: 'ESNext',\n        moduleResolution: 'bundler',\n        jsx: 'react-jsx',\n        strict: true,\n        skipLibCheck: true,\n        noEmit: true,\n        isolatedModules: true,\n        // The type bridge pulls server controller sources into this\n        // program — they use legacy TS decorators.\n        experimentalDecorators: true,\n        // The KickRoutes ambient types come from the server's generated\n        // typegen output via src/types/kick-routes.d.ts.\n        types: [],\n      },\n      include: ['src'],\n    },\n    null,\n    2,\n  )}\\n`\n}\n\nfunction webIndexHtml(name: string): string {\n  return `<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>${name}</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n`\n}\n\nfunction webMain(): string {\n  return `import { StrictMode } from 'react'\nimport { createRoot } from 'react-dom/client'\nimport { App } from './App'\n\ncreateRoot(document.getElementById('root')!).render(\n  <StrictMode>\n    <App />\n  </StrictMode>,\n)\n`\n}\n\nfunction webApp(): string {\n  return `import { useEffect, useState } from 'react'\nimport { api } from './api'\n\n// The response types below are INFERRED from the server's handlers —\n// change server/src/modules/hello/hello.service.ts and these types follow\n// on the next \\`kick typegen\\` (automatic under \\`kick dev\\`).\ntype Greeting = Awaited<ReturnType<typeof fetchGreeting>>\n\nfunction fetchGreeting() {\n  return api.get('/hello')\n}\n\nexport function App() {\n  const [greeting, setGreeting] = useState<Greeting | null>(null)\n  const [health, setHealth] = useState<string>('checking…')\n\n  useEffect(() => {\n    fetchGreeting().then(setGreeting).catch(console.error)\n    api\n      .get('/hello/health')\n      .then((h) => setHealth(h.status))\n      .catch(() => setHealth('down'))\n  }, [])\n\n  return (\n    <main style={{ fontFamily: 'system-ui', maxWidth: 640, margin: '4rem auto', padding: '0 1rem' }}>\n      <h1>KickJS fullstack</h1>\n      <p>\n        <strong>{greeting?.message ?? 'loading…'}</strong>\n      </p>\n      <p>\n        Server said hello at <code>{greeting?.timestamp ?? '…'}</code> — health:{' '}\n        <code>{health}</code>\n      </p>\n      <p style={{ color: '#666' }}>\n        This call is typed end to end: <code>api.get('/hello')</code> returns the exact shape\n        <code> HelloService.greet()</code> produces. Rename a field on the server and this file\n        stops compiling.\n      </p>\n    </main>\n  )\n}\n`\n}\n\nfunction webApi(): string {\n  return `import { createClient } from '@forinda/kickjs-client'\n\n// KickApi (alias of KickRoutes.Api) is ambient — populated by server/.kickjs/types (see\n// src/types/kick-routes.d.ts). Keys are module-mount-relative paths;\n// the bootstrap-level '/api/v1' prefix lives here in baseUrl, and the\n// Vite dev proxy forwards it to the KickJS server.\nexport const api = createClient<KickApi>({ baseUrl: '/api/v1' })\n`\n}\n\nfunction webRouteTypes(): string {\n  return `// Type-only bridge to the server's generated route types. The import is\n// erased at build time — no server code ever enters the web bundle.\n// Regenerate with \\`kick typegen\\` in server/ (automatic under \\`kick dev\\`).\nimport '../../../server/.kickjs/types/kick__routes'\n`\n}\n\n// ── root templates ────────────────────────────────────────────────────\n\nfunction rootPackageJson(name: string, pm: string): string {\n  const scripts: Record<string, string> =\n    pm === 'pnpm'\n      ? {\n          dev: 'pnpm --parallel -r run dev',\n          // One origin: the server serves `web/dist` through SpaAdapter.\n          start: 'pnpm --filter ./server run start',\n          'dev:server': 'pnpm --filter ./server dev',\n          'dev:web': 'pnpm --filter ./web dev',\n          build: 'pnpm -r run build',\n          typecheck: 'pnpm -r run typecheck',\n        }\n      : {\n          // cd-based scripts — the one form npm, yarn (classic AND berry),\n          // and bun all run identically; workspace-filter flags differ per\n          // manager (--workspace vs `yarn workspace <name>` vs --filter).\n          start: `cd server && ${pm} run start`,\n          'dev:server': `cd server && ${pm} run dev`,\n          'dev:web': `cd web && ${pm} run dev`,\n          build: `${pm} run build:server && ${pm} run build:web`,\n          'build:server': `cd server && ${pm} run build`,\n          'build:web': `cd web && ${pm} run build`,\n          typecheck: `cd web && ${pm} run typecheck`,\n        }\n  return `${JSON.stringify(\n    {\n      name,\n      private: true,\n      version: '0.0.0',\n      type: 'module',\n      scripts,\n      workspaces: ['server', 'web'],\n    },\n    null,\n    2,\n  )}\\n`\n}\n\nfunction rootGitignore(): string {\n  return `node_modules/\ndist/\n.env\n*.log\n.DS_Store\n`\n}\n\nfunction rootReadme(name: string, pm: string): string {\n  return `# ${name}\n\nFullstack KickJS workspace — typed end to end.\n\n| Package   | What                                                              |\n| --------- | ----------------------------------------------------------------- |\n| \\`server/\\` | KickJS API (decorators, DI, \\`kick dev\\` with typegen watch)       |\n| \\`web/\\`    | Vite + React, typed against the API via \\`@forinda/kickjs-client\\` |\n\n## Develop\n\n\\`\\`\\`bash\n${pm} install\n${pm === 'pnpm' ? 'pnpm dev            # server (kick dev) + web (vite), in parallel' : `${pm} run dev:server   # terminal 1\\n${pm} run dev:web      # terminal 2`}\n\\`\\`\\`\n\nServer: http://localhost:3000 · Web: http://localhost:5173 (Vite proxies \\`/api\\`).\n\n## The type loop\n\n1. Server handlers **return** their payloads (\\`return this.service.greet(...)\\`).\n2. \\`kick typegen\\` (auto under \\`kick dev\\`) emits \\`server/.kickjs/types/kick__routes.ts\\` —\n   including the flat \\`KickRoutes.Api\\` map with inferred response types.\n3. \\`web/src/types/kick-routes.d.ts\\` imports that file type-only.\n4. \\`web/src/api.ts\\`'s \\`createClient<KickRoutes.Api>\\` types every call site.\n\nRename a field in \\`server/src/modules/hello/hello.service.ts\\` → \\`web/src/App.tsx\\`\nstops compiling. That's the point.\n\nDocs: https://kickjs.app/guide/typed-client.html\n`\n}\n"],"mappings":"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"}