create-win-project 1.3.0 → 1.4.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 (232) hide show
  1. package/README.md +19 -28
  2. package/{scripts/compatibility-matrix.mjs → checks/check-compatibility.js} +24 -6
  3. package/{scripts/verify-generated.mjs → checks/check-generated-project.js} +2 -2
  4. package/{scripts/validate-content.mjs → checks/check-library.js} +5 -5
  5. package/checks/classify-changes.js +39 -0
  6. package/docs/ARCHITECTURE.md +83 -40
  7. package/docs/CONTENT_MODEL.md +3 -3
  8. package/docs/CONTRIBUTING.md +154 -0
  9. package/docs/DEPENDENCY_MAINTENANCE.md +1 -1
  10. package/index.js +2 -429
  11. package/{playbooks → library}/INDEX.md +4 -4
  12. package/{playbooks/capabilities/ci/github-actions.manifest.json → library/development-tools/ci/definition.json} +1 -0
  13. package/{playbooks/devops/makefile.manifest.json → library/development-tools/devops/makefile/definition.json} +1 -0
  14. package/{playbooks/devops/pr-template.manifest.json → library/development-tools/devops/pr-template/definition.json} +1 -0
  15. package/{playbooks/platform/mobile.manifest.json → library/platforms/mobile/definition.json} +1 -0
  16. package/{playbooks/platform/web.manifest.json → library/platforms/web/definition.json} +1 -0
  17. package/{compatibility/profiles.json → library/tested-versions.json} +4 -0
  18. package/{playbooks/universal/accessibility.manifest.json → library/universal/accessibility/definition.json} +1 -1
  19. package/{playbooks/universal/coding-rules.manifest.json → library/universal/coding-rules/definition.json} +1 -0
  20. package/{playbooks/universal/error-handling.manifest.json → library/universal/error-handling/definition.json} +1 -0
  21. package/{playbooks/universal/git-conventions.manifest.json → library/universal/git-conventions/definition.json} +1 -0
  22. package/{playbooks/universal/observability.manifest.json → library/universal/observability/definition.json} +1 -0
  23. package/{playbooks/universal/security.manifest.json → library/universal/security/definition.json} +1 -0
  24. package/{playbooks/universal/typescript.manifest.json → library/universal/typescript/definition.json} +1 -0
  25. package/package.json +12 -11
  26. package/public/logo.svg +19 -0
  27. package/src/cli/arguments.js +52 -0
  28. package/src/cli/display.js +220 -0
  29. package/src/cli/main.js +185 -0
  30. package/src/cli/navigation.js +156 -0
  31. package/src/cli/questions.js +187 -0
  32. package/src/cli/system-check.js +138 -0
  33. package/src/engine/create-project.js +10 -0
  34. package/src/engine/install-dependencies.js +13 -0
  35. package/{lib/catalog.js → src/engine/load-library.js} +5 -6
  36. package/{lib/playbooks.js → src/engine/project-guidance.js} +25 -3
  37. package/{lib/template.js → src/engine/render-templates.js} +8 -0
  38. package/src/engine/write-files.js +34 -0
  39. package/{lib/stacks/registry.js → src/stacks/available-stacks.js} +23 -1
  40. package/src/stacks/backends/README.md +5 -0
  41. package/{lib/stacks → src/stacks/backends}/laravel/architecture.js +1 -0
  42. package/{lib/stacks → src/stacks/backends}/laravel/auth/oidc.js +1 -0
  43. package/{lib/stacks → src/stacks/backends}/laravel/auth/public.js +1 -0
  44. package/{lib/stacks → src/stacks/backends}/laravel/auth/sanctum.js +1 -0
  45. package/{lib/stacks → src/stacks/backends}/laravel/auth/session.js +1 -0
  46. package/src/stacks/backends/laravel/ci.js +3 -0
  47. package/{lib/stacks → src/stacks/backends}/laravel/composer.js +1 -1
  48. package/{lib/stacks/laravel/generate.js → src/stacks/backends/laravel/create-files.js} +1 -0
  49. package/src/stacks/backends/laravel/docker.js +14 -0
  50. package/src/stacks/backends/laravel/environment.js +5 -0
  51. package/{lib/stacks → src/stacks/backends}/laravel/index.js +11 -3
  52. package/{lib/stacks → src/stacks/backends}/laravel/ui/blade.js +1 -0
  53. package/{lib/stacks → src/stacks/backends}/laravel/ui/index.js +1 -0
  54. package/{lib/stacks → src/stacks/backends}/laravel/ui/inertia-react.js +4 -2
  55. package/{lib/stacks → src/stacks/backends}/laravel/ui/livewire.js +1 -0
  56. package/{lib/stacks → src/stacks/backends}/laravel/ui/shared.js +1 -0
  57. package/src/stacks/backends/none/index.js +23 -0
  58. package/src/stacks/backends/postgres/ci.js +3 -0
  59. package/src/stacks/backends/postgres/create-files.js +21 -0
  60. package/src/stacks/backends/postgres/docker.js +3 -0
  61. package/src/stacks/backends/postgres/environment.js +3 -0
  62. package/src/stacks/backends/postgres/index.js +30 -0
  63. package/src/stacks/backends/springboot/ci.js +3 -0
  64. package/src/stacks/backends/springboot/create-files.js +91 -0
  65. package/src/stacks/backends/springboot/docker.js +3 -0
  66. package/src/stacks/backends/springboot/environment.js +3 -0
  67. package/src/stacks/backends/springboot/index.js +31 -0
  68. package/src/stacks/backends/supabase/ci.js +3 -0
  69. package/src/stacks/backends/supabase/create-files.js +52 -0
  70. package/src/stacks/backends/supabase/docker.js +3 -0
  71. package/src/stacks/backends/supabase/environment.js +3 -0
  72. package/src/stacks/backends/supabase/index.js +31 -0
  73. package/src/stacks/backends/supabase/native.js +14 -0
  74. package/src/stacks/compose-files.js +241 -0
  75. package/{lib/generator.js → src/stacks/create-project.js} +78 -50
  76. package/src/stacks/frontends/README.md +4 -0
  77. package/src/stacks/frontends/nextjs/ci.js +3 -0
  78. package/src/stacks/frontends/nextjs/create-files.js +46 -0
  79. package/src/stacks/frontends/nextjs/dependencies.js +5 -0
  80. package/src/stacks/frontends/nextjs/docker.js +3 -0
  81. package/src/stacks/frontends/nextjs/environment.js +7 -0
  82. package/src/stacks/frontends/nextjs/index.js +35 -0
  83. package/src/stacks/frontends/react-native/ci.js +3 -0
  84. package/src/stacks/frontends/react-native/create-files.js +25 -0
  85. package/src/stacks/frontends/react-native/dependencies.js +5 -0
  86. package/src/stacks/frontends/react-native/docker.js +5 -0
  87. package/src/stacks/frontends/react-native/environment.js +7 -0
  88. package/src/stacks/frontends/react-native/index.js +30 -0
  89. package/src/stacks/frontends/react-vite/ci.js +3 -0
  90. package/src/stacks/frontends/react-vite/create-files.js +27 -0
  91. package/src/stacks/frontends/react-vite/dependencies.js +5 -0
  92. package/src/stacks/frontends/react-vite/docker.js +3 -0
  93. package/src/stacks/frontends/react-vite/environment.js +8 -0
  94. package/src/stacks/frontends/react-vite/index.js +32 -0
  95. package/{lib → src}/stacks/shared/contributions.js +1 -1
  96. package/src/stacks/shared/environment.js +30 -0
  97. package/src/stacks/shared/javascript-package.js +111 -0
  98. package/src/stacks/shared/testing-files.js +16 -0
  99. package/templates/docker/compose/postgres.yml +1 -1
  100. package/templates/docker/compose/springboot.yml +3 -3
  101. package/templates/docker/compose/supabase.yml +1 -1
  102. package/templates/docker/compose-prod/springboot.yml +1 -1
  103. package/lib/banner.js +0 -45
  104. package/lib/constants.js +0 -3
  105. package/lib/doctor.js +0 -45
  106. package/lib/interview.js +0 -77
  107. package/lib/laravel-scaffold.js +0 -3
  108. package/lib/scaffold.js +0 -467
  109. package/lib/stacks/index.js +0 -8
  110. /package/{playbooks/capabilities → library/development-tools}/ci/github-actions.md +0 -0
  111. /package/{playbooks/devops → library/development-tools/devops/makefile}/makefile.md +0 -0
  112. /package/{playbooks/devops → library/development-tools/devops/pr-template}/pr-template.md +0 -0
  113. /package/{playbooks/capabilities/docker/docker.manifest.json → library/development-tools/docker/definition.json} +0 -0
  114. /package/{playbooks/capabilities → library/development-tools}/docker/overview.md +0 -0
  115. /package/{playbooks/capabilities → library/features}/auth/oidc-resource-server.md +0 -0
  116. /package/{playbooks/capabilities → library/features}/auth/spring-session.md +0 -0
  117. /package/{playbooks/capabilities/flyway/flyway.manifest.json → library/features/flyway/definition.json} +0 -0
  118. /package/{playbooks/capabilities → library/features}/flyway/environments.md +0 -0
  119. /package/{playbooks/capabilities → library/features}/flyway/migrations.md +0 -0
  120. /package/{playbooks/capabilities → library/features}/flyway/testing.md +0 -0
  121. /package/{playbooks/capabilities → library/features}/laravel/database.md +0 -0
  122. /package/{playbooks/capabilities → library/features}/laravel/migrations.md +0 -0
  123. /package/{playbooks/capabilities → library/features}/laravel/observability.md +0 -0
  124. /package/{playbooks/capabilities → library/features}/laravel/oidc-resource-server.md +0 -0
  125. /package/{playbooks/capabilities → library/features}/laravel/queues.md +0 -0
  126. /package/{playbooks/capabilities → library/features}/laravel/sanctum-spa.md +0 -0
  127. /package/{playbooks/capabilities → library/features}/laravel/scheduler.md +0 -0
  128. /package/{playbooks/capabilities → library/features}/laravel/session-auth.md +0 -0
  129. /package/{playbooks/capabilities → library/features}/laravel/storage-uploads.md +0 -0
  130. /package/{playbooks/capabilities → library/features}/postgresql/architecture.md +0 -0
  131. /package/{playbooks/capabilities/postgresql/postgresql.manifest.json → library/features/postgresql/definition.json} +0 -0
  132. /package/{playbooks/capabilities → library/features}/postgresql/migrations.md +0 -0
  133. /package/{playbooks/capabilities → library/features}/postgresql/schema-design.md +0 -0
  134. /package/{playbooks/capabilities → library/features}/postgresql/security.md +0 -0
  135. /package/{playbooks/capabilities → library/features}/postgresql/testing.md +0 -0
  136. /package/{playbooks/capabilities → library/features}/prisma/architecture.md +0 -0
  137. /package/{playbooks/capabilities/prisma/prisma.manifest.json → library/features/prisma/definition.json} +0 -0
  138. /package/{playbooks/capabilities → library/features}/prisma/migrations.md +0 -0
  139. /package/{playbooks/capabilities → library/features}/prisma/runtime.md +0 -0
  140. /package/{playbooks/capabilities → library/features}/prisma/schema.md +0 -0
  141. /package/{playbooks/capabilities → library/features}/prisma/testing.md +0 -0
  142. /package/{playbooks/capabilities → library/features}/supabase/architecture.md +0 -0
  143. /package/{playbooks/capabilities → library/features}/supabase/authentication.md +0 -0
  144. /package/{playbooks/capabilities/supabase/supabase.manifest.json → library/features/supabase/definition.json} +0 -0
  145. /package/{playbooks/capabilities → library/features}/supabase/expo.md +0 -0
  146. /package/{playbooks/capabilities → library/features}/supabase/migrations.md +0 -0
  147. /package/{playbooks/capabilities → library/features}/supabase/nextjs.md +0 -0
  148. /package/{playbooks/capabilities → library/features}/supabase/rls.md +0 -0
  149. /package/{playbooks/capabilities → library/features}/supabase/testing.md +0 -0
  150. /package/{playbooks/capabilities → library/features}/supabase/vite.md +0 -0
  151. /package/{playbooks → library/optional-features}/concerns/axios.md +0 -0
  152. /package/{playbooks → library/optional-features}/concerns/next-safe-action.md +0 -0
  153. /package/{playbooks → library/optional-features}/concerns/next-themes.md +0 -0
  154. /package/{playbooks → library/optional-features}/concerns/nuqs.md +0 -0
  155. /package/{playbooks → library/optional-features}/concerns/t3-env.md +0 -0
  156. /package/{playbooks → library/optional-features}/concerns/tanstack-query.md +0 -0
  157. /package/{playbooks → library/optional-features}/concerns/zod.md +0 -0
  158. /package/{playbooks → library/optional-features}/concerns/zustand.md +0 -0
  159. /package/{playbooks/styling → library/optional-features/styling/css-modules}/css-modules-extensions.md +0 -0
  160. /package/{playbooks/styling/css-modules.manifest.json → library/optional-features/styling/css-modules/definition.json} +0 -0
  161. /package/{playbooks/styling/native-styles.manifest.json → library/optional-features/styling/native-styles/definition.json} +0 -0
  162. /package/{playbooks/styling → library/optional-features/styling/native-styles}/native-styles.md +0 -0
  163. /package/{playbooks/styling/tailwind.manifest.json → library/optional-features/styling/tailwind/definition.json} +0 -0
  164. /package/{playbooks/styling → library/optional-features/styling/tailwind}/tailwind-extensions.md +0 -0
  165. /package/{playbooks/platform → library/platforms}/laravel-ui/blade/architecture.md +0 -0
  166. /package/{playbooks/platform → library/platforms}/laravel-ui/blade/runtime.md +0 -0
  167. /package/{playbooks/platform → library/platforms}/laravel-ui/blade/security.md +0 -0
  168. /package/{playbooks/platform → library/platforms}/laravel-ui/blade/structure.md +0 -0
  169. /package/{playbooks/platform → library/platforms}/laravel-ui/blade/testing.md +0 -0
  170. /package/{playbooks/stack/laravel-ui.manifest.json → library/platforms/laravel-ui/definition.json} +0 -0
  171. /package/{playbooks/platform → library/platforms}/laravel-ui/inertia-react/architecture.md +0 -0
  172. /package/{playbooks/platform → library/platforms}/laravel-ui/inertia-react/runtime.md +0 -0
  173. /package/{playbooks/platform → library/platforms}/laravel-ui/inertia-react/security.md +0 -0
  174. /package/{playbooks/platform → library/platforms}/laravel-ui/inertia-react/structure.md +0 -0
  175. /package/{playbooks/platform → library/platforms}/laravel-ui/inertia-react/testing.md +0 -0
  176. /package/{playbooks/platform → library/platforms}/laravel-ui/livewire/architecture.md +0 -0
  177. /package/{playbooks/platform → library/platforms}/laravel-ui/livewire/runtime.md +0 -0
  178. /package/{playbooks/platform → library/platforms}/laravel-ui/livewire/security.md +0 -0
  179. /package/{playbooks/platform → library/platforms}/laravel-ui/livewire/structure.md +0 -0
  180. /package/{playbooks/platform → library/platforms}/laravel-ui/livewire/testing.md +0 -0
  181. /package/{playbooks/platform → library/platforms/mobile}/mobile.md +0 -0
  182. /package/{playbooks/platform → library/platforms/web}/web.md +0 -0
  183. /package/{playbooks/stack → library/stacks}/expo/architecture.md +0 -0
  184. /package/{playbooks/stack/react-native.manifest.json → library/stacks/expo/definition.json} +0 -0
  185. /package/{playbooks/stack → library/stacks}/expo/runtime.md +0 -0
  186. /package/{playbooks/stack → library/stacks}/expo/security.md +0 -0
  187. /package/{playbooks/stack → library/stacks}/expo/structure.md +0 -0
  188. /package/{playbooks/stack → library/stacks}/expo/testing.md +0 -0
  189. /package/{playbooks/stack → library/stacks}/laravel/architecture.md +0 -0
  190. /package/{playbooks/stack/laravel.manifest.json → library/stacks/laravel/definition.json} +0 -0
  191. /package/{playbooks/stack → library/stacks}/laravel/runtime.md +0 -0
  192. /package/{playbooks/stack → library/stacks}/laravel/security.md +0 -0
  193. /package/{playbooks/stack → library/stacks}/laravel/structure.md +0 -0
  194. /package/{playbooks/stack → library/stacks}/laravel/testing.md +0 -0
  195. /package/{playbooks/stack → library/stacks}/nextjs/architecture.md +0 -0
  196. /package/{playbooks/stack/nextjs.manifest.json → library/stacks/nextjs/definition.json} +0 -0
  197. /package/{playbooks/stack → library/stacks}/nextjs/runtime.md +0 -0
  198. /package/{playbooks/stack → library/stacks}/nextjs/security.md +0 -0
  199. /package/{playbooks/stack → library/stacks}/nextjs/structure.md +0 -0
  200. /package/{playbooks/stack → library/stacks}/nextjs/testing.md +0 -0
  201. /package/{playbooks/stack/no-frontend.manifest.json → library/stacks/no-frontend/definition.json} +0 -0
  202. /package/{playbooks/stack/none.manifest.json → library/stacks/none/definition.json} +0 -0
  203. /package/{playbooks/stack → library/stacks}/react-vite/architecture.md +0 -0
  204. /package/{playbooks/stack/react-vite.manifest.json → library/stacks/react-vite/definition.json} +0 -0
  205. /package/{playbooks/stack → library/stacks}/react-vite/runtime.md +0 -0
  206. /package/{playbooks/stack → library/stacks}/react-vite/security.md +0 -0
  207. /package/{playbooks/stack → library/stacks}/react-vite/structure.md +0 -0
  208. /package/{playbooks/stack → library/stacks}/react-vite/testing.md +0 -0
  209. /package/{playbooks/stack → library/stacks}/springboot/architecture.md +0 -0
  210. /package/{playbooks/stack/springboot.manifest.json → library/stacks/springboot/definition.json} +0 -0
  211. /package/{playbooks/stack → library/stacks}/springboot/runtime.md +0 -0
  212. /package/{playbooks/stack → library/stacks}/springboot/security.md +0 -0
  213. /package/{playbooks/stack → library/stacks}/springboot/structure.md +0 -0
  214. /package/{playbooks/stack → library/stacks}/springboot/testing.md +0 -0
  215. /package/{playbooks/universal → library/universal/accessibility}/accessibility.md +0 -0
  216. /package/{playbooks/universal → library/universal/coding-rules}/coding-rules.md +0 -0
  217. /package/{playbooks/universal → library/universal/error-handling}/error-handling.md +0 -0
  218. /package/{playbooks/universal → library/universal/git-conventions}/git-conventions.md +0 -0
  219. /package/{playbooks/universal → library/universal/observability}/observability.md +0 -0
  220. /package/{playbooks/universal → library/universal/security}/security.md +0 -0
  221. /package/{playbooks/universal → library/universal/typescript}/typescript.md +0 -0
  222. /package/{lib/files.js → src/engine/project-files.js} +0 -0
  223. /package/{lib → src/engine}/project-location.js +0 -0
  224. /package/{lib/application-shapes.js → src/engine/project-shapes.js} +0 -0
  225. /package/{lib/compatibility.js → src/engine/tested-versions.js} +0 -0
  226. /package/{lib → src}/stacks/context.js +0 -0
  227. /package/{lib/stacks/contract.js → src/stacks/rules.js} +0 -0
  228. /package/{ci → templates/ci}/expo.yml +0 -0
  229. /package/{ci → templates/ci}/laravel.yml +0 -0
  230. /package/{ci → templates/ci}/nextjs.yml +0 -0
  231. /package/{ci → templates/ci}/springboot.yml +0 -0
  232. /package/{ci → templates/ci}/vite.yml +0 -0
@@ -0,0 +1,5 @@
1
+ # Backend stacks
2
+
3
+ Backend implementations move here one focused phase at a time. Laravel has an
4
+ initial compatibility export; other stacks remain in their existing modules
5
+ until their roadmap phases.
@@ -25,3 +25,4 @@ arch('application does not depend on http adapters')
25
25
  }
26
26
  return files
27
27
  }
28
+
@@ -11,3 +11,4 @@ export const laravelOidcAuthentication = Object.freeze({
11
11
  export function isLaravelOidc(authentication) {
12
12
  return authentication === laravelOidcAuthentication.id
13
13
  }
14
+
@@ -26,3 +26,4 @@ use Illuminate\\Support\\Facades\\Route;
26
26
 
27
27
  ${home}`)
28
28
  }
29
+
@@ -12,3 +12,4 @@ export const sanctumSpaAuthentication = Object.freeze({
12
12
  export function isSanctumSpa(authentication) {
13
13
  return authentication === sanctumSpaAuthentication.id
14
14
  }
15
+
@@ -15,3 +15,4 @@ export function usesLaravelSession(authentication) {
15
15
  export function sessionEnvironment(authentication) {
16
16
  return usesLaravelSession(authentication) ? laravelSessionAuthentication.environment : []
17
17
  }
18
+
@@ -0,0 +1,3 @@
1
+ export function ciContributions() {
2
+ return [{ template: 'laravel', path: '.github/workflows/ci-backend.yml' }]
3
+ }
@@ -1,4 +1,4 @@
1
- import { composerPackageVersion } from '../../compatibility.js'
1
+ import { composerPackageVersion } from '../../shared/javascript-package.js'
2
2
  import { usesLaravelSession } from './auth/session.js'
3
3
  import { isSanctumSpa, sanctumSpaAuthentication } from './auth/sanctum.js'
4
4
  import { isLaravelOidc, laravelOidcAuthentication } from './auth/oidc.js'
@@ -360,3 +360,4 @@ it('reports application health', function () {
360
360
  files['README.md'] = `# ${answers.projectName}\n\n> ${answers.projectDescription}\n\n## Start Laravel\n\n\`\`\`bash\n${root ? `cd ${root.slice(0, -1)}\n` : ''}cp .env.example .env\ncomposer install\nphp artisan key:generate\nphp artisan serve\n\`\`\`\n\nHealth: \`GET /api/health\`\n\n## Validate\n\n\`\`\`bash\ncomposer check\n\`\`\`\n`
361
361
  return files
362
362
  }
363
+
@@ -0,0 +1,14 @@
1
+ export function laravelCompose(_answers, stack, vars) {
2
+ const laravelDir = stack.frontendKey === 'laravel-ui' || stack.frontendKey === 'no-frontend' ? '.' : './backend'
3
+ const frontend = stack.frontendKey === 'react'
4
+ ? ` frontend:\n build:\n context: ./frontend\n dockerfile: Dockerfile.dev\n ports:\n - "\${FRONTEND_HOST_PORT:-5173}:5173"\n volumes:\n - ./frontend:/app\n - frontend-node-modules:/app/node_modules\n environment:\n VITE_API_URL: http://backend:8000\n depends_on:\n - backend\n\n`
5
+ : stack.frontendKey === 'nextjs'
6
+ ? ` frontend:\n build:\n context: .\n dockerfile: Dockerfile.dev\n ports:\n - "\${FRONTEND_HOST_PORT:-3000}:3000"\n volumes:\n - .:/app\n - /app/backend\n - frontend-node-modules:/app/node_modules\n environment:\n NEXT_PUBLIC_API_URL: http://backend:8000\n depends_on:\n - backend\n\n`
7
+ : ''
8
+ const frontendVolume = frontend ? ' frontend-node-modules:\n' : ''
9
+ return `services:\n${frontend} backend:\n build:\n context: ${laravelDir}\n dockerfile: Dockerfile.dev\n ports:\n - "\${BACKEND_HOST_PORT:-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 - "\${POSTGRES_HOST_PORT:-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`
10
+ }
11
+
12
+ export function dockerContributions() {
13
+ return [{ template: 'laravel', path: 'docker-compose.yml' }]
14
+ }
@@ -0,0 +1,5 @@
1
+ // Laravel auth modules add their model-specific names while catalog resolution
2
+ // builds the final ordered environment list.
3
+ export function environmentContributions({ stack } = {}) {
4
+ return stack?.env ? [...stack.env] : []
5
+ }
@@ -1,4 +1,8 @@
1
- import { defineStackAdapter } from '../contract.js'
1
+ import { defineStackAdapter } from '../../rules.js'
2
+ import { buildLaravelFiles } from './create-files.js'
3
+ import { dockerContributions } from './docker.js'
4
+ import { ciContributions } from './ci.js'
5
+ import { environmentContributions } from './environment.js'
2
6
 
3
7
  export const laravelAdapter = defineStackAdapter({
4
8
  id: 'laravel',
@@ -13,6 +17,10 @@ export const laravelAdapter = defineStackAdapter({
13
17
  authenticationModels: ['public', 'undecided', 'laravel-session', 'sanctum-spa', 'laravel-oidc'],
14
18
  runtime: 'php',
15
19
  },
16
- contributes: {},
20
+ contributes: {
21
+ files: ({ answers, stack, vars }) => Object.entries(buildLaravelFiles(answers, stack, vars)),
22
+ environment: environmentContributions,
23
+ docker: dockerContributions,
24
+ ci: ciContributions,
25
+ },
17
26
  })
18
-
@@ -13,3 +13,4 @@ export const bladeUi = Object.freeze({
13
13
  }
14
14
  },
15
15
  })
16
+
@@ -27,3 +27,4 @@ export function laravelUiPromptContribution(argument) {
27
27
  }],
28
28
  }
29
29
  }
30
+
@@ -1,4 +1,4 @@
1
- import { packageVersion } from '../../../compatibility.js'
1
+ import { packageVersion } from '../../../shared/javascript-package.js'
2
2
  import { buildLaravelAuthView } from './shared.js'
3
3
 
4
4
  const php = (value) => `${value.trim()}\n`
@@ -15,7 +15,9 @@ export const inertiaReactUi = Object.freeze({
15
15
  const npm = (name) => packageVersion(stack.profile, name, 'laravel-ui', 'Laravel Inertia scaffold')
16
16
  return {
17
17
  ...buildLaravelAuthView(stack.authentication),
18
- 'package.json': json({ private: true, type: 'module', scripts: { dev: 'vite', build: 'vite build' }, dependencies: { '@inertiajs/react': npm('@inertiajs/react'), react: npm('react'), 'react-dom': npm('react-dom') }, devDependencies: { '@vitejs/plugin-react': npm('@vitejs/plugin-react'), 'laravel-vite-plugin': npm('laravel-vite-plugin'), vite: npm('vite') } }),
18
+ 'package.json': json({ private: true, type: 'module', packageManager: `npm@${stack.profile.runtimes.npmMinimum}`, engines: { node: `>=${stack.profile.runtimes.nodeMinimum}`, npm: `>=${stack.profile.runtimes.npmMinimum}` }, scripts: { dev: 'vite', build: 'vite build' }, dependencies: { '@inertiajs/react': npm('@inertiajs/react'), react: npm('react'), 'react-dom': npm('react-dom') }, devDependencies: { '@vitejs/plugin-react': npm('@vitejs/plugin-react'), 'laravel-vite-plugin': npm('laravel-vite-plugin'), vite: npm('vite') } }),
19
+ '.node-version': `${stack.profile.runtimes.node}\n`,
20
+ '.npmrc': 'engine-strict=true\n',
19
21
  'vite.config.js': `import { defineConfig } from 'vite'\nimport laravel from 'laravel-vite-plugin'\nimport react from '@vitejs/plugin-react'\n\nexport default defineConfig({ plugins: [laravel({ input: 'resources/js/app.jsx', refresh: true }), react()] })\n`,
20
22
  'resources/js/app.jsx': `import { createInertiaApp } from '@inertiajs/react'\nimport { createRoot } from 'react-dom/client'\n\nconst pages = import.meta.glob('./Pages/**/*.jsx', { eager: true })\ncreateInertiaApp({ resolve: (name) => pages[\`./Pages/\${name}.jsx\`], setup({ el, App, props }) { createRoot(el).render(<App {...props} />) } })\n`,
21
23
  'resources/js/Pages/Home.jsx': `export default function Home() { return <main><h1>${answers.projectName}</h1><p>Laravel + Inertia + React</p></main> }\n`,
@@ -37,3 +37,4 @@ final class HomePage extends Component
37
37
  }
38
38
  },
39
39
  })
40
+
@@ -12,3 +12,4 @@ export function laravelLoginNavigation(authentication) {
12
12
  ? '<nav>@auth <form method="POST" action="/logout">@csrf<button>Log out</button></form> @else <a href="/login">Log in</a> @endauth</nav>'
13
13
  : ''
14
14
  }
15
+
@@ -0,0 +1,23 @@
1
+ import { defineStackAdapter } from '../../rules.js'
2
+
3
+ export const noBackendAdapter = defineStackAdapter({
4
+ id: 'none',
5
+ kind: 'backend',
6
+ label: 'None / frontend only',
7
+ compatibleWith: { frontend: ['nextjs', 'react', 'react-native'] },
8
+ capabilities: {
9
+ applicationShapes: ['fullstack', 'frontend', 'mobile'],
10
+ architectureProfiles: ['small', 'medium', 'large'],
11
+ authenticationModels: ['public', 'undecided'],
12
+ runtime: 'none',
13
+ },
14
+ contributes: {
15
+ environment: () => [],
16
+ verification: () => [
17
+ { frontend: 'nextjs', architecture: 'small', authentication: 'public' },
18
+ { frontend: 'react', architecture: 'medium', authentication: 'undecided' },
19
+ { frontend: 'react-native', architecture: 'large', authentication: 'public' },
20
+ ],
21
+ },
22
+ })
23
+
@@ -0,0 +1,3 @@
1
+ export function ciContributions() {
2
+ return []
3
+ }
@@ -0,0 +1,21 @@
1
+ export function buildPostgresFiles() {
2
+ return {
3
+ 'prisma.config.ts': `import 'dotenv/config'\nimport { defineConfig, env } from 'prisma/config'\n\nexport default defineConfig({\n schema: 'prisma/schema.prisma',\n migrations: { path: 'prisma/migrations' },\n datasource: { url: env('DATABASE_URL') },\n})\n`,
4
+ 'prisma/schema.prisma': `generator client {\n provider = "prisma-client"\n output = "../src/generated/prisma"\n}\n\ndatasource db {\n provider = "postgresql"\n}\n\nmodel Example {\n id String @id @default(uuid())\n createdAt DateTime @default(now()) @map("created_at")\n updatedAt DateTime @updatedAt @map("updated_at")\n\n @@map("examples")\n}\n`,
5
+ 'src/lib/prisma.ts': `import { PrismaPg } from '@prisma/adapter-pg'\nimport { PrismaClient } from '@/generated/prisma/client'\n\nconst connectionString = process.env.DATABASE_URL\nif (!connectionString) throw new Error('DATABASE_URL is required')\nconst globalForPrisma = globalThis as unknown as { prisma?: PrismaClient }\nexport const prisma = globalForPrisma.prisma ?? new PrismaClient({ adapter: new PrismaPg({ connectionString }) })\nif (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma\n`,
6
+ }
7
+ }
8
+
9
+ export function addPostgresScripts(scripts) {
10
+ Object.assign(scripts, {
11
+ 'prisma:generate': 'prisma generate',
12
+ 'db:migrate': 'prisma migrate dev',
13
+ 'db:deploy': 'prisma migrate deploy',
14
+ 'db:reset': 'prisma migrate reset --force',
15
+ 'db:studio': 'prisma studio',
16
+ dev: 'prisma generate && next dev',
17
+ build: 'prisma generate && next build',
18
+ typecheck: 'prisma generate && tsc --noEmit',
19
+ })
20
+ }
21
+
@@ -0,0 +1,3 @@
1
+ export function dockerContributions() {
2
+ return [{ template: 'postgres', path: 'docker-compose.yml' }]
3
+ }
@@ -0,0 +1,3 @@
1
+ export function environmentContributions() {
2
+ return ['DATABASE_URL']
3
+ }
@@ -0,0 +1,30 @@
1
+ import { defineStackAdapter } from '../../rules.js'
2
+ import { buildPostgresFiles } from './create-files.js'
3
+ import { ciContributions } from './ci.js'
4
+ import { dockerContributions } from './docker.js'
5
+ import { environmentContributions } from './environment.js'
6
+
7
+ export const postgresAdapter = defineStackAdapter({
8
+ id: 'postgres',
9
+ kind: 'backend',
10
+ label: 'PostgreSQL + Prisma',
11
+ compatibleWith: { frontend: ['nextjs'] },
12
+ capabilities: {
13
+ applicationShapes: ['fullstack'],
14
+ architectureProfiles: ['small', 'medium', 'large'],
15
+ authenticationModels: ['public', 'undecided'],
16
+ runtime: 'postgres',
17
+ },
18
+ contributes: {
19
+ files: () => Object.entries(buildPostgresFiles()),
20
+ environment: environmentContributions,
21
+ install: () => [{ cwd: '.', command: 'npm', args: ['run', 'prisma:generate'] }],
22
+ docker: dockerContributions,
23
+ ci: ciContributions,
24
+ verification: () => [
25
+ { frontend: 'nextjs', architecture: 'small', authentication: 'public' },
26
+ { frontend: 'nextjs', architecture: 'medium', authentication: 'undecided' },
27
+ { frontend: 'nextjs', architecture: 'large', authentication: 'public' },
28
+ ],
29
+ },
30
+ })
@@ -0,0 +1,3 @@
1
+ export function ciContributions() {
2
+ return [{ template: 'springboot', path: '.github/workflows/ci-backend.yml' }]
3
+ }
@@ -0,0 +1,91 @@
1
+ export function buildSpringBootFiles(answers, vars, stack) {
2
+ const pkg = vars.PACKAGE_NAME
3
+ const pkgPath = vars.PACKAGE_PATH
4
+ const appName = `${answers.projectName.split('-').map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join('')}Application`
5
+ const isLarge = stack.architecture === 'large'
6
+ const withTests = (answers.testing || 'basic') !== 'none'
7
+ const withFullTests = answers.testing === 'full'
8
+ const healthWebMvcTest = stack.authentication === 'oidc'
9
+ ? '@WebMvcTest(controllers = HealthController.class, properties = "spring.autoconfigure.exclude=org.springframework.boot.security.oauth2.server.resource.autoconfigure.servlet.OAuth2ResourceServerAutoConfiguration")'
10
+ : '@WebMvcTest(HealthController.class)'
11
+ const integrationSecurityImports = stack.authentication === 'oidc'
12
+ ? `\nimport static org.mockito.ArgumentMatchers.anyString;\nimport static org.mockito.Mockito.doThrow;\nimport org.springframework.security.oauth2.jwt.JwtDecoder;\nimport org.springframework.security.oauth2.jwt.BadJwtException;\nimport org.springframework.security.oauth2.jwt.JwtException;\nimport org.springframework.test.context.bean.override.mockito.MockitoBean;`
13
+ : stack.authentication === 'session'
14
+ ? `\nimport static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;\nimport static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;\nimport static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;`
15
+ : ''
16
+ const integrationSecurityFields = stack.authentication === 'oidc'
17
+ ? '\n @MockitoBean JwtDecoder jwtDecoder;'
18
+ : ''
19
+ const integrationSecurityTests = stack.authentication === 'oidc'
20
+ ? `\n @Test void protectedRoutesRejectMissingBearerToken() throws Exception { mvc.perform(get("/api/private-probe")).andExpect(status().isUnauthorized()); }\n @Test void invalidBearerTokenIsRejected() throws Exception { doThrow(new BadJwtException("invalid")).when(jwtDecoder).decode(anyString()); mvc.perform(get("/api/private-probe").header("Authorization", "Bearer invalid")).andExpect(status().isUnauthorized()); }`
21
+ : stack.authentication === 'session'
22
+ ? `\n @Test void protectedRoutesRequireASession() throws Exception { mvc.perform(get("/api/private-probe")).andExpect(status().is3xxRedirection()); }\n @Test void authenticatedRequestsReachTheApplication() throws Exception { mvc.perform(get("/api/private-probe").with(user("test"))).andExpect(status().isNotFound()); }\n @Test void logoutRequiresCsrf() throws Exception { mvc.perform(post("/api/auth/logout").with(user("test"))).andExpect(status().isForbidden()); mvc.perform(post("/api/auth/logout").with(user("test")).with(csrf())).andExpect(status().is3xxRedirection()); }`
23
+ : stack.authentication === 'undecided'
24
+ ? `\n @Test void undecidedAuthenticationFailsClosed() throws Exception { mvc.perform(get("/api/private-probe")).andExpect(status().isForbidden()); }`
25
+ : `\n @Test void publicApplicationsDoNotInventAuthentication() throws Exception { mvc.perform(get("/api/private-probe")).andExpect(status().isNotFound()); }`
26
+ const securityDependency = stack.authentication === 'oidc'
27
+ ? '\n <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-oauth2-resource-server</artifactId></dependency>'
28
+ : ''
29
+ const modulithManagement = isLarge
30
+ ? `\n <dependencyManagement><dependencies><dependency><groupId>org.springframework.modulith</groupId><artifactId>spring-modulith-bom</artifactId><version>${vars.SPRING_MODULITH_VERSION}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement>`
31
+ : ''
32
+ const modulithDependency = isLarge && withTests
33
+ ? '\n <dependency><groupId>org.springframework.modulith</groupId><artifactId>spring-modulith-starter-test</artifactId><scope>test</scope></dependency>'
34
+ : ''
35
+ const testDependencies = withTests
36
+ ? `\n <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webmvc-test</artifactId><scope>test</scope></dependency>\n <dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-test</artifactId><scope>test</scope></dependency>${modulithDependency}${withFullTests ? `\n <dependency><groupId>org.testcontainers</groupId><artifactId>junit-jupiter</artifactId><version>${vars.TESTCONTAINERS_VERSION}</version><scope>test</scope></dependency>\n <dependency><groupId>org.testcontainers</groupId><artifactId>postgresql</artifactId><version>${vars.TESTCONTAINERS_VERSION}</version><scope>test</scope></dependency>` : ''}`
37
+ : ''
38
+ const excludeDefaultUser = stack.authentication === 'session' ? '' : '\nimport org.springframework.boot.security.autoconfigure.UserDetailsServiceAutoConfiguration;'
39
+ const applicationAnnotation = stack.authentication === 'session'
40
+ ? '@SpringBootApplication'
41
+ : '@SpringBootApplication(exclude = UserDetailsServiceAutoConfiguration.class)'
42
+ const accessRule = stack.authentication === 'public'
43
+ ? '.anyRequest().permitAll()'
44
+ : stack.authentication === 'undecided' ? '.anyRequest().denyAll()' : '.anyRequest().authenticated()'
45
+ const authConfig = stack.authentication === 'session'
46
+ ? '\n .formLogin(Customizer.withDefaults())\n .logout(logout -> logout.logoutUrl("/api/auth/logout").invalidateHttpSession(true).deleteCookies("JSESSIONID"))'
47
+ : stack.authentication === 'oidc'
48
+ ? '\n .csrf(csrf -> csrf.disable())\n .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))\n .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))'
49
+ : stack.authentication === 'public' ? '\n .csrf(csrf -> csrf.disable())' : ''
50
+ const securityImports = `${['session', 'oidc'].includes(stack.authentication) ? '\nimport org.springframework.security.config.Customizer;' : ''}${stack.authentication === 'oidc' ? '\nimport org.springframework.security.config.http.SessionCreationPolicy;' : ''}`
51
+ const files = {
52
+ 'backend/mvnw': `#!/bin/sh\nset -eu\nif command -v mvn >/dev/null 2>&1; then exec mvn "$@"; fi\nif command -v docker >/dev/null 2>&1; then exec docker run --rm -v "$PWD:/workspace" -w /workspace ${vars.MAVEN_IMAGE} mvn "$@"; fi\necho "Maven is unavailable. Install Maven ${vars.MAVEN_VERSION} or Docker." >&2\nexit 1\n`,
53
+ 'backend/mvnw.cmd': `@echo off\r\nwhere mvn >nul 2>nul\r\nif %errorlevel% equ 0 (mvn %* & exit /b %errorlevel%)\r\nwhere docker >nul 2>nul\r\nif %errorlevel% equ 0 (docker run --rm -v "%cd%:/workspace" -w /workspace ${vars.MAVEN_IMAGE} mvn %* & exit /b %errorlevel%)\r\necho Maven is unavailable. Install Maven ${vars.MAVEN_VERSION} or Docker. 1>&2\r\nexit /b 1\r\n`,
54
+ 'backend/pom.xml': `<?xml version="1.0" encoding="UTF-8"?>\n<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">\n <modelVersion>4.0.0</modelVersion>\n <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>${vars.SPRING_BOOT_VERSION}</version><relativePath/></parent>\n <groupId>${pkg}</groupId><artifactId>${answers.projectName}</artifactId><version>0.0.1-SNAPSHOT</version>\n <properties><java.version>${vars.JAVA_VERSION}</java.version></properties>${modulithManagement}\n <dependencies>\n <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>\n <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency>\n <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency>\n <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency>\n <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency>\n <dependency><groupId>org.flywaydb</groupId><artifactId>flyway-database-postgresql</artifactId></dependency>\n <dependency><groupId>org.postgresql</groupId><artifactId>postgresql</artifactId><scope>runtime</scope></dependency>${securityDependency}${testDependencies}\n </dependencies>\n <build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>\n</project>\n`,
55
+ [`backend/src/main/java/${pkgPath}/${appName}.java`]: `package ${pkg};\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;${excludeDefaultUser}\n\n${applicationAnnotation}\npublic class ${appName} {\n public static void main(String[] args) { SpringApplication.run(${appName}.class, args); }\n}\n`,
56
+ [`backend/src/main/java/${pkgPath}/health/HealthController.java`]: `package ${pkg}.health;\n\nimport java.util.Map;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n@RestController\npublic class HealthController {\n @GetMapping("/api/health")\n Map<String, String> health() { return Map.of("status", "ok"); }\n}\n`,
57
+ [`backend/src/main/java/${pkgPath}/config/SecurityConfig.java`]: `package ${pkg}.config;\n\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;${securityImports}\nimport org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;\nimport org.springframework.security.config.annotation.web.builders.HttpSecurity;\nimport org.springframework.security.web.SecurityFilterChain;\n\n@Configuration\n@EnableMethodSecurity\npublic class SecurityConfig {\n @Bean\n SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {\n return http\n .authorizeHttpRequests(auth -> auth\n .requestMatchers("/api/health", "/actuator/health").permitAll()\n ${accessRule}\n )${authConfig}\n .build();\n }\n}\n`,
58
+ [`backend/src/main/java/${pkgPath}/common/error/ErrorCode.java`]: `package ${pkg}.common.error;\n\npublic enum ErrorCode { RESOURCE_NOT_FOUND, CONFLICT, OPERATION_FORBIDDEN }\n`,
59
+ [`backend/src/main/java/${pkgPath}/common/error/AppException.java`]: `package ${pkg}.common.error;\n\npublic final class AppException extends RuntimeException {\n private final ErrorCode code;\n public AppException(ErrorCode code, String message) { super(message); this.code = code; }\n public ErrorCode code() { return code; }\n}\n`,
60
+ [`backend/src/main/java/${pkgPath}/common/error/ApiExceptionHandler.java`]: `package ${pkg}.common.error;\n\nimport jakarta.servlet.http.HttpServletRequest;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.ProblemDetail;\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.RestControllerAdvice;\n\n@RestControllerAdvice\npublic class ApiExceptionHandler {\n @ExceptionHandler(AppException.class)\n ProblemDetail handle(AppException exception, HttpServletRequest request) {\n HttpStatus status = switch (exception.code()) {\n case RESOURCE_NOT_FOUND -> HttpStatus.NOT_FOUND;\n case CONFLICT -> HttpStatus.CONFLICT;\n case OPERATION_FORBIDDEN -> HttpStatus.FORBIDDEN;\n };\n ProblemDetail problem = ProblemDetail.forStatusAndDetail(status, exception.getMessage());\n problem.setTitle(status.getReasonPhrase());\n problem.setProperty("code", exception.code().name());\n return problem;\n }\n}\n`,
61
+ 'backend/src/main/resources/application.yml': `spring:\n application:\n name: ${answers.projectName}\n datasource:\n url: \${DATABASE_URL:jdbc:postgresql://localhost:5432/${answers.projectName.replaceAll('-', '_')}}\n username: \${POSTGRES_USER:postgres}\n password: \${POSTGRES_PASSWORD:postgres}\n jpa:\n open-in-view: false\n hibernate:\n ddl-auto: validate\n flyway:\n enabled: true\nmanagement:\n endpoints:\n web:\n exposure:\n include: health,info\n endpoint:\n health:\n probes:\n enabled: true\nserver:\n error:\n include-message: never\n`,
62
+ 'backend/src/main/resources/db/migration/V1__baseline.sql': 'CREATE TABLE examples (\n id UUID PRIMARY KEY,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n);\n',
63
+ }
64
+ if (stack.authentication === 'oidc') {
65
+ files['backend/src/main/resources/application.yml'] += '\nspring.security.oauth2.resourceserver.jwt.issuer-uri: ${OIDC_ISSUER_URI:http://localhost:9090/realms/app}\nspring.security.oauth2.resourceserver.jwt.audiences: ${OIDC_AUDIENCE:api}\n'
66
+ }
67
+
68
+ if (stack.architecture !== 'small') {
69
+ if (isLarge) {
70
+ files[`backend/src/main/java/${pkgPath}/system/SystemStatus.java`] = `package ${pkg}.system;\n\npublic interface SystemStatus { Status current(); record Status(String status, String architecture) {} }\n`
71
+ files[`backend/src/main/java/${pkgPath}/system/internal/SystemStatusService.java`] = `package ${pkg}.system.internal;\n\nimport org.springframework.stereotype.Service;\nimport ${pkg}.system.SystemStatus;\n\n@Service\nclass SystemStatusService implements SystemStatus {\n public Status current() { return new Status("ok", "large"); }\n}\n`
72
+ files[`backend/src/main/java/${pkgPath}/system/internal/SystemStatusController.java`] = `package ${pkg}.system.internal;\n\nimport ${pkg}.system.SystemStatus;\nimport ${pkg}.system.SystemStatus.Status;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n@RestController\n@RequestMapping("/api/status")\nclass SystemStatusController {\n private final SystemStatus status;\n SystemStatusController(SystemStatus status) { this.status = status; }\n @GetMapping Status current() { return status.current(); }\n}\n`
73
+ } else {
74
+ files[`backend/src/main/java/${pkgPath}/system/api/SystemStatusResponse.java`] = `package ${pkg}.system.api;\n\npublic record SystemStatusResponse(String status, String architecture) {}\n`
75
+ files[`backend/src/main/java/${pkgPath}/system/service/SystemStatusService.java`] = `package ${pkg}.system.service;\n\nimport ${pkg}.system.api.SystemStatusResponse;\nimport org.springframework.stereotype.Service;\n\n@Service\npublic class SystemStatusService {\n public SystemStatusResponse current() { return new SystemStatusResponse("ok", "medium"); }\n}\n`
76
+ files[`backend/src/main/java/${pkgPath}/system/api/SystemStatusController.java`] = `package ${pkg}.system.api;\n\nimport ${pkg}.system.service.SystemStatusService;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n@RestController\n@RequestMapping("/api/status")\npublic class SystemStatusController {\n private final SystemStatusService service;\n public SystemStatusController(SystemStatusService service) { this.service = service; }\n @GetMapping SystemStatusResponse current() { return service.current(); }\n}\n`
77
+ }
78
+ }
79
+
80
+ if (withTests) {
81
+ files[`backend/src/test/java/${pkgPath}/health/HealthControllerTest.java`] = `package ${pkg}.health;\n\nimport static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;\nimport static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;\nimport static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;\nimport org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;\nimport org.springframework.test.web.servlet.MockMvc;\n\n${healthWebMvcTest}\n@AutoConfigureMockMvc(addFilters = false)\nclass HealthControllerTest {\n @Autowired MockMvc mvc;\n @Test void healthContractIsStable() throws Exception { mvc.perform(get("/api/health")).andExpect(status().isOk()).andExpect(jsonPath("$.status").value("ok")); }\n}\n`
82
+ if (isLarge) {
83
+ files[`backend/src/test/java/${pkgPath}/ArchitectureTest.java`] = `package ${pkg};\n\nimport org.junit.jupiter.api.Test;\nimport org.springframework.modulith.core.ApplicationModules;\n\nclass ArchitectureTest {\n @Test void modulesRespectTheirPublicApis() { ApplicationModules.of(${appName}.class).verify(); }\n}\n`
84
+ }
85
+ if (withFullTests) {
86
+ files[`backend/src/test/java/${pkgPath}/PostgresIntegrationTest.java`] = `package ${pkg};\n\nimport static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;\nimport static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;${integrationSecurityImports}\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;\nimport org.springframework.test.context.DynamicPropertyRegistry;\nimport org.springframework.test.context.DynamicPropertySource;\nimport org.springframework.test.web.servlet.MockMvc;\nimport org.testcontainers.containers.PostgreSQLContainer;\nimport org.testcontainers.junit.jupiter.Container;\nimport org.testcontainers.junit.jupiter.Testcontainers;\n\n@SpringBootTest\n@AutoConfigureMockMvc\n@Testcontainers\nclass PostgresIntegrationTest {\n @Container static final PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("${vars.POSTGRES_IMAGE}");\n @Autowired MockMvc mvc;${integrationSecurityFields}\n @DynamicPropertySource static void database(DynamicPropertyRegistry registry) {\n registry.add("spring.datasource.url", postgres::getJdbcUrl);\n registry.add("spring.datasource.username", postgres::getUsername);\n registry.add("spring.datasource.password", postgres::getPassword);\n }\n @Test void migrationsAndPostgresContextStart() {}${integrationSecurityTests}\n}\n`
87
+ }
88
+ }
89
+ return files
90
+ }
91
+
@@ -0,0 +1,3 @@
1
+ export function dockerContributions() {
2
+ return [{ template: 'springboot', developmentPath: 'backend/Dockerfile.dev', productionPath: 'backend/Dockerfile' }]
3
+ }
@@ -0,0 +1,3 @@
1
+ export function environmentContributions() {
2
+ return ['API_URL', 'DATABASE_URL', 'POSTGRES_USER', 'POSTGRES_PASSWORD', 'POSTGRES_DB', 'SPRING_PROFILES_ACTIVE']
3
+ }
@@ -0,0 +1,31 @@
1
+ import { defineStackAdapter } from '../../rules.js'
2
+ import { buildSpringBootFiles } from './create-files.js'
3
+ import { ciContributions } from './ci.js'
4
+ import { dockerContributions } from './docker.js'
5
+ import { environmentContributions } from './environment.js'
6
+
7
+ export const springbootAdapter = defineStackAdapter({
8
+ id: 'springboot',
9
+ kind: 'backend',
10
+ label: 'Spring Boot',
11
+ compatibleWith: { frontend: ['nextjs', 'react', 'react-native', 'no-frontend'] },
12
+ capabilities: {
13
+ applicationShapes: ['separate', 'api', 'mobile'],
14
+ architectureProfiles: ['small', 'medium', 'large'],
15
+ authenticationModels: ['public', 'undecided', 'session', 'oidc'],
16
+ runtime: 'java',
17
+ },
18
+ contributes: {
19
+ files: ({ answers, stack, vars }) => Object.entries(buildSpringBootFiles(answers, vars, stack)),
20
+ environment: environmentContributions,
21
+ install: () => [{ cwd: 'backend', command: './mvnw', args: ['dependency:go-offline'] }],
22
+ docker: dockerContributions,
23
+ ci: ciContributions,
24
+ verification: () => [
25
+ { frontend: 'nextjs', architecture: 'small', authentication: 'public' },
26
+ { frontend: 'react', architecture: 'medium', authentication: 'session' },
27
+ { frontend: 'react-native', architecture: 'large', authentication: 'oidc' },
28
+ { frontend: 'no-frontend', architecture: 'medium', authentication: 'undecided' },
29
+ ],
30
+ },
31
+ })
@@ -0,0 +1,3 @@
1
+ export function ciContributions() {
2
+ return []
3
+ }
@@ -0,0 +1,52 @@
1
+ import path from 'node:path'
2
+
3
+ export function buildSupabaseProjectFiles(stack) {
4
+ if (stack.backendKey !== 'supabase') return {}
5
+ const authenticated = stack.authentication === 'supabase'
6
+ const policies = authenticated
7
+ ? `grant select, insert, update, delete on table public.examples to authenticated;\n\ncreate policy "owners read examples" on public.examples for select to authenticated using ((select auth.uid()) = user_id);\ncreate policy "owners create examples" on public.examples for insert to authenticated with check ((select auth.uid()) = user_id);\ncreate policy "owners update examples" on public.examples for update to authenticated using ((select auth.uid()) = user_id) with check ((select auth.uid()) = user_id);\ncreate policy "owners delete examples" on public.examples for delete to authenticated using ((select auth.uid()) = user_id);`
8
+ : stack.authentication === 'public'
9
+ ? `grant select on table public.examples to anon, authenticated;\ncreate policy "public reads examples" on public.examples for select to anon, authenticated using (true);`
10
+ : '-- No grants or policies: authentication is intentionally undecided and access is fail-closed.'
11
+ return {
12
+ 'supabase/config.toml': `project_id = "${stack.key}"\n\n[api]\nenabled = true\nport = 54321\nschemas = ["public", "graphql_public"]\nextra_search_path = ["public", "extensions"]\n\n[db]\nport = 54322\nmajor_version = 16\n\n[studio]\nenabled = true\nport = 54323\n`,
13
+ 'supabase/migrations/00000000000000_create_examples.sql': `create table public.examples (\n id uuid primary key default gen_random_uuid(),\n user_id uuid references auth.users(id) on delete cascade,\n name text not null check (char_length(name) between 1 and 120),\n created_at timestamptz not null default now(),\n updated_at timestamptz not null default now()\n);\n\nalter table public.examples enable row level security;\nrevoke all on table public.examples from anon, authenticated;\ncreate index examples_user_id_idx on public.examples (user_id);\n\n${policies}\n`,
14
+ 'supabase/tests/examples_rls.test.sql': authenticated
15
+ ? `begin;\nselect plan(5);\nselect ok((select relrowsecurity from pg_class where oid = 'public.examples'::regclass), 'examples has RLS enabled');\nselect ok((select count(*) >= 1 from pg_indexes where schemaname = 'public' and tablename = 'examples' and indexdef like '%user_id%'), 'RLS ownership column is indexed');\n\ninsert into auth.users (id, instance_id, aud, role, email, encrypted_password, created_at, updated_at) values\n ('11111111-1111-1111-1111-111111111111', '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated', 'owner@example.test', '', now(), now()),\n ('22222222-2222-2222-2222-222222222222', '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated', 'other@example.test', '', now(), now());\ninsert into public.examples (user_id, name) values ('11111111-1111-1111-1111-111111111111', 'owned row');\n\nset local role authenticated;\nselect set_config('request.jwt.claim.sub', '11111111-1111-1111-1111-111111111111', true);\nselect results_eq('select name from public.examples order by name', $$values ('owned row'::text)$$, 'owner can read their row');\nselect set_config('request.jwt.claim.sub', '22222222-2222-2222-2222-222222222222', true);\nselect is((select count(*) from public.examples), 0::bigint, 'non-owner cannot read the row');\nreset role;\nset local role anon;\nselect is((select count(*) from public.examples), 0::bigint, 'anonymous caller cannot read the row');\nreset role;\n\nselect * from finish();\nrollback;\n`
16
+ : `begin;\nselect plan(2);\nselect ok((select relrowsecurity from pg_class where oid = 'public.examples'::regclass), 'examples has RLS enabled');\nselect ok((select count(*) >= 1 from pg_indexes where schemaname = 'public' and tablename = 'examples' and indexdef like '%user_id%'), 'RLS policy column is indexed');\nselect * from finish();\nrollback;\n`,
17
+ }
18
+ }
19
+
20
+ export function buildSharedTestFiles(files, root, stack, level) {
21
+ if (level === 'none') return
22
+ if (stack.isMobile) {
23
+ files[path.join(root, 'jest.config.js')] = "export default { preset: 'jest-expo' }\n"
24
+ files[path.join(root, 'app/index.test.tsx')] = `import { render } from '@testing-library/react-native'\nimport HomeScreen from './index'\n\ntest('renders the starter heading', async () => {\n const view = await render(<HomeScreen />)\n expect(view.getByText('Your starter is running')).toBeTruthy()\n})\n`
25
+ return
26
+ }
27
+
28
+ files[path.join(root, 'vitest.config.ts')] = `import { fileURLToPath, URL } from 'node:url'\nimport { defineConfig } from 'vitest/config'\n\nexport default defineConfig({\n resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } },\n test: { environment: 'jsdom', setupFiles: './src/test/setup.ts', exclude: ['e2e/**', 'node_modules/**'] },\n})\n`
29
+ files[path.join(root, 'src/test/setup.ts')] = "import '@testing-library/jest-dom/vitest'\n"
30
+ if (level === 'full') {
31
+ files[path.join(root, 'playwright.config.ts')] = `import { defineConfig, devices } from '@playwright/test'\n\nexport default defineConfig({\n testDir: './e2e',\n use: { baseURL: 'http://127.0.0.1:${stack.frontendPort}' },\n webServer: { command: 'npm run dev -- ${stack.frontendKey === 'nextjs' ? '--hostname' : '--host'} 127.0.0.1 --port ${stack.frontendPort}', url: 'http://127.0.0.1:${stack.frontendPort}', reuseExistingServer: true, timeout: 120_000 },\n projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],\n})\n`
32
+ files[path.join(root, 'e2e/home.spec.ts')] = `import { expect, test } from '@playwright/test'\n\ntest('loads the starter', async ({ page }) => {\n await page.goto('/')\n await expect(page.getByRole('heading', { name: 'Your starter is running' })).toBeVisible()\n})\n`
33
+ }
34
+ }
35
+
36
+ export function buildSupabaseWebFiles(isNext, withAuth = false) {
37
+ if (!isNext) return {
38
+ 'src/lib/supabase.ts': `import { createClient } from '@supabase/supabase-js'\n\nconst url = import.meta.env.VITE_SUPABASE_URL\nconst key = import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY\nif (!url || !key) throw new Error('Missing VITE_SUPABASE_URL or VITE_SUPABASE_PUBLISHABLE_KEY')\nexport const supabase = createClient(url, key)\n`,
39
+ }
40
+ const files = {
41
+ 'src/lib/supabase/client.ts': `import { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n )\n}\n`,
42
+ 'src/lib/supabase/server.ts': `import { createServerClient } from '@supabase/ssr'\nimport { cookies } from 'next/headers'\n\nexport async function createClient() {\n const store = await cookies()\n return createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n { cookies: { getAll: () => store.getAll(), setAll: (values) => {\n try { values.forEach(({ name, value, options }) => store.set(name, value, options)) } catch { /* Proxy owns refresh writes. */ }\n } } },\n )\n}\n`,
43
+ }
44
+ if (withAuth) Object.assign(files, {
45
+ 'src/lib/supabase/proxy.ts': `import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let response = NextResponse.next({ request })\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n { cookies: {\n getAll: () => request.cookies.getAll(),\n setAll: (values) => {\n values.forEach(({ name, value }) => request.cookies.set(name, value))\n response = NextResponse.next({ request })\n values.forEach(({ name, value, options }) => response.cookies.set(name, value, options))\n },\n } },\n )\n await supabase.auth.getClaims()\n return response\n}\n`,
46
+ 'src/proxy.ts': `import type { NextRequest } from 'next/server'\nimport { updateSession } from '@/lib/supabase/proxy'\n\nexport async function proxy(request: NextRequest) { return updateSession(request) }\n\nexport const config = { matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)'] }\n`,
47
+ 'src/app/auth/callback/route.ts': `import { NextResponse, type NextRequest } from 'next/server'\nimport { createClient } from '@/lib/supabase/server'\n\nexport async function GET(request: NextRequest) {\n const code = request.nextUrl.searchParams.get('code')\n const destination = new URL('/', request.url)\n if (!code) return NextResponse.redirect(destination)\n const supabase = await createClient()\n const { error } = await supabase.auth.exchangeCodeForSession(code)\n if (error) destination.searchParams.set('authError', 'callback_failed')\n return NextResponse.redirect(destination)\n}\n`,
48
+ 'src/app/login/actions.ts': `'use server'\n\nimport { redirect } from 'next/navigation'\nimport { createClient } from '@/lib/supabase/server'\n\nexport async function signIn(formData: FormData) {\n const email = String(formData.get('email') || '').trim()\n const password = String(formData.get('password') || '')\n if (!email || !password) redirect('/login?error=missing_credentials')\n const supabase = await createClient()\n const { error } = await supabase.auth.signInWithPassword({ email, password })\n if (error) redirect('/login?error=invalid_credentials')\n redirect('/')\n}\n\nexport async function signOut() {\n const supabase = await createClient()\n await supabase.auth.signOut()\n redirect('/login')\n}\n`,
49
+ 'src/app/login/page.tsx': `import { signIn } from './actions'\n\nexport default async function LoginPage({ searchParams }: { searchParams: Promise<{ error?: string }> }) {\n const { error } = await searchParams\n return <main><h1>Sign in</h1>{error ? <p role="alert">Sign-in failed. Check your details and try again.</p> : null}<form action={signIn}><label>Email <input name="email" type="email" autoComplete="email" required /></label><label>Password <input name="password" type="password" autoComplete="current-password" required /></label><button type="submit">Sign in</button></form></main>\n}\n`,
50
+ })
51
+ return files
52
+ }
@@ -0,0 +1,3 @@
1
+ export function dockerContributions() {
2
+ return [{ template: 'supabase', path: 'docker-compose.yml' }]
3
+ }
@@ -0,0 +1,3 @@
1
+ export function environmentContributions() {
2
+ return ['SUPABASE_URL', 'SUPABASE_PUBLISHABLE_KEY']
3
+ }
@@ -0,0 +1,31 @@
1
+ import { defineStackAdapter } from '../../rules.js'
2
+ import { buildSupabaseProjectFiles } from './create-files.js'
3
+ import { ciContributions } from './ci.js'
4
+ import { dockerContributions } from './docker.js'
5
+ import { environmentContributions } from './environment.js'
6
+
7
+ export const supabaseAdapter = defineStackAdapter({
8
+ id: 'supabase',
9
+ kind: 'backend',
10
+ label: 'Supabase',
11
+ compatibleWith: { frontend: ['nextjs', 'react', 'react-native'] },
12
+ capabilities: {
13
+ applicationShapes: ['fullstack', 'separate', 'mobile'],
14
+ architectureProfiles: ['small', 'medium', 'large'],
15
+ authenticationModels: ['public', 'undecided', 'supabase'],
16
+ runtime: 'docker',
17
+ },
18
+ contributes: {
19
+ files: ({ stack }) => Object.entries(buildSupabaseProjectFiles(stack)),
20
+ environment: environmentContributions,
21
+ install: () => [{ cwd: '.', command: 'npm', args: ['run', 'supabase:start'] }],
22
+ docker: dockerContributions,
23
+ ci: ciContributions,
24
+ verification: () => [
25
+ { frontend: 'nextjs', architecture: 'small', authentication: 'public' },
26
+ { frontend: 'nextjs', architecture: 'large', authentication: 'supabase' },
27
+ { frontend: 'react', architecture: 'medium', authentication: 'supabase' },
28
+ { frontend: 'react-native', architecture: 'medium', authentication: 'supabase' },
29
+ ],
30
+ },
31
+ })
@@ -0,0 +1,14 @@
1
+ export function augmentSupabaseNativeFiles(files, answers, stack) {
2
+ if (stack.backendKey === 'supabase') {
3
+ files['lib/supabase.ts'] = stack.authentication === 'supabase'
4
+ ? `import { createClient } from '@supabase/supabase-js'\nimport * as SecureStore from 'expo-secure-store'\n\nconst storage = { getItem: SecureStore.getItemAsync, setItem: SecureStore.setItemAsync, removeItem: SecureStore.deleteItemAsync }\nexport const supabase = createClient(process.env.EXPO_PUBLIC_SUPABASE_URL!, process.env.EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY!, { auth: { storage, persistSession: true, autoRefreshToken: true, detectSessionInUrl: false } })\n`
5
+ : `import { createClient } from '@supabase/supabase-js'\n\nexport const supabase = createClient(process.env.EXPO_PUBLIC_SUPABASE_URL!, process.env.EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY!, { auth: { persistSession: false, autoRefreshToken: false, detectSessionInUrl: false } })\n`
6
+ if (stack.authentication === 'supabase') {
7
+ files['lib/supabase-lifecycle.ts'] = `import { AppState, type AppStateStatus } from 'react-native'\nimport { supabase } from './supabase'\n\nexport function bindSupabaseAuthLifecycle() {\n const update = (state: AppStateStatus) => {\n if (state === 'active') supabase.auth.startAutoRefresh()\n else supabase.auth.stopAutoRefresh()\n }\n update(AppState.currentState)\n const subscription = AppState.addEventListener('change', update)\n return () => { subscription.remove(); supabase.auth.stopAutoRefresh() }\n}\n`
8
+ files['app/login.tsx'] = `import { useState } from 'react'\nimport { Pressable, StyleSheet, Text, TextInput, View } from 'react-native'\nimport { supabase } from '@/lib/supabase'\n\nexport default function LoginScreen() {\n const [email, setEmail] = useState('')\n const [password, setPassword] = useState('')\n const [error, setError] = useState('')\n const [pending, setPending] = useState(false)\n async function signIn() {\n setPending(true); setError('')\n const result = await supabase.auth.signInWithPassword({ email: email.trim(), password })\n if (result.error) setError('Unable to sign in with those credentials.')\n setPending(false)\n }\n return <View style={styles.container}><Text accessibilityRole="header">Sign in</Text>{error ? <Text accessibilityRole="alert">{error}</Text> : null}<TextInput accessibilityLabel="Email" autoCapitalize="none" autoComplete="email" keyboardType="email-address" onChangeText={setEmail} value={email} /><TextInput accessibilityLabel="Password" autoComplete="current-password" onChangeText={setPassword} secureTextEntry value={password} /><Pressable accessibilityRole="button" disabled={pending} onPress={signIn}><Text>{pending ? 'Signing in…' : 'Sign in'}</Text></Pressable></View>\n}\nconst styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', padding: 24, gap: 12 } })\n`
9
+ if ((answers.testing || 'basic') !== 'none') files['lib/supabase-lifecycle.test.ts'] = `import { AppState } from 'react-native'\nimport { bindSupabaseAuthLifecycle } from './supabase-lifecycle'\nimport { supabase } from './supabase'\n\njest.mock('./supabase', () => ({ supabase: { auth: { startAutoRefresh: jest.fn(), stopAutoRefresh: jest.fn() } } }))\n\ntest('starts refresh while active and stops it on cleanup', () => {\n const remove = jest.fn()\n jest.spyOn(AppState, 'addEventListener').mockReturnValue({ remove } as never)\n Object.defineProperty(AppState, 'currentState', { configurable: true, value: 'active' })\n const cleanup = bindSupabaseAuthLifecycle()\n expect(supabase.auth.startAutoRefresh).toHaveBeenCalled()\n cleanup()\n expect(remove).toHaveBeenCalled()\n expect(supabase.auth.stopAutoRefresh).toHaveBeenCalled()\n})\n`
10
+ }
11
+ }
12
+ return files
13
+ }
14
+