create-win-project 1.3.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +58 -160
  3. package/checks/check-compatibility.js +95 -0
  4. package/{scripts/verify-generated.mjs → checks/check-generated-project.js} +28 -4
  5. package/{scripts/validate-content.mjs → checks/check-library.js} +17 -7
  6. package/checks/check-package.js +15 -0
  7. package/checks/classify-changes.js +29 -0
  8. package/checks/run-compatibility-shard.js +21 -0
  9. package/docs/README.md +20 -0
  10. package/docs/capabilities.md +13 -0
  11. package/docs/compatibility.md +13 -0
  12. package/docs/generated-project.md +12 -0
  13. package/docs/getting-started.md +26 -0
  14. package/docs/maintainers/architecture.md +161 -0
  15. package/docs/maintainers/ci-strategy.md +46 -0
  16. package/docs/{CONTENT_MODEL.md → maintainers/content-model.md} +3 -3
  17. package/docs/maintainers/contributing.md +154 -0
  18. package/docs/{DEPENDENCY_MAINTENANCE.md → maintainers/dependencies.md} +1 -1
  19. package/docs/migration-v2.md +13 -0
  20. package/docs/production-contract.md +24 -0
  21. package/index.js +2 -429
  22. package/{playbooks → library}/INDEX.md +5 -5
  23. package/library/compatibility-impact.json +14 -0
  24. package/{playbooks/capabilities/ci/github-actions.manifest.json → library/development-tools/ci/definition.json} +1 -0
  25. package/library/development-tools/devops/makefile/commands.md +11 -0
  26. package/library/development-tools/devops/makefile/definition.json +11 -0
  27. package/library/development-tools/devops/makefile/docker.md +9 -0
  28. package/library/development-tools/devops/makefile/validation.md +5 -0
  29. package/{playbooks/devops/pr-template.manifest.json → library/development-tools/devops/pr-template/definition.json} +1 -0
  30. package/library/features/sqlalchemy-alembic.md +17 -0
  31. package/library/optional-features/concerns/zod/errors.md +5 -0
  32. package/library/optional-features/concerns/zod/testing.md +5 -0
  33. package/library/optional-features/concerns/zod/validation.md +21 -0
  34. package/library/optional-features/styling/css-modules/definition.json +12 -0
  35. package/library/optional-features/styling/css-modules/responsive.md +5 -0
  36. package/library/optional-features/styling/css-modules/theme.md +7 -0
  37. package/{playbooks/platform/mobile.manifest.json → library/platforms/mobile/definition.json} +1 -0
  38. package/{playbooks/platform/web.manifest.json → library/platforms/web/definition.json} +1 -0
  39. package/{playbooks/stack/react-native.manifest.json → library/stacks/expo/definition.json} +2 -2
  40. package/library/stacks/fastapi/architecture.md +40 -0
  41. package/library/stacks/fastapi/definition.json +39 -0
  42. package/library/stacks/fastapi/runtime.md +25 -0
  43. package/library/stacks/fastapi/security.md +26 -0
  44. package/library/stacks/fastapi/structure.md +27 -0
  45. package/library/stacks/fastapi/testing.md +23 -0
  46. package/{playbooks/stack/nextjs.manifest.json → library/stacks/nextjs/definition.json} +2 -2
  47. package/{playbooks/stack/no-frontend.manifest.json → library/stacks/no-frontend/definition.json} +1 -1
  48. package/{playbooks/stack/react-vite.manifest.json → library/stacks/react-vite/definition.json} +2 -2
  49. package/{compatibility/profiles.json → library/tested-versions.json} +46 -2
  50. package/{playbooks/universal/accessibility.manifest.json → library/universal/accessibility/definition.json} +1 -1
  51. package/library/universal/coding-rules/definition.json +13 -0
  52. package/library/universal/coding-rules/hygiene.md +9 -0
  53. package/library/universal/coding-rules/naming.md +17 -0
  54. package/{playbooks/universal/error-handling.manifest.json → library/universal/error-handling/definition.json} +1 -0
  55. package/library/universal/git-conventions/branches.md +5 -0
  56. package/library/universal/git-conventions/commits.md +7 -0
  57. package/library/universal/git-conventions/definition.json +13 -0
  58. package/library/universal/git-conventions/workflow.md +5 -0
  59. package/{playbooks/universal/observability.manifest.json → library/universal/observability/definition.json} +1 -0
  60. package/{playbooks/universal/security.manifest.json → library/universal/security/definition.json} +1 -0
  61. package/library/universal/typescript/boundaries.md +13 -0
  62. package/library/universal/typescript/definition.json +12 -0
  63. package/library/universal/typescript/errors.md +5 -0
  64. package/library/universal/typescript/patterns.md +7 -0
  65. package/package.json +14 -15
  66. package/public/logo.svg +19 -0
  67. package/src/cli/arguments.js +63 -0
  68. package/src/cli/display.js +220 -0
  69. package/src/cli/main.js +203 -0
  70. package/src/cli/navigation.js +156 -0
  71. package/src/cli/questions.js +188 -0
  72. package/src/cli/system-check.js +158 -0
  73. package/src/engine/create-project.js +10 -0
  74. package/src/engine/install-dependencies.js +13 -0
  75. package/{lib/catalog.js → src/engine/load-library.js} +8 -8
  76. package/{lib/files.js → src/engine/project-files.js} +10 -1
  77. package/{lib/playbooks.js → src/engine/project-guidance.js} +26 -3
  78. package/{lib/application-shapes.js → src/engine/project-shapes.js} +4 -4
  79. package/{lib/template.js → src/engine/render-templates.js} +11 -0
  80. package/{lib/compatibility.js → src/engine/tested-versions.js} +21 -2
  81. package/src/engine/upgrade-report.js +20 -0
  82. package/src/engine/write-files.js +34 -0
  83. package/{lib/stacks/registry.js → src/stacks/available-stacks.js} +25 -1
  84. package/src/stacks/backends/README.md +5 -0
  85. package/src/stacks/backends/fastapi/ci.js +3 -0
  86. package/src/stacks/backends/fastapi/create-files.js +874 -0
  87. package/src/stacks/backends/fastapi/docker.js +75 -0
  88. package/src/stacks/backends/fastapi/environment.js +3 -0
  89. package/src/stacks/backends/fastapi/index.js +32 -0
  90. package/{lib/stacks → src/stacks/backends}/laravel/architecture.js +1 -0
  91. package/{lib/stacks → src/stacks/backends}/laravel/auth/oidc.js +1 -0
  92. package/{lib/stacks → src/stacks/backends}/laravel/auth/public.js +1 -0
  93. package/{lib/stacks → src/stacks/backends}/laravel/auth/sanctum.js +1 -0
  94. package/{lib/stacks → src/stacks/backends}/laravel/auth/session.js +1 -0
  95. package/src/stacks/backends/laravel/ci.js +3 -0
  96. package/{lib/stacks → src/stacks/backends}/laravel/composer.js +1 -1
  97. package/{lib/stacks/laravel/generate.js → src/stacks/backends/laravel/create-files.js} +1 -0
  98. package/src/stacks/backends/laravel/docker.js +14 -0
  99. package/src/stacks/backends/laravel/environment.js +5 -0
  100. package/{lib/stacks → src/stacks/backends}/laravel/index.js +11 -3
  101. package/{lib/stacks → src/stacks/backends}/laravel/ui/blade.js +1 -0
  102. package/{lib/stacks → src/stacks/backends}/laravel/ui/index.js +1 -0
  103. package/{lib/stacks → src/stacks/backends}/laravel/ui/inertia-react.js +4 -2
  104. package/{lib/stacks → src/stacks/backends}/laravel/ui/livewire.js +1 -0
  105. package/{lib/stacks → src/stacks/backends}/laravel/ui/shared.js +1 -0
  106. package/src/stacks/backends/none/index.js +23 -0
  107. package/src/stacks/backends/postgres/ci.js +3 -0
  108. package/src/stacks/backends/postgres/create-files.js +21 -0
  109. package/src/stacks/backends/postgres/docker.js +3 -0
  110. package/src/stacks/backends/postgres/environment.js +3 -0
  111. package/src/stacks/backends/postgres/index.js +30 -0
  112. package/src/stacks/backends/springboot/ci.js +3 -0
  113. package/src/stacks/backends/springboot/create-files.js +91 -0
  114. package/src/stacks/backends/springboot/docker.js +3 -0
  115. package/src/stacks/backends/springboot/environment.js +3 -0
  116. package/src/stacks/backends/springboot/index.js +31 -0
  117. package/src/stacks/backends/supabase/ci.js +3 -0
  118. package/src/stacks/backends/supabase/create-files.js +52 -0
  119. package/src/stacks/backends/supabase/docker.js +3 -0
  120. package/src/stacks/backends/supabase/environment.js +3 -0
  121. package/src/stacks/backends/supabase/index.js +31 -0
  122. package/src/stacks/backends/supabase/native.js +14 -0
  123. package/src/stacks/compose-files.js +259 -0
  124. package/{lib/generator.js → src/stacks/create-project.js} +206 -57
  125. package/src/stacks/frontends/README.md +4 -0
  126. package/src/stacks/frontends/nextjs/ci.js +3 -0
  127. package/src/stacks/frontends/nextjs/create-files.js +46 -0
  128. package/src/stacks/frontends/nextjs/dependencies.js +5 -0
  129. package/src/stacks/frontends/nextjs/docker.js +3 -0
  130. package/src/stacks/frontends/nextjs/environment.js +7 -0
  131. package/src/stacks/frontends/nextjs/index.js +35 -0
  132. package/src/stacks/frontends/react-native/ci.js +3 -0
  133. package/src/stacks/frontends/react-native/create-files.js +28 -0
  134. package/src/stacks/frontends/react-native/dependencies.js +5 -0
  135. package/src/stacks/frontends/react-native/docker.js +5 -0
  136. package/src/stacks/frontends/react-native/environment.js +7 -0
  137. package/src/stacks/frontends/react-native/index.js +30 -0
  138. package/src/stacks/frontends/react-vite/ci.js +3 -0
  139. package/src/stacks/frontends/react-vite/create-files.js +27 -0
  140. package/src/stacks/frontends/react-vite/dependencies.js +5 -0
  141. package/src/stacks/frontends/react-vite/docker.js +3 -0
  142. package/src/stacks/frontends/react-vite/environment.js +8 -0
  143. package/src/stacks/frontends/react-vite/index.js +32 -0
  144. package/src/stacks/shared/capability-packs.js +31 -0
  145. package/{lib → src}/stacks/shared/contributions.js +1 -1
  146. package/src/stacks/shared/environment.js +38 -0
  147. package/src/stacks/shared/javascript-package.js +117 -0
  148. package/src/stacks/shared/testing-files.js +16 -0
  149. package/templates/ci/fastapi.yml +62 -0
  150. package/templates/docker/compose/postgres.yml +1 -1
  151. package/templates/docker/compose/springboot.yml +3 -3
  152. package/templates/docker/compose/supabase.yml +1 -1
  153. package/templates/docker/compose-prod/fastapi.yml +52 -0
  154. package/templates/docker/compose-prod/springboot.yml +20 -2
  155. package/templates/docker/dockerfile/fastapi.dev.dockerfile +9 -0
  156. package/templates/docker/dockerfile/fastapi.prod.dockerfile +11 -0
  157. package/templates/docker/dockerfile/nextjs.prod.dockerfile +1 -0
  158. package/templates/docker/dockerfile/springboot.prod.dockerfile +4 -1
  159. package/templates/docker/dockerfile/vite.prod.dockerfile +2 -1
  160. package/templates/makefile/fastapi.mk +97 -0
  161. package/docs/ARCHITECTURE.md +0 -118
  162. package/lib/banner.js +0 -45
  163. package/lib/constants.js +0 -3
  164. package/lib/doctor.js +0 -45
  165. package/lib/interview.js +0 -77
  166. package/lib/laravel-scaffold.js +0 -3
  167. package/lib/scaffold.js +0 -467
  168. package/lib/stacks/index.js +0 -8
  169. package/playbooks/concerns/zod.md +0 -174
  170. package/playbooks/devops/makefile.manifest.json +0 -10
  171. package/playbooks/devops/makefile.md +0 -556
  172. package/playbooks/styling/css-modules-extensions.md +0 -267
  173. package/playbooks/styling/css-modules.manifest.json +0 -11
  174. package/playbooks/universal/coding-rules.manifest.json +0 -12
  175. package/playbooks/universal/coding-rules.md +0 -281
  176. package/playbooks/universal/git-conventions.manifest.json +0 -10
  177. package/playbooks/universal/git-conventions.md +0 -186
  178. package/playbooks/universal/typescript.manifest.json +0 -11
  179. package/playbooks/universal/typescript.md +0 -272
  180. package/scripts/compatibility-matrix.mjs +0 -48
  181. /package/{playbooks/capabilities → library/development-tools}/ci/github-actions.md +0 -0
  182. /package/{playbooks/devops → library/development-tools/devops/pr-template}/pr-template.md +0 -0
  183. /package/{playbooks/capabilities/docker/docker.manifest.json → library/development-tools/docker/definition.json} +0 -0
  184. /package/{playbooks/capabilities → library/development-tools}/docker/overview.md +0 -0
  185. /package/{playbooks/capabilities → library/features}/auth/oidc-resource-server.md +0 -0
  186. /package/{playbooks/capabilities → library/features}/auth/spring-session.md +0 -0
  187. /package/{playbooks/capabilities/flyway/flyway.manifest.json → library/features/flyway/definition.json} +0 -0
  188. /package/{playbooks/capabilities → library/features}/flyway/environments.md +0 -0
  189. /package/{playbooks/capabilities → library/features}/flyway/migrations.md +0 -0
  190. /package/{playbooks/capabilities → library/features}/flyway/testing.md +0 -0
  191. /package/{playbooks/capabilities → library/features}/laravel/database.md +0 -0
  192. /package/{playbooks/capabilities → library/features}/laravel/migrations.md +0 -0
  193. /package/{playbooks/capabilities → library/features}/laravel/observability.md +0 -0
  194. /package/{playbooks/capabilities → library/features}/laravel/oidc-resource-server.md +0 -0
  195. /package/{playbooks/capabilities → library/features}/laravel/queues.md +0 -0
  196. /package/{playbooks/capabilities → library/features}/laravel/sanctum-spa.md +0 -0
  197. /package/{playbooks/capabilities → library/features}/laravel/scheduler.md +0 -0
  198. /package/{playbooks/capabilities → library/features}/laravel/session-auth.md +0 -0
  199. /package/{playbooks/capabilities → library/features}/laravel/storage-uploads.md +0 -0
  200. /package/{playbooks/capabilities → library/features}/postgresql/architecture.md +0 -0
  201. /package/{playbooks/capabilities/postgresql/postgresql.manifest.json → library/features/postgresql/definition.json} +0 -0
  202. /package/{playbooks/capabilities → library/features}/postgresql/migrations.md +0 -0
  203. /package/{playbooks/capabilities → library/features}/postgresql/schema-design.md +0 -0
  204. /package/{playbooks/capabilities → library/features}/postgresql/security.md +0 -0
  205. /package/{playbooks/capabilities → library/features}/postgresql/testing.md +0 -0
  206. /package/{playbooks/capabilities → library/features}/prisma/architecture.md +0 -0
  207. /package/{playbooks/capabilities/prisma/prisma.manifest.json → library/features/prisma/definition.json} +0 -0
  208. /package/{playbooks/capabilities → library/features}/prisma/migrations.md +0 -0
  209. /package/{playbooks/capabilities → library/features}/prisma/runtime.md +0 -0
  210. /package/{playbooks/capabilities → library/features}/prisma/schema.md +0 -0
  211. /package/{playbooks/capabilities → library/features}/prisma/testing.md +0 -0
  212. /package/{playbooks/capabilities → library/features}/supabase/architecture.md +0 -0
  213. /package/{playbooks/capabilities → library/features}/supabase/authentication.md +0 -0
  214. /package/{playbooks/capabilities/supabase/supabase.manifest.json → library/features/supabase/definition.json} +0 -0
  215. /package/{playbooks/capabilities → library/features}/supabase/expo.md +0 -0
  216. /package/{playbooks/capabilities → library/features}/supabase/migrations.md +0 -0
  217. /package/{playbooks/capabilities → library/features}/supabase/nextjs.md +0 -0
  218. /package/{playbooks/capabilities → library/features}/supabase/rls.md +0 -0
  219. /package/{playbooks/capabilities → library/features}/supabase/testing.md +0 -0
  220. /package/{playbooks/capabilities → library/features}/supabase/vite.md +0 -0
  221. /package/{playbooks → library/optional-features}/concerns/axios.md +0 -0
  222. /package/{playbooks → library/optional-features}/concerns/next-safe-action.md +0 -0
  223. /package/{playbooks → library/optional-features}/concerns/next-themes.md +0 -0
  224. /package/{playbooks → library/optional-features}/concerns/nuqs.md +0 -0
  225. /package/{playbooks → library/optional-features}/concerns/t3-env.md +0 -0
  226. /package/{playbooks → library/optional-features}/concerns/tanstack-query.md +0 -0
  227. /package/{playbooks → library/optional-features}/concerns/zustand.md +0 -0
  228. /package/{playbooks/styling/native-styles.manifest.json → library/optional-features/styling/native-styles/definition.json} +0 -0
  229. /package/{playbooks/styling → library/optional-features/styling/native-styles}/native-styles.md +0 -0
  230. /package/{playbooks/styling/tailwind.manifest.json → library/optional-features/styling/tailwind/definition.json} +0 -0
  231. /package/{playbooks/styling → library/optional-features/styling/tailwind}/tailwind-extensions.md +0 -0
  232. /package/{playbooks/platform → library/platforms}/laravel-ui/blade/architecture.md +0 -0
  233. /package/{playbooks/platform → library/platforms}/laravel-ui/blade/runtime.md +0 -0
  234. /package/{playbooks/platform → library/platforms}/laravel-ui/blade/security.md +0 -0
  235. /package/{playbooks/platform → library/platforms}/laravel-ui/blade/structure.md +0 -0
  236. /package/{playbooks/platform → library/platforms}/laravel-ui/blade/testing.md +0 -0
  237. /package/{playbooks/stack/laravel-ui.manifest.json → library/platforms/laravel-ui/definition.json} +0 -0
  238. /package/{playbooks/platform → library/platforms}/laravel-ui/inertia-react/architecture.md +0 -0
  239. /package/{playbooks/platform → library/platforms}/laravel-ui/inertia-react/runtime.md +0 -0
  240. /package/{playbooks/platform → library/platforms}/laravel-ui/inertia-react/security.md +0 -0
  241. /package/{playbooks/platform → library/platforms}/laravel-ui/inertia-react/structure.md +0 -0
  242. /package/{playbooks/platform → library/platforms}/laravel-ui/inertia-react/testing.md +0 -0
  243. /package/{playbooks/platform → library/platforms}/laravel-ui/livewire/architecture.md +0 -0
  244. /package/{playbooks/platform → library/platforms}/laravel-ui/livewire/runtime.md +0 -0
  245. /package/{playbooks/platform → library/platforms}/laravel-ui/livewire/security.md +0 -0
  246. /package/{playbooks/platform → library/platforms}/laravel-ui/livewire/structure.md +0 -0
  247. /package/{playbooks/platform → library/platforms}/laravel-ui/livewire/testing.md +0 -0
  248. /package/{playbooks/platform → library/platforms/mobile}/mobile.md +0 -0
  249. /package/{playbooks/platform → library/platforms/web}/web.md +0 -0
  250. /package/{playbooks/stack → library/stacks}/expo/architecture.md +0 -0
  251. /package/{playbooks/stack → library/stacks}/expo/runtime.md +0 -0
  252. /package/{playbooks/stack → library/stacks}/expo/security.md +0 -0
  253. /package/{playbooks/stack → library/stacks}/expo/structure.md +0 -0
  254. /package/{playbooks/stack → library/stacks}/expo/testing.md +0 -0
  255. /package/{playbooks/stack → library/stacks}/laravel/architecture.md +0 -0
  256. /package/{playbooks/stack/laravel.manifest.json → library/stacks/laravel/definition.json} +0 -0
  257. /package/{playbooks/stack → library/stacks}/laravel/runtime.md +0 -0
  258. /package/{playbooks/stack → library/stacks}/laravel/security.md +0 -0
  259. /package/{playbooks/stack → library/stacks}/laravel/structure.md +0 -0
  260. /package/{playbooks/stack → library/stacks}/laravel/testing.md +0 -0
  261. /package/{playbooks/stack → library/stacks}/nextjs/architecture.md +0 -0
  262. /package/{playbooks/stack → library/stacks}/nextjs/runtime.md +0 -0
  263. /package/{playbooks/stack → library/stacks}/nextjs/security.md +0 -0
  264. /package/{playbooks/stack → library/stacks}/nextjs/structure.md +0 -0
  265. /package/{playbooks/stack → library/stacks}/nextjs/testing.md +0 -0
  266. /package/{playbooks/stack/none.manifest.json → library/stacks/none/definition.json} +0 -0
  267. /package/{playbooks/stack → library/stacks}/react-vite/architecture.md +0 -0
  268. /package/{playbooks/stack → library/stacks}/react-vite/runtime.md +0 -0
  269. /package/{playbooks/stack → library/stacks}/react-vite/security.md +0 -0
  270. /package/{playbooks/stack → library/stacks}/react-vite/structure.md +0 -0
  271. /package/{playbooks/stack → library/stacks}/react-vite/testing.md +0 -0
  272. /package/{playbooks/stack → library/stacks}/springboot/architecture.md +0 -0
  273. /package/{playbooks/stack/springboot.manifest.json → library/stacks/springboot/definition.json} +0 -0
  274. /package/{playbooks/stack → library/stacks}/springboot/runtime.md +0 -0
  275. /package/{playbooks/stack → library/stacks}/springboot/security.md +0 -0
  276. /package/{playbooks/stack → library/stacks}/springboot/structure.md +0 -0
  277. /package/{playbooks/stack → library/stacks}/springboot/testing.md +0 -0
  278. /package/{playbooks/universal → library/universal/accessibility}/accessibility.md +0 -0
  279. /package/{playbooks/universal → library/universal/error-handling}/error-handling.md +0 -0
  280. /package/{playbooks/universal → library/universal/observability}/observability.md +0 -0
  281. /package/{playbooks/universal → library/universal/security}/security.md +0 -0
  282. /package/{lib → src/engine}/project-location.js +0 -0
  283. /package/{lib → src}/stacks/context.js +0 -0
  284. /package/{lib/stacks/contract.js → src/stacks/rules.js} +0 -0
  285. /package/{ci → templates/ci}/expo.yml +0 -0
  286. /package/{ci → templates/ci}/laravel.yml +0 -0
  287. /package/{ci → templates/ci}/nextjs.yml +0 -0
  288. /package/{ci → templates/ci}/springboot.yml +0 -0
  289. /package/{ci → templates/ci}/vite.yml +0 -0
@@ -1,39 +1,33 @@
1
1
  import fs from 'fs-extra'
2
2
  import path from 'path'
3
- import { randomUUID } from 'node:crypto'
4
- import { loadCatalog, resolveStack } from './catalog.js'
5
- import { loadCompatibility } from './compatibility.js'
6
- import { buildRulesIndex, copySelectedPlaybooks } from './playbooks.js'
7
- import { buildVars, render, readTemplate } from './template.js'
8
- import { buildRunnableFiles } from './scaffold.js'
9
- import { buildLaravelFiles } from './laravel-scaffold.js'
3
+ import { loadCatalog, resolveStack } from '../engine/load-library.js'
4
+ import { loadCompatibility } from '../engine/tested-versions.js'
5
+ import { buildRulesIndex, copySelectedPlaybooks } from '../engine/project-guidance.js'
6
+ import { buildVars, render, readTemplate } from '../engine/render-templates.js'
7
+ import { buildRunnableFiles } from './compose-files.js'
10
8
  import {
11
9
  contextMd, progressMd, docPlaceholder,
12
10
  editorconfig, prettierrc, prTemplate,
13
- } from './files.js'
14
-
15
- function laravelCompose(answers, stack, vars) {
16
- const laravelDir = stack.frontendKey === 'laravel-ui' || stack.frontendKey === 'no-frontend' ? '.' : './backend'
17
- const frontend = stack.frontendKey === 'react'
18
- ? ` frontend:\n build:\n context: ./frontend\n dockerfile: Dockerfile.dev\n ports:\n - "5173:5173"\n volumes:\n - ./frontend:/app\n - frontend-node-modules:/app/node_modules\n environment:\n VITE_API_URL: http://localhost:8000\n depends_on:\n - backend\n\n`
19
- : stack.frontendKey === 'nextjs'
20
- ? ` frontend:\n build:\n context: .\n dockerfile: Dockerfile.dev\n ports:\n - "3000:3000"\n volumes:\n - .:/app\n - /app/backend\n - frontend-node-modules:/app/node_modules\n environment:\n NEXT_PUBLIC_API_URL: http://localhost:8000\n depends_on:\n - backend\n\n`
21
- : ''
22
- const frontendVolume = frontend ? ' frontend-node-modules:\n' : ''
23
- return `services:\n${frontend} backend:\n build:\n context: ${laravelDir}\n dockerfile: Dockerfile.dev\n ports:\n - "8000:8000"\n volumes:\n - ${laravelDir}:/app\n - laravel-vendor:/app/vendor\n environment:\n APP_ENV: local\n APP_DEBUG: "true"\n APP_KEY: \${APP_KEY:-}\n DB_CONNECTION: pgsql\n DB_HOST: db\n DB_PORT: 5432\n DB_DATABASE: \${POSTGRES_DB}\n DB_USERNAME: \${POSTGRES_USER}\n DB_PASSWORD: \${POSTGRES_PASSWORD}\n depends_on:\n db:\n condition: service_healthy\n\n db:\n image: ${vars.POSTGRES_IMAGE}\n ports:\n - "5432:5432"\n environment:\n POSTGRES_DB: \${POSTGRES_DB}\n POSTGRES_USER: \${POSTGRES_USER}\n POSTGRES_PASSWORD: \${POSTGRES_PASSWORD}\n volumes:\n - postgres-data:/var/lib/postgresql/data\n healthcheck:\n test: ["CMD-SHELL", "pg_isready -U \${POSTGRES_USER} -d \${POSTGRES_DB}"]\n interval: 5s\n timeout: 5s\n retries: 10\n\nvolumes:\n${frontendVolume} laravel-vendor:\n postgres-data:\n`
24
- }
11
+ } from '../engine/project-files.js'
12
+ import {
13
+ projectDestinations,
14
+ writeFile as write,
15
+ writeProjectAtomically,
16
+ } from '../engine/write-files.js'
17
+ import { writeRenderedFile as writeTemplate } from '../engine/render-templates.js'
18
+ import { laravelCompose } from './backends/laravel/docker.js'
19
+ import { fastapiCompose } from './backends/fastapi/docker.js'
25
20
 
26
21
  /**
27
22
  * Main entry point — generates the full project
28
23
  */
29
- export async function generateProject(answers, cliRoot) {
24
+ export async function scaffoldProject(answers, cliRoot) {
30
25
  validateAnswers(answers)
31
- const finalDest = path.join(process.cwd(), answers.projectName)
32
- const dest = path.join(process.cwd(), `.${answers.projectName}.tmp-${randomUUID()}`)
33
- const playbooksDir = path.join(cliRoot, 'playbooks')
34
- const ciDir = path.join(cliRoot, 'ci')
26
+ const { finalDestination, stagingDestination } = await projectDestinations(process.cwd(), answers.projectName)
27
+ const playbooksDir = path.join(cliRoot, 'library')
28
+ const ciDir = path.join(cliRoot, 'templates', 'ci')
35
29
  const { profile } = await loadCompatibility(
36
- path.join(cliRoot, 'compatibility/profiles.json'),
30
+ path.join(cliRoot, 'library/tested-versions.json'),
37
31
  answers.compatibilityProfile,
38
32
  )
39
33
  const catalog = await loadCatalog(playbooksDir, profile)
@@ -50,15 +44,12 @@ export async function generateProject(answers, cliRoot) {
50
44
 
51
45
  // 1. Refuse to merge into an existing project. Generation must never silently
52
46
  // overwrite user work.
53
- if (await fs.pathExists(finalDest)) {
54
- throw new Error(`Destination already exists: ${finalDest}`)
55
- }
56
-
57
- try {
47
+ await writeProjectAtomically({
48
+ finalDestination,
49
+ stagingDestination,
50
+ generate: async (dest) => {
58
51
  // 2. Staging folder. It is moved into place only after every generation
59
52
  // step succeeds, so failures never leave a half-written project.
60
- await fs.ensureDir(dest)
61
-
62
53
  // 3. Root documentation and repository files. Directories are created only
63
54
  // when a real generated file needs them; empty architecture theatre is not
64
55
  // part of the scaffold contract.
@@ -82,11 +73,8 @@ export async function generateProject(answers, cliRoot) {
82
73
  // 8. RULES.md
83
74
  const rulesContent = await buildRulesIndex(stack, catalog, playbooksDir)
84
75
  await write(dest, 'RULES.md', rulesContent)
85
- await fs.move(dest, finalDest)
86
- } catch (error) {
87
- await fs.remove(dest)
88
- throw error
89
- }
76
+ },
77
+ })
90
78
  }
91
79
 
92
80
  function validateAnswers(answers) {
@@ -97,9 +85,18 @@ function validateAnswers(answers) {
97
85
  if (typeof answers.projectDescription !== 'string' || !answers.projectDescription.trim()) {
98
86
  throw new Error('Project description is required')
99
87
  }
100
- if (answers.testing && !['none', 'basic', 'full'].includes(answers.testing)) {
88
+ if (answers.testing && !['basic', 'full'].includes(answers.testing)) {
101
89
  throw new Error(`Unknown testing setup: ${answers.testing}`)
102
90
  }
91
+ const uploads = answers.uploads || 'none'
92
+ const jobs = answers.backgroundJobs || 'none'
93
+ const offline = answers.offline || 'none'
94
+ if (!['none', 'object-storage'].includes(uploads)) throw new Error(`Unknown uploads requirement: ${uploads}`)
95
+ if (!['none', 'queue'].includes(jobs)) throw new Error(`Unknown background-jobs requirement: ${jobs}`)
96
+ if (!['none', 'cache', 'sync'].includes(offline)) throw new Error(`Unknown offline requirement: ${offline}`)
97
+ if (offline !== 'none' && answers.frontend !== 'react-native') throw new Error('Offline capabilities are supported only for mobile applications')
98
+ if (jobs === 'queue' && !['springboot', 'laravel'].includes(answers.backend)) throw new Error('Queues require a Spring Boot or Laravel backend')
99
+ if (uploads === 'object-storage' && answers.backend === 'none') throw new Error('Object storage uploads require a backend or managed data service')
103
100
  if (answers.architecture && !['small', 'medium', 'large'].includes(answers.architecture)) {
104
101
  throw new Error(`Unknown architecture profile: ${answers.architecture}`)
105
102
  }
@@ -114,11 +111,11 @@ function validateAnswers(answers) {
114
111
  // ─── Root files ───────────────────────────────────────────────────────────────
115
112
 
116
113
  async function generateRootFiles(dest, answers, vars, stack, templatesDir) {
117
- await writeTemplate(dest, 'CONTEXT.md', contextMd(vars, answers.expectedConcerns), vars)
114
+ await writeTemplate(dest, 'CONTEXT.md', contextMd(vars, answers.expectedConcerns, answers), vars)
118
115
  // AGENTS.md — template-driven
119
116
  {
120
117
  const tpl = await readTemplate(templatesDir, 'agents', stack.agentsTemplate, '.md')
121
- if (tpl) await writeTemplate(dest, 'AGENTS.md', tpl, vars)
118
+ if (tpl) await writeTemplate(dest, 'AGENTS.md', `${tpl}\n## Deviation policy\n\nAgents may recommend alternatives, but must propose the change and receive explicit approval before changing the selected architecture, provider, authentication model, data boundary, production baseline, or major dependency. Record approved deviations and their rationale in \`CONTEXT.md\`.\n`, vars)
122
119
  else await write(dest, 'AGENTS.md', `# AGENTS.md\nStack: ${stack.label}\n`)
123
120
  }
124
121
  await write(dest, 'PROGRESS.md', progressMd())
@@ -135,6 +132,15 @@ async function generateRootFiles(dest, answers, vars, stack, templatesDir) {
135
132
  }
136
133
  await write(dest, '.editorconfig', editorconfig())
137
134
  await write(dest, '.prettierrc', prettierrc())
135
+ if (stack.backendKey === 'springboot') await write(dest, 'backend/.java-version', `${stack.profile.runtimes.java}\n`)
136
+ if (stack.backendKey === 'fastapi') {
137
+ const apiRoot = stack.frontendKey === 'no-frontend' ? '' : 'backend/'
138
+ await write(dest, `${apiRoot}.python-version`, `${stack.profile.runtimes.python}\n`)
139
+ }
140
+ if (stack.backendKey === 'laravel') {
141
+ const laravelRoot = stack.frontendKey === 'laravel-ui' || stack.frontendKey === 'no-frontend' ? '' : 'backend/'
142
+ await write(dest, `${laravelRoot}.php-version`, `${stack.profile.runtimes.php}\n`)
143
+ }
138
144
 
139
145
  // Makefile — template-driven (web only)
140
146
  if (answers.makefile && !stack.isMobile) {
@@ -146,11 +152,13 @@ async function generateRootFiles(dest, answers, vars, stack, templatesDir) {
146
152
  // Mobile frontends run through Expo, but a separate Laravel backend still
147
153
  // needs its backend and PostgreSQL services. laravelCompose intentionally
148
154
  // omits a frontend service for React Native.
149
- if (answers.docker && (!stack.isMobile || stack.backendKey === 'laravel')) {
155
+ if (answers.docker && (!stack.isMobile || ['laravel', 'fastapi'].includes(stack.backendKey))) {
150
156
  // docker-compose.yml
151
157
  let composeTpl = null
152
158
  if (stack.backendKey === 'laravel') {
153
159
  composeTpl = laravelCompose(answers, stack, vars)
160
+ } else if (stack.backendKey === 'fastapi') {
161
+ composeTpl = fastapiCompose(answers, stack, vars)
154
162
  } else if (stack.needsPackage) {
155
163
  composeTpl = await readTemplate(templatesDir, 'docker/compose', 'springboot', '.yml')
156
164
  } else if (stack.backendKey === 'supabase') {
@@ -180,6 +188,22 @@ async function generateRootFiles(dest, answers, vars, stack, templatesDir) {
180
188
  if (beProd) await writeTemplate(dest, 'backend/Dockerfile', beProd, vars)
181
189
  }
182
190
 
191
+ if (stack.backendKey === 'fastapi') {
192
+ const apiRoot = stack.frontendKey === 'no-frontend' ? '' : 'backend/'
193
+ const prodTpl = await readTemplate(templatesDir, 'docker/compose-prod', 'fastapi', '.yml')
194
+ if (prodTpl) {
195
+ const rendered = stack.frontendKey === 'no-frontend'
196
+ ? prodTpl.replaceAll('context: ./backend', 'context: .').replaceAll('./backend:/app', '.:/app')
197
+ : prodTpl
198
+ await writeTemplate(dest, 'docker-compose.prod.yml', rendered, vars)
199
+ }
200
+
201
+ const beDev = await readTemplate(templatesDir, 'docker/dockerfile', 'fastapi.dev', '.dockerfile')
202
+ if (beDev) await writeTemplate(dest, `${apiRoot}Dockerfile.dev`, beDev, vars)
203
+ const beProd = await readTemplate(templatesDir, 'docker/dockerfile', 'fastapi.prod', '.dockerfile')
204
+ if (beProd) await writeTemplate(dest, `${apiRoot}Dockerfile`, beProd, vars)
205
+ }
206
+
183
207
  // frontend dockerfiles — vite vs nextjs
184
208
  if (stack.frontendKey === 'react') {
185
209
  const viteDev = await readTemplate(templatesDir, 'docker/dockerfile', 'vite.dev', '.dockerfile')
@@ -187,7 +211,7 @@ async function generateRootFiles(dest, answers, vars, stack, templatesDir) {
187
211
  const viteProd = await readTemplate(templatesDir, 'docker/dockerfile', 'vite.prod', '.dockerfile')
188
212
  if (viteProd) {
189
213
  await writeTemplate(dest, 'frontend/Dockerfile', viteProd, vars)
190
- const nginx = `server {\n listen 80;\n location / {\n root /usr/share/nginx/html;\n index index.html;\n try_files $uri $uri/ /index.html;\n }\n}\n`
214
+ const nginx = `server {\n listen 8080;\n server_tokens off;\n root /usr/share/nginx/html;\n add_header X-Content-Type-Options nosniff always;\n add_header Referrer-Policy strict-origin-when-cross-origin always;\n add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;\n add_header Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'" always;\n location /assets/ { try_files $uri =404; add_header Cache-Control "public, max-age=31536000, immutable"; }\n location /api/ { add_header Cache-Control "no-store" always; try_files $uri =404; }\n location / { index index.html; try_files $uri $uri/ /index.html; add_header Cache-Control "no-cache"; }\n}\n`
191
215
  await write(dest, 'frontend/nginx.conf', nginx)
192
216
  }
193
217
  } else if (stack.frontendKey === 'nextjs') {
@@ -198,6 +222,45 @@ async function generateRootFiles(dest, answers, vars, stack, templatesDir) {
198
222
  }
199
223
  }
200
224
 
225
+ // Production artifacts are part of the deployable web contract even when
226
+ // the optional development Docker workflow was not selected.
227
+ if (!answers.docker && !stack.isMobile) {
228
+ if (stack.frontendKey === 'nextjs') {
229
+ const nextProd = await readTemplate(templatesDir, 'docker/dockerfile', 'nextjs.prod', '.dockerfile')
230
+ if (nextProd) await writeTemplate(dest, 'Dockerfile', nextProd, vars)
231
+ }
232
+ if (stack.frontendKey === 'react') {
233
+ const viteProd = await readTemplate(templatesDir, 'docker/dockerfile', 'vite.prod', '.dockerfile')
234
+ if (viteProd) {
235
+ await writeTemplate(dest, 'frontend/Dockerfile', viteProd, vars)
236
+ await write(dest, 'frontend/nginx.conf', `server {\n listen 8080;\n server_tokens off;\n root /usr/share/nginx/html;\n add_header X-Content-Type-Options nosniff always;\n add_header Referrer-Policy strict-origin-when-cross-origin always;\n add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;\n add_header Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'" always;\n location /assets/ { try_files $uri =404; add_header Cache-Control "public, max-age=31536000, immutable"; }\n location /api/ { add_header Cache-Control "no-store" always; try_files $uri =404; }\n location / { index index.html; try_files $uri $uri/ /index.html; add_header Cache-Control "no-cache"; }\n}\n`)
237
+ }
238
+ }
239
+ if (stack.backendKey === 'springboot') {
240
+ const backendProd = await readTemplate(templatesDir, 'docker/dockerfile', 'springboot.prod', '.dockerfile')
241
+ if (backendProd) await writeTemplate(dest, 'backend/Dockerfile', backendProd, vars)
242
+ const composeProd = await readTemplate(templatesDir, 'docker/compose-prod', 'springboot', '.yml')
243
+ if (composeProd) await writeTemplate(dest, 'docker-compose.prod.yml', composeProd, vars)
244
+ }
245
+ if (stack.backendKey === 'fastapi') {
246
+ const apiRoot = stack.frontendKey === 'no-frontend' ? '' : 'backend/'
247
+ const backendProd = await readTemplate(templatesDir, 'docker/dockerfile', 'fastapi.prod', '.dockerfile')
248
+ if (backendProd) await writeTemplate(dest, `${apiRoot}Dockerfile`, backendProd, vars)
249
+ const composeProd = await readTemplate(templatesDir, 'docker/compose-prod', 'fastapi', '.yml')
250
+ if (composeProd) {
251
+ const rendered = stack.frontendKey === 'no-frontend'
252
+ ? composeProd.replaceAll('context: ./backend', 'context: .')
253
+ : composeProd
254
+ await writeTemplate(dest, 'docker-compose.prod.yml', rendered, vars)
255
+ }
256
+ }
257
+ if (stack.backendKey === 'laravel') {
258
+ const laravelRoot = ['laravel-ui', 'no-frontend'].includes(stack.frontendKey) ? '' : 'backend/'
259
+ const laravelProd = await readTemplate(templatesDir, 'docker/dockerfile', 'laravel.prod', '.dockerfile')
260
+ if (laravelProd) await writeTemplate(dest, `${laravelRoot}Dockerfile`, laravelProd, vars)
261
+ }
262
+ }
263
+
201
264
  // PR template
202
265
  if (answers.githubActions) {
203
266
  await write(dest, '.github/PULL_REQUEST_TEMPLATE.md', prTemplate())
@@ -206,6 +269,45 @@ async function generateRootFiles(dest, answers, vars, stack, templatesDir) {
206
269
 
207
270
  // ─── Doc placeholders ─────────────────────────────────────────────────────────
208
271
 
272
+ function toolchainGuide(answers, stack) {
273
+ const javascriptRoot = stack.frontendKey === 'react' ? 'frontend/' : ''
274
+ const hasJavaScript = stack.frontendKey !== 'no-frontend' &&
275
+ !(stack.frontendKey === 'laravel-ui' && stack.laravelUi !== 'inertia-react')
276
+ const rows = []
277
+ if (hasJavaScript) {
278
+ rows.push(`| Node.js | ${stack.profile.runtimes.nodeMinimum}+ (tested ${stack.profile.runtimes.node}) | JavaScript application | \`${javascriptRoot}.node-version\`, \`${javascriptRoot}package.json\` |`)
279
+ rows.push(`| npm | ${stack.profile.runtimes.npmMinimum}+ | Local package scripts | \`${javascriptRoot}package.json#packageManager\`, \`${javascriptRoot}.npmrc\` |`)
280
+ }
281
+ if (stack.backendKey === 'springboot') {
282
+ rows.push(`| Java | ${stack.profile.runtimes.java} | Host-run backend | \`backend/.java-version\`, \`backend/pom.xml\` |`)
283
+ rows.push(`| Maven | ${stack.profile.runtimes.maven} | Optional globally; prefer the generated wrapper | \`backend/mvnw\` or \`backend/mvnw.cmd\` |`)
284
+ }
285
+ if (stack.backendKey === 'laravel') {
286
+ const root = stack.frontendKey === 'laravel-ui' || stack.frontendKey === 'no-frontend' ? '' : 'backend/'
287
+ rows.push(`| PHP | ${stack.profile.runtimes.php} | Host-run Laravel backend | \`${root}.php-version\` |`)
288
+ rows.push(`| Composer | ${stack.profile.runtimes.composer} | Host-run Laravel backend | \`${root}composer.json\` |`)
289
+ }
290
+ if (stack.backendKey === 'fastapi') {
291
+ const root = stack.frontendKey === 'no-frontend' ? '' : 'backend/'
292
+ rows.push(`| Python | ${stack.profile.runtimes.python} | Host-run FastAPI backend | \`${root}.python-version\` |`)
293
+ rows.push(`| uv | ${stack.profile.runtimes.uv} | Host-run FastAPI backend | \`${root}pyproject.toml\` |`)
294
+ }
295
+ if (answers.docker || ['supabase', 'postgres'].includes(stack.backendKey)) {
296
+ rows.push('| Docker with Compose | Current supported release | Generated containers and local managed services | `docker-compose.yml` when selected |')
297
+ }
298
+ return `# Toolchain Requirements\n\nInstall only the tools required by the chosen local workflow. ESLint, Prettier, Prisma, TypeScript, and framework CLIs are project dependencies; run them through npm scripts or \`npx\`, never as global installations.\n\n| Tool | Version | When needed | Version source |\n|---|---|---|---|\n${rows.join('\n')}\n\nDependency retries run inside the generated package directory, so npm reads this project's own \`package.json\`, lockfile, engines, and \`.npmrc\`. Different projects can retain different tested dependency versions without global conflicts.\n\n## Host ports\n\nBefore starting containers, check whether the default ports are already in use (for example, \`ss -ltn\` on Linux). Override conflicts when launching Compose with \`FRONTEND_HOST_PORT\`, \`BACKEND_HOST_PORT\`, or \`POSTGRES_HOST_PORT\`; container ports and service-to-service addresses remain unchanged.\n`
299
+ }
300
+
301
+ function developmentEnvironmentGuide(answers, stack) {
302
+ const mobile = stack.isMobile
303
+ ? '\n## Mobile boundary\n\nRun Expo, the iOS Simulator or Android Emulator, and physical-device tooling on the host. Containers may run a selected backend and database, but they do not replace the local device/emulator workflow.\n'
304
+ : ''
305
+ const docker = answers.docker
306
+ ? `\n## Optional Docker workflow\n\nDocker Engine 27 or newer with Docker Compose v2.30 or newer is recommended. Allocate at least 4 GB of memory and 10 GB of free disk space for images, caches, and databases. Check occupied ports before startup with \`ss -ltn\` (Linux) or your platform's equivalent.\n\n\`\`\`bash\ndocker compose config\ndocker compose build\ndocker compose up -d\n\`\`\`\n\nOverride host collisions with \`FRONTEND_HOST_PORT\`, \`BACKEND_HOST_PORT\`, and \`POSTGRES_HOST_PORT\`. Backend runtimes and databases stay inside containers, reducing the host tools you need.\n`
307
+ : '\n## Adding Docker later\n\nDocker files were not selected. Re-run the generator for a fresh project with Docker enabled if you want isolated backend runtimes and databases; the normal host workflow remains fully supported.\n'
308
+ return `# Development Environments\n\n## Default local workflow\n\nRun the generator directly on the host, then use the commands in \`setup.md\`. Docker is optional and is never required to run create-win-project itself. Local files and package-manager metadata remain the source of truth.\n${docker}${mobile}\n## Dev Containers\n\nA generic Dev Container is intentionally not generated: JavaScript, Java, PHP, and mobile stacks need different host/device boundaries. VS Code and Codespaces users can open the generated repository normally and add a stack-specific Dev Container later without changing the supported local or Compose workflows.\n`
309
+ }
310
+
209
311
  async function generateDocs(dest, answers, stack) {
210
312
  const docs = [
211
313
  ['docs/api/overview.md', 'API Overview', 'Base URL, authentication method, and response format.'],
@@ -221,6 +323,8 @@ async function generateDocs(dest, answers, stack) {
221
323
  for (const [filePath, title, description] of docs) {
222
324
  await write(dest, filePath, docPlaceholder(title, description))
223
325
  }
326
+ await write(dest, 'docs/guides/deployment.md', `# Production deployment and rollback\n\nBuild immutable images from the committed lockfiles and deploy \`docker-compose.prod.yml\` where generated. Validate required environment variables before starting; secrets belong in the deployment platform, never images or client bundles. Run readiness checks before routing traffic and allow the documented graceful-shutdown window during replacement.\n\n## Database preflight\n\nBack up PostgreSQL with encryption before migrations, check available connections and migration compatibility, then run migrations as a single release task. Application instances use a bounded connection pool; size the total across replicas below the database limit.\n\n## Rollback\n\nRetain the previous image digest and a compatible database restore point. Stop routing to the failed release, restore the prior image, and restore data only when the migration is not backward compatible. Test restores automatically on a separate database and document retention and recovery ownership.\n`)
327
+ await write(dest, 'docs/guides/operations.md', `# Operations\n\nEvery request crossing an HTTP boundary receives or creates an \`X-Request-ID\` and returns it in responses. Errors use a stable JSON shape: \`{ "error": { "code": "stable_code", "message": "safe message", "requestId": "..." } }\`. Never expose stack traces.\n\nList endpoints use cursor pagination with explicit maximum page sizes. Outbound calls have connection and response timeouts; retry only bounded idempotent operations with jitter. Readiness checks include required downstream dependencies while liveness checks remain process-local.\n\nCORS uses an explicit origin allowlist. Cookie-authenticated browser writes require SameSite cookies plus CSRF validation; bearer-token APIs do not use wildcard origins with credentials. Static fingerprinted assets are immutable, HTML revalidates, and API/auth responses default to \`no-store\`.\n`)
224
328
 
225
329
  const frontendRoot = stack.frontendKey === 'react' ? 'frontend/' : ''
226
330
  const validation = stack.isMobile
@@ -230,15 +334,37 @@ async function generateDocs(dest, answers, stack) {
230
334
 
231
335
  const setupPath = path.join(dest, 'docs/guides/setup.md')
232
336
  let setupGuide = (await fs.readFile(setupPath, 'utf8'))
233
- .replace('Node.js 20 or newer', `Node.js ${stack.profile.runtimes.node}`)
337
+ .replace('Node.js 20 or newer', `Node.js ${stack.profile.runtimes.nodeMinimum} or newer with npm ${stack.profile.runtimes.npmMinimum} or newer (tested on Node.js ${stack.profile.runtimes.node})`)
234
338
  .replace('Java 21 and Maven 3.9, or Docker', `Java ${stack.profile.runtimes.java}; Maven ${stack.profile.runtimes.maven} or Docker is used through the generated launcher`)
235
339
  .replace('PostgreSQL 16, or Docker', `PostgreSQL ${stack.profile.runtimes.postgres}, or Docker`)
236
340
  .replace('mvn spring-boot:run', './mvnw spring-boot:run # use mvnw.cmd on Windows')
341
+ if (stack.backendKey === 'springboot' && !answers.docker) {
342
+ setupGuide = setupGuide.replace('docker compose up -d db', '# Start PostgreSQL on the host, then configure DATABASE_URL')
343
+ }
237
344
  if (stack.backendKey === 'laravel') {
238
345
  const laravelDir = stack.frontendKey === 'laravel-ui' || stack.frontendKey === 'no-frontend' ? '' : 'backend/'
239
- setupGuide = `# Local Setup Guide\n\n## Docker-first setup\n\nOnly Docker with Compose is required. Make is optional; every Make target has a direct Docker equivalent.\n\n\`\`\`bash\ncp ${laravelDir}.env.example ${laravelDir}.env\ndocker compose build\ndocker compose up -d\ndocker compose exec backend php artisan key:generate\ndocker compose exec backend php artisan migrate\n\`\`\`\n\nLater runs use \`docker compose up -d\`; rebuilding is explicit with \`docker compose build\`. If Make is available, \`make setup\` performs the first-run sequence and \`make run\` starts existing images without rebuilding.\n\n## Validate\n\n\`\`\`bash\ndocker compose exec backend composer check\n\`\`\`\n\nWhen dependencies are installed outside Docker, commit \`composer.lock\`${stack.frontendKey === 'laravel-ui' && stack.laravelUi === 'inertia-react' ? ' and `package-lock.json`' : ''}.\n`
346
+ const frontendSetup = stack.frontendKey === 'react'
347
+ ? '\nIn another terminal:\n\n```bash\ncd frontend\nnpm install\nnpm run dev\n```\n'
348
+ : stack.frontendKey === 'nextjs'
349
+ ? '\nIn another terminal from the repository root:\n\n```bash\nnpm install\nnpm run dev\n```\n'
350
+ : stack.frontendKey === 'laravel-ui' && stack.laravelUi === 'inertia-react'
351
+ ? '\nIn another terminal from the repository root:\n\n```bash\nnpm install\nnpm run dev\n```\n'
352
+ : ''
353
+ setupGuide = `# Local Setup Guide\n\n## Default local setup\n\nUse PHP ${stack.profile.runtimes.php}, Composer ${stack.profile.runtimes.composer}, and PostgreSQL ${stack.profile.runtimes.postgres}.\n\n\`\`\`bash\ncd ${laravelDir || '.'}\ncomposer install\ncp .env.example .env\nphp artisan key:generate\nphp artisan migrate\nphp artisan serve\n\`\`\`\n${frontendSetup}${answers.docker ? `\n## Optional Docker setup\n\nFrom the repository root:\n\n\`\`\`bash\ndocker compose build\ndocker compose up -d\ndocker compose exec backend php artisan key:generate\ndocker compose exec backend php artisan migrate\n\`\`\`\n\nLater runs use \`docker compose up -d\`; rebuilding remains explicit.\n` : ''}\n## Validate\n\n\`\`\`bash\n${laravelDir ? `cd ${laravelDir}\n` : ''}composer check\n\`\`\`\n\nCommit \`composer.lock\`${stack.frontendKey === 'laravel-ui' && stack.laravelUi === 'inertia-react' ? ' and `package-lock.json`' : ''}.\n`
354
+ }
355
+ if (stack.backendKey === 'fastapi') {
356
+ const apiDir = stack.frontendKey === 'no-frontend' ? '' : 'backend/'
357
+ const frontendSetup = stack.frontendKey === 'react'
358
+ ? '\nIn another terminal:\n\n```bash\ncd frontend\nnpm install\nnpm run dev\n```\n'
359
+ : stack.frontendKey === 'nextjs'
360
+ ? '\nIn another terminal from the repository root:\n\n```bash\nnpm install\nnpm run dev\n```\n'
361
+ : ''
362
+ setupGuide = `# Local Setup Guide\n\n## Default local setup\n\nUse Python ${stack.profile.runtimes.python}, uv ${stack.profile.runtimes.uv}, and PostgreSQL ${stack.profile.runtimes.postgres}.\n\n\`\`\`bash\ncd ${apiDir || '.'}\nuv sync\ncp .env.example .env\nalembic upgrade head\nuv run uvicorn app.main:app --reload\n\`\`\`\n${frontendSetup}${answers.docker ? `\n## Optional Docker setup\n\nFrom the repository root:\n\n\`\`\`bash\ndocker compose build\ndocker compose up -d\ndocker compose exec backend uv run alembic upgrade head\n\`\`\`\n\nLater runs use \`docker compose up -d\`; rebuilding remains explicit.\n` : ''}\n## Validate\n\n\`\`\`bash\n${apiDir ? `cd ${apiDir}\n` : ''}uv run ruff check . && uv run ruff format --check . && uv run mypy . && uv run pytest\n\`\`\`\n\nCommit the generated \`uv.lock\`; CI uses \`uv sync --frozen\`.\n`
363
+ setupGuide = setupGuide.replace('\nalembic upgrade head\n', '\nuv run alembic upgrade head\n')
240
364
  }
241
365
  await fs.writeFile(setupPath, setupGuide, 'utf8')
366
+ await write(dest, 'docs/guides/toolchain.md', toolchainGuide(answers, stack))
367
+ await write(dest, 'docs/guides/development-environments.md', developmentEnvironmentGuide(answers, stack))
242
368
 
243
369
  const envLocation = stack.frontendKey === 'react' ? '`frontend/.env` for client values' : stack.isMobile ? '`.env`' : '`.env.local`'
244
370
  await write(dest, 'docs/guides/env-variables.md', `# Environment Variables\n\nCopy the generated example before starting. Client environment location: ${envLocation}.\n\n| Variable | Visibility | Required | Purpose |\n|---|---|---:|---|\n${stack.env.map((name) => `| \`${name}\` | ${name.startsWith(stack.envPrefix) ? 'client/public' : 'server only'} | yes | ${environmentPurpose(name)} |`).join('\n')}\n\nValues with \`${stack.envPrefix}\` are bundled into client code and must never contain secrets. Keep real environment files out of version control.\n`)
@@ -251,7 +377,9 @@ async function generateDocs(dest, answers, stack) {
251
377
  ? 'GET http://localhost:8080/api/health'
252
378
  : stack.backendKey === 'laravel'
253
379
  ? 'GET http://localhost:8000/api/health'
254
- : stack.frontendKey === 'nextjs' ? 'GET /api/health' : '(No custom HTTP API is generated for this client-only starter.)'
380
+ : stack.backendKey === 'fastapi'
381
+ ? 'GET http://localhost:8000/health'
382
+ : stack.frontendKey === 'nextjs' ? 'GET /api/health' : '(No custom HTTP API is generated for this client-only starter.)'
255
383
  await write(dest, 'docs/api/overview.md', `# API Overview\n\n## Health\n\n\`${health}\`\n\nAuthentication and authorization behavior is documented in \`docs/architecture/auth-flow.md\`. Add endpoints to \`docs/api/endpoints.md\` in the same change that adds their implementation and tests.\n`)
256
384
  }
257
385
 
@@ -259,7 +387,7 @@ function environmentPurpose(name) {
259
387
  if (name.endsWith('SUPABASE_URL')) return 'Supabase project URL.'
260
388
  if (name.endsWith('SUPABASE_PUBLISHABLE_KEY')) return 'Public Supabase key; RLS protects data.'
261
389
  if (name.endsWith('API_URL')) return 'Base URL of the application API.'
262
- if (name === 'DATABASE_URL') return 'Server-side PostgreSQL JDBC connection URL.'
390
+ if (name === 'DATABASE_URL') return 'Server-side PostgreSQL connection URL.'
263
391
  if (name === 'POSTGRES_USER') return 'Local/deployed database user.'
264
392
  if (name === 'POSTGRES_PASSWORD') return 'Database credential; replace the development example.'
265
393
  if (name === 'POSTGRES_DB') return 'Database name.'
@@ -267,7 +395,10 @@ function environmentPurpose(name) {
267
395
  if (name === 'SPRING_SECURITY_USER_NAME') return 'Development-only generated Spring login name; replace with the product identity store.'
268
396
  if (name === 'SPRING_SECURITY_USER_PASSWORD') return 'Development-only Spring login credential; never commit a real value.'
269
397
  if (name === 'OIDC_ISSUER_URI') return 'Trusted OpenID Connect issuer used to validate access tokens.'
398
+ if (name === 'OIDC_ISSUER') return 'Trusted OpenID Connect issuer used to validate access tokens.'
270
399
  if (name === 'OIDC_AUDIENCE') return 'Required audience for tokens accepted by this API.'
400
+ if (name === 'OIDC_ALGORITHMS') return 'Allowed signing algorithms for accepted access tokens.'
401
+ if (name === 'OIDC_JWKS_URL') return 'JWKS endpoint for access-token signature validation; defaults to the issuer well-known location.'
271
402
  if (name === 'SESSION_DOMAIN') return 'Exact domain scope for the secure session cookie.'
272
403
  if (name === 'SANCTUM_STATEFUL_DOMAINS') return 'Comma-separated first-party browser hosts allowed to use Sanctum session authentication.'
273
404
  if (name === 'CORS_ALLOWED_ORIGINS') return 'Exact browser origins allowed to make credentialed API requests.'
@@ -285,11 +416,24 @@ function authDocumentation(stack) {
285
416
  if (stack.authentication === 'laravel-session') return `# Authentication Flow\n\nLaravel owns the website session. Login regenerates the session identifier, logout invalidates the session and rotates the CSRF token, and protected routes use server-side authorization. The browser receives an HttpOnly session cookie; there is no browser refresh token. Keep CSRF protection enabled for every cookie-authenticated mutation.\n`
286
417
  if (stack.authentication === 'sanctum-spa') return `# Authentication Flow\n\nLaravel Sanctum uses Laravel's secure session cookie for this first-party browser application. The SPA first requests \`/sanctum/csrf-cookie\`, then sends credentialed login and API requests. Sanctum does not give this browser a custom bearer/refresh-token system. Configure exact stateful domains and CORS origins, and enforce resource authorization in Laravel.\n`
287
418
  if (stack.authentication === 'laravel-oidc') return `# Authentication Flow\n\nAuth0 owns login, access/refresh-token issuance, rotation, revocation, and recovery. Clients use Authorization Code with PKCE. Laravel uses the pinned Auth0 resource-server adapter to accept access tokens only and validate signature, issuer, audience, and time claims. Refresh tokens never go to this API. Configure \`AUTH0_DOMAIN\` and \`AUTH0_AUDIENCE\`; resource ownership and permission checks remain application responsibilities.\n`
419
+ if (stack.backendKey === 'fastapi') return `# Authentication Flow\n\nAn external OpenID Connect provider owns login, access/refresh token issuance, rotation, revocation, and recovery. Clients use Authorization Code with PKCE. FastAPI validates bearer access tokens: issuer, audience, algorithm, JWKS signature, expiry, and required claims on every protected request. Refresh tokens never go to the FastAPI resource API. Configure \`OIDC_ISSUER\` and \`OIDC_AUDIENCE\`, then test invalid and authorized tokens.\n`
288
420
  return `# Authentication Flow\n\nAn external OpenID Connect provider owns login, access/refresh token issuance, rotation, revocation, and recovery. Clients use Authorization Code with PKCE. Spring is an OAuth2 Resource Server: it accepts bearer access tokens and validates signature, issuer, audience, time, and authorities. Refresh tokens never go to the Spring resource API. Configure \`OIDC_ISSUER_URI\` and \`OIDC_AUDIENCE\`, then test invalid and authorized tokens.\n`
289
421
  }
290
422
 
291
423
  // ─── CI — template-driven ─────────────────────────────────────────────────────
292
424
 
425
+ function generatedSecurityWorkflow(stack) {
426
+ const hasJavaScript = stack.frontendKey !== 'no-frontend' &&
427
+ !(stack.frontendKey === 'laravel-ui' && stack.laravelUi !== 'inertia-react')
428
+ const npmDirectory = stack.frontendKey === 'react' ? 'frontend' : '.'
429
+ const laravelDirectory = ['laravel-ui', 'no-frontend'].includes(stack.frontendKey) ? '.' : 'backend'
430
+ const codeqlLanguages = [hasJavaScript && 'javascript-typescript', stack.backendKey === 'springboot' && 'java-kotlin'].filter(Boolean)
431
+ const npmAudit = hasJavaScript ? `\n npm-audit:\n runs-on: ubuntu-latest\n defaults:\n run:\n working-directory: ${npmDirectory}\n steps:\n - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4\n - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4\n with:\n node-version: "${stack.profile.runtimes.node}"\n cache: npm\n cache-dependency-path: ${npmDirectory === '.' ? 'package-lock.json' : `${npmDirectory}/package-lock.json`}\n - run: npm ci\n - run: npm audit --audit-level=high\n` : ''
432
+ const composerAudit = stack.backendKey === 'laravel' ? `\n composer-audit:\n runs-on: ubuntu-latest\n defaults:\n run:\n working-directory: ${laravelDirectory}\n steps:\n - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4\n - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2\n with:\n php-version: "${stack.profile.runtimes.php}"\n coverage: none\n - run: composer install --no-interaction --prefer-dist\n - run: composer audit --locked\n` : ''
433
+ const codeql = codeqlLanguages.length ? `\n codeql:\n permissions:\n contents: read\n security-events: write\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4\n - uses: github/codeql-action/init@5ba2889ada762081db2c4f32a729827dce632c7b # v3\n with:\n languages: ${codeqlLanguages.join(',')}\n - uses: github/codeql-action/analyze@5ba2889ada762081db2c4f32a729827dce632c7b # v3\n` : ''
434
+ return `name: Security\n\non:\n pull_request:\n branches: [dev, main]\n push:\n branches: [dev, main]\n schedule:\n - cron: '31 4 * * 1'\n\npermissions:\n contents: read\n\njobs:\n dependency-review:\n if: github.event_name == 'pull_request'\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4\n - uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0\n with:\n fail-on-severity: high\n\n secret-scan:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4\n with:\n fetch-depth: 0\n - uses: trufflesecurity/trufflehog@466da5b0bb161144f6afca9afe5d57975828c410 # v3.90.8\n with:\n extra_args: --results=verified,unknown\n${npmAudit}${composerAudit}${codeql}`
435
+ }
436
+
293
437
  async function generateCI(dest, stack, ciDir, answers, vars) {
294
438
  // Frontend CI — read from ci/{ciTemplate}.yml
295
439
  const feTpl = path.join(ciDir, `${stack.ciTemplate}.yml`)
@@ -339,12 +483,27 @@ async function generateCI(dest, stack, ciDir, answers, vars) {
339
483
  await write(dest, `.github/workflows/ci-backend.yml`, render(content, vars))
340
484
  }
341
485
  }
486
+ if (stack.backendKey === 'fastapi') {
487
+ const beTpl = path.join(ciDir, 'fastapi.yml')
488
+ if (await fs.pathExists(beTpl)) {
489
+ let content = await fs.readFile(beTpl, 'utf-8')
490
+ if (stack.frontendKey === 'no-frontend') {
491
+ content = content.replaceAll(' - backend/**', " - '**'")
492
+ .replaceAll('working-directory: backend', 'working-directory: .')
493
+ }
494
+ if (answers.testing === 'none') {
495
+ content = content.replace(' - name: Run tests\n run: uv run pytest\n', '')
496
+ }
497
+ await write(dest, `.github/workflows/ci-backend.yml`, render(content, vars))
498
+ }
499
+ }
500
+ await write(dest, '.github/workflows/security.yml', generatedSecurityWorkflow(stack))
342
501
  }
343
502
 
344
503
  // ─── Runnable application files ─────────────────────────────────────────────────
345
504
 
346
505
  async function generateRunnableFiles(dest, answers, stack, vars) {
347
- const files = { ...buildRunnableFiles(answers, stack, vars), ...buildLaravelFiles(answers, stack, vars) }
506
+ const files = buildRunnableFiles(answers, stack, vars)
348
507
  for (const [filePath, content] of Object.entries(files)) await write(dest, filePath, content)
349
508
  if (stack.backendKey === 'springboot') await fs.chmod(path.join(dest, 'backend/mvnw'), 0o755)
350
509
  if (stack.backendKey === 'laravel') {
@@ -354,13 +513,3 @@ async function generateRunnableFiles(dest, answers, stack, vars) {
354
513
  }
355
514
 
356
515
  // ─── Helpers ──────────────────────────────────────────────────────────────────
357
-
358
- async function write(dest, filePath, content) {
359
- const fullPath = path.join(dest, filePath)
360
- await fs.ensureDir(path.dirname(fullPath))
361
- await fs.writeFile(fullPath, content, 'utf-8')
362
- }
363
-
364
- async function writeTemplate(dest, filePath, content, vars) {
365
- await write(dest, filePath, render(content, vars))
366
- }
@@ -0,0 +1,4 @@
1
+ # Frontend stacks
2
+
3
+ Frontend implementations move here one focused phase at a time. Until then,
4
+ the compatibility exports keep existing `lib` imports stable.
@@ -0,0 +1,3 @@
1
+ export function ciContributions() {
2
+ return [{ template: 'nextjs', path: '.github/workflows/ci-frontend.yml' }]
3
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Builds the files owned by the Next.js frontend. Shared package, testing,
3
+ * feature, and backend integration builders are injected by the scaffold
4
+ * composer so this stack does not depend on unrelated implementations.
5
+ */
6
+ export function buildNextjsFiles(answers, stack, shared) {
7
+ const files = {}
8
+ files['package.json'] = packageFile(answers, stack)
9
+ files['.node-version'] = `${stack.profile.runtimes.node}\n`
10
+ files['.npmrc'] = 'engine-strict=true\n'
11
+ files['tsconfig.json'] = json({
12
+ compilerOptions: {
13
+ target: 'ES2017', lib: ['dom', 'dom.iterable', 'esnext'], allowJs: false,
14
+ skipLibCheck: true, strict: true, noEmit: true, esModuleInterop: true,
15
+ module: 'esnext', moduleResolution: 'bundler', resolveJsonModule: true,
16
+ isolatedModules: true, jsx: 'react-jsx', incremental: true,
17
+ plugins: [{ name: 'next' }], paths: { '@/*': ['./src/*'] },
18
+ },
19
+ include: ['next-env.d.ts', '**/*.ts', '**/*.tsx', '.next/types/**/*.ts', '.next/dev/types/**/*.ts'],
20
+ exclude: ['node_modules'],
21
+ })
22
+ files['next-env.d.ts'] = "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n"
23
+ files['eslint.config.mjs'] = "import { defineConfig, globalIgnores } from 'eslint/config'\nimport nextVitals from 'eslint-config-next/core-web-vitals'\nimport nextTs from 'eslint-config-next/typescript'\n\nexport default defineConfig([\n ...nextVitals,\n ...nextTs,\n globalIgnores(['.next/**', 'out/**', 'next-env.d.ts']),\n])\n"
24
+ files['next.config.ts'] = `import type { NextConfig } from 'next'\n\nconst securityHeaders = [\n { key: 'X-Content-Type-Options', value: 'nosniff' },\n { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },\n { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },\n { key: 'X-Frame-Options', value: 'DENY' },\n]\n\nconst nextConfig: NextConfig = {\n reactStrictMode: true,\n output: 'standalone',\n async headers() { return [{ source: '/(.*)', headers: securityHeaders }] },\n}\n\nexport default nextConfig\n`
25
+ files['postcss.config.mjs'] = stack.styleId === 'tailwind'
26
+ ? "const config = { plugins: { '@tailwindcss/postcss': {} } }\nexport default config\n"
27
+ : "const config = { plugins: {} }\nexport default config\n"
28
+ files['src/app/globals.css'] = `${stack.styleId === 'tailwind' ? '@import "tailwindcss";\n\n' : ''}:root { color-scheme: light dark; font-family: system-ui, sans-serif; }\n* { box-sizing: border-box; }\nbody { margin: 0; min-height: 100vh; }\nmain { max-width: 48rem; margin: 0 auto; padding: 4rem 1.5rem; }\na { color: inherit; }\n`
29
+ files['src/app/layout.tsx'] = `import type { Metadata } from 'next'\nimport type { ReactNode } from 'react'\nimport './globals.css'\n\nexport const metadata: Metadata = { title: ${JSON.stringify(answers.projectName)}, description: ${JSON.stringify(answers.projectDescription)} }\n\nexport default function RootLayout({ children }: Readonly<{ children: ReactNode }>) {\n return <html lang="en"><body>{children}</body></html>\n}\n`
30
+ files['src/app/page.tsx'] = stack.architecture === 'small'
31
+ ? `export default function HomePage() {\n return (\n <main>\n <p>create-win-project</p>\n <h1>Your starter is running</h1>\n <p>{${JSON.stringify(answers.projectDescription)}}</p>\n <p>Read <code>AGENTS.md</code> before your first agent-assisted change.</p>\n </main>\n )\n}\n`
32
+ : `${stack.architecture === 'large'
33
+ ? "import { getStarterStatus, StarterStatus } from '@/features/status'"
34
+ : "import { StarterStatus } from '@/features/status/components/StarterStatus'\nimport { getStarterStatus } from '@/features/status/services/getStarterStatus'"}\n\nexport default function HomePage() {\n const status = getStarterStatus()\n return <main><p>create-win-project</p><StarterStatus status={status} /><p>{${JSON.stringify(answers.projectDescription)}}</p><p>Read <code>AGENTS.md</code> before your first agent-assisted change.</p></main>\n}\n`
35
+ files['src/app/api/health/route.ts'] = "export function GET() {\n return Response.json({ status: 'ok' })\n}\n"
36
+ files['src/app/page.test.tsx'] = `import { expect, test } from 'vitest'\nimport { render, screen } from '@testing-library/react'\nimport HomePage from './page'\n\ntest('renders the starter heading', () => {\n render(<HomePage />)\n expect(screen.getByRole('heading', { name: 'Your starter is running' })).toBeInTheDocument()\n})\n`
37
+ if ((answers.testing || 'basic') === 'none') delete files['src/app/page.test.tsx']
38
+ addTestingFiles(files, '', stack, answers.testing || 'basic')
39
+ Object.assign(files, shared.statusFeatureFiles('', stack))
40
+ if (stack.backendKey === 'supabase') Object.assign(files, shared.supabaseWebFiles(true, stack.authentication === 'supabase'))
41
+ if (stack.backendKey === 'postgres') Object.assign(files, shared.prismaFiles())
42
+ return files
43
+ }
44
+ import { json } from '../../shared/javascript-package.js'
45
+ import { addTestingFiles } from '../../shared/testing-files.js'
46
+ import { packageFile } from './dependencies.js'
@@ -0,0 +1,5 @@
1
+ import { buildJavaScriptPackage } from '../../shared/javascript-package.js'
2
+
3
+ export function packageFile(answers, stack) {
4
+ return buildJavaScriptPackage(answers, stack, 'nextjs')
5
+ }
@@ -0,0 +1,3 @@
1
+ export function dockerContributions() {
2
+ return [{ template: 'nextjs', developmentPath: 'Dockerfile.dev', productionPath: 'Dockerfile' }]
3
+ }
@@ -0,0 +1,7 @@
1
+ import { partitionEnvironment, renderEnvironment } from '../../shared/environment.js'
2
+
3
+ export function buildEnvironmentFiles(answers, stack) {
4
+ const { publicNames, serverNames } = partitionEnvironment(stack)
5
+ const names = stack.backendKey === 'supabase' ? stack.env : [...publicNames, ...serverNames]
6
+ return { '.env.example': renderEnvironment(names, answers) }
7
+ }
@@ -0,0 +1,35 @@
1
+ import { defineStackAdapter } from '../../rules.js'
2
+ import { ciContributions } from './ci.js'
3
+ import { dockerContributions } from './docker.js'
4
+ import { buildNextjsFiles } from './create-files.js'
5
+
6
+ const verificationCases = Object.freeze([
7
+ Object.freeze({ backend: 'none', styling: 'tailwind', architecture: 'small', authentication: 'public' }),
8
+ Object.freeze({ backend: 'postgres', styling: 'tailwind', architecture: 'medium', authentication: 'undecided' }),
9
+ Object.freeze({ backend: 'supabase', styling: 'tailwind', architecture: 'large', authentication: 'supabase' }),
10
+ Object.freeze({ backend: 'springboot', styling: 'css-modules', architecture: 'medium', authentication: 'session' }),
11
+ Object.freeze({ backend: 'laravel', styling: 'tailwind', architecture: 'medium', authentication: 'sanctum-spa' }),
12
+ ])
13
+
14
+ export const nextjsAdapter = defineStackAdapter({
15
+ id: 'nextjs',
16
+ kind: 'frontend',
17
+ label: 'Next.js',
18
+ compatibleWith: {
19
+ backend: ['none', 'postgres', 'supabase', 'springboot', 'laravel', 'fastapi'],
20
+ },
21
+ capabilities: {
22
+ applicationShapes: ['fullstack', 'separate'],
23
+ architectureProfiles: ['small', 'medium', 'large'],
24
+ authenticationModels: ['public', 'undecided', 'supabase', 'session', 'oidc', 'sanctum-spa', 'laravel-oidc'],
25
+ runtime: 'node',
26
+ },
27
+ contributes: {
28
+ files: ({ answers, stack, shared }) => Object.entries(buildNextjsFiles(answers, stack, shared)),
29
+ environment: ({ backend }) => backend.id === 'none' ? [] : ['API_URL'],
30
+ install: () => [{ cwd: '.', command: 'npm', args: ['install'] }],
31
+ docker: dockerContributions,
32
+ ci: ciContributions,
33
+ verification: () => verificationCases,
34
+ },
35
+ })
@@ -0,0 +1,3 @@
1
+ export function ciContributions() {
2
+ return [{ template: 'expo', path: '.github/workflows/ci-frontend.yml' }]
3
+ }
@@ -0,0 +1,28 @@
1
+ import { json } from '../../shared/javascript-package.js'
2
+ import { addTestingFiles } from '../../shared/testing-files.js'
3
+ import { packageFile } from './dependencies.js'
4
+
5
+ export function buildReactNativeFiles(answers, stack, shared) {
6
+ const files = {
7
+ 'package.json': packageFile(answers, stack),
8
+ '.node-version': `${stack.profile.runtimes.node}\n`,
9
+ '.npmrc': 'engine-strict=true\n',
10
+ 'app.json': json({ expo: { name: answers.projectName, slug: answers.projectName, version: '1.0.0', orientation: 'portrait', scheme: answers.projectName, userInterfaceStyle: 'automatic', runtimeVersion: { policy: 'appVersion' }, updates: { checkAutomatically: 'ON_LOAD', fallbackToCacheTimeout: 0 }, plugins: stack.authentication === 'supabase' ? ['expo-router', 'expo-secure-store'] : ['expo-router'], experiments: { typedRoutes: true } } }),
11
+ 'eas.json': json({ cli: { version: '>= 16.0.0', appVersionSource: 'remote' }, build: { development: { developmentClient: true, distribution: 'internal', channel: 'development' }, preview: { distribution: 'internal', channel: 'preview' }, production: { autoIncrement: true, channel: 'production' } }, submit: { production: {} } }),
12
+ 'lib/deep-links.ts': `const allowedRoutes = new Set(['/', '/login'])\n\nexport function acceptDeepLink(value: string, scheme = '${answers.projectName}') {\n try {\n const url = new URL(value)\n if (url.protocol !== \`${'${scheme}'}:\`) return false\n const route = \`/${'${url.host}'}${'${url.pathname}'}\`.replace(/\\/$/, '') || '/'\n return allowedRoutes.has(route)\n } catch { return false }\n}\n`,
13
+ 'docs/mobile/production.md': `# Mobile production\n\nEAS development, preview, and production builds use separate channels and environment variables. Keep signing credentials, service-role keys, and privileged operations outside the client. Sessions use platform SecureStore where authentication is enabled. Only allowlisted custom-scheme routes are accepted.\n\nRuntime compatibility follows the app version; publish updates only after testing the matching native runtime. Request the minimum native permissions and keep store privacy disclosures aligned with actual collection. Verify authentication, deep links, notifications, permissions, updates, and offline behavior on physical iOS and Android devices before release.\n`,
14
+ 'tsconfig.json': json({ extends: 'expo/tsconfig.base', compilerOptions: { strict: true, types: ['jest'], paths: { '@/*': ['./*'] } }, include: ['**/*.ts', '**/*.tsx', '.expo/types/**/*.ts', 'expo-env.d.ts'] }),
15
+ 'expo-env.d.ts': "/// <reference types=\"expo/types\" />\n",
16
+ 'app/_layout.tsx': stack.authentication === 'supabase'
17
+ ? `import { Stack } from 'expo-router'\nimport { useEffect } from 'react'\nimport { bindSupabaseAuthLifecycle } from '@/lib/supabase-lifecycle'\n\nexport default function RootLayout() {\n useEffect(() => bindSupabaseAuthLifecycle(), [])\n return <Stack screenOptions={{ headerTitle: '${answers.projectName}' }} />\n}\n`
18
+ : `import { Stack } from 'expo-router'\n\nexport default function RootLayout() { return <Stack screenOptions={{ headerTitle: '${answers.projectName}' }} /> }\n`,
19
+ 'app/index.tsx': stack.architecture === 'small'
20
+ ? `import { StyleSheet, Text, View } from 'react-native'\nimport { SafeAreaView } from 'react-native-safe-area-context'\n\nexport default function HomeScreen() {\n return <SafeAreaView style={styles.safe}><View style={styles.container}><Text>create-win-project</Text><Text accessibilityRole="header">Your starter is running</Text><Text>{${JSON.stringify(answers.projectDescription)}}</Text></View></SafeAreaView>\n}\nconst styles = StyleSheet.create({ safe: { flex: 1 }, container: { flex: 1, justifyContent: 'center', padding: 24, gap: 12 } })\n`
21
+ : `import { StyleSheet, Text, View } from 'react-native'\nimport { SafeAreaView } from 'react-native-safe-area-context'\n${stack.architecture === 'large'
22
+ ? "import { getStarterStatus, StarterStatus } from '@/features/status'"
23
+ : "import { StarterStatus } from '@/features/status/components/StarterStatus'\nimport { getStarterStatus } from '@/features/status/services/getStarterStatus'"}\n\nexport default function HomeScreen() {\n const status = getStarterStatus()\n return <SafeAreaView style={styles.safe}><View style={styles.container}><Text>create-win-project</Text><StarterStatus status={status} /><Text>{${JSON.stringify(answers.projectDescription)}}</Text></View></SafeAreaView>\n}\nconst styles = StyleSheet.create({ safe: { flex: 1 }, container: { flex: 1, justifyContent: 'center', padding: 24, gap: 12 } })\n`,
24
+ }
25
+ addTestingFiles(files, '', stack, answers.testing || 'basic')
26
+ Object.assign(files, shared.nativeStatusFeatureFiles(stack))
27
+ return files
28
+ }
@@ -0,0 +1,5 @@
1
+ import { buildJavaScriptPackage } from '../../shared/javascript-package.js'
2
+
3
+ export function packageFile(answers, stack) {
4
+ return buildJavaScriptPackage(answers, stack, 'react-native')
5
+ }