create-win-project 1.3.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 (198) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +187 -0
  3. package/ci/expo.yml +28 -0
  4. package/ci/laravel.yml +49 -0
  5. package/ci/nextjs.yml +29 -0
  6. package/ci/springboot.yml +33 -0
  7. package/ci/vite.yml +36 -0
  8. package/compatibility/profiles.json +237 -0
  9. package/docs/ARCHITECTURE.md +118 -0
  10. package/docs/CONTENT_MODEL.md +36 -0
  11. package/docs/DEPENDENCY_MAINTENANCE.md +31 -0
  12. package/index.js +431 -0
  13. package/lib/application-shapes.js +95 -0
  14. package/lib/banner.js +45 -0
  15. package/lib/catalog.js +343 -0
  16. package/lib/compatibility.js +116 -0
  17. package/lib/constants.js +3 -0
  18. package/lib/doctor.js +45 -0
  19. package/lib/files.js +158 -0
  20. package/lib/generator.js +366 -0
  21. package/lib/interview.js +77 -0
  22. package/lib/laravel-scaffold.js +3 -0
  23. package/lib/playbooks.js +172 -0
  24. package/lib/project-location.js +18 -0
  25. package/lib/scaffold.js +467 -0
  26. package/lib/stacks/context.js +27 -0
  27. package/lib/stacks/contract.js +142 -0
  28. package/lib/stacks/index.js +8 -0
  29. package/lib/stacks/laravel/architecture.js +27 -0
  30. package/lib/stacks/laravel/auth/oidc.js +13 -0
  31. package/lib/stacks/laravel/auth/public.js +28 -0
  32. package/lib/stacks/laravel/auth/sanctum.js +14 -0
  33. package/lib/stacks/laravel/auth/session.js +17 -0
  34. package/lib/stacks/laravel/composer.js +50 -0
  35. package/lib/stacks/laravel/generate.js +362 -0
  36. package/lib/stacks/laravel/index.js +18 -0
  37. package/lib/stacks/laravel/ui/blade.js +15 -0
  38. package/lib/stacks/laravel/ui/index.js +29 -0
  39. package/lib/stacks/laravel/ui/inertia-react.js +35 -0
  40. package/lib/stacks/laravel/ui/livewire.js +39 -0
  41. package/lib/stacks/laravel/ui/shared.js +14 -0
  42. package/lib/stacks/registry.js +42 -0
  43. package/lib/stacks/shared/contributions.js +25 -0
  44. package/lib/template.js +62 -0
  45. package/package.json +49 -0
  46. package/playbooks/INDEX.md +58 -0
  47. package/playbooks/capabilities/auth/oidc-resource-server.md +15 -0
  48. package/playbooks/capabilities/auth/spring-session.md +15 -0
  49. package/playbooks/capabilities/ci/github-actions.manifest.json +10 -0
  50. package/playbooks/capabilities/ci/github-actions.md +61 -0
  51. package/playbooks/capabilities/docker/docker.manifest.json +11 -0
  52. package/playbooks/capabilities/docker/overview.md +55 -0
  53. package/playbooks/capabilities/flyway/environments.md +5 -0
  54. package/playbooks/capabilities/flyway/flyway.manifest.json +12 -0
  55. package/playbooks/capabilities/flyway/migrations.md +7 -0
  56. package/playbooks/capabilities/flyway/testing.md +5 -0
  57. package/playbooks/capabilities/laravel/database.md +8 -0
  58. package/playbooks/capabilities/laravel/migrations.md +15 -0
  59. package/playbooks/capabilities/laravel/observability.md +8 -0
  60. package/playbooks/capabilities/laravel/oidc-resource-server.md +10 -0
  61. package/playbooks/capabilities/laravel/queues.md +8 -0
  62. package/playbooks/capabilities/laravel/sanctum-spa.md +8 -0
  63. package/playbooks/capabilities/laravel/scheduler.md +7 -0
  64. package/playbooks/capabilities/laravel/session-auth.md +8 -0
  65. package/playbooks/capabilities/laravel/storage-uploads.md +7 -0
  66. package/playbooks/capabilities/postgresql/architecture.md +5 -0
  67. package/playbooks/capabilities/postgresql/migrations.md +5 -0
  68. package/playbooks/capabilities/postgresql/postgresql.manifest.json +15 -0
  69. package/playbooks/capabilities/postgresql/schema-design.md +7 -0
  70. package/playbooks/capabilities/postgresql/security.md +5 -0
  71. package/playbooks/capabilities/postgresql/testing.md +5 -0
  72. package/playbooks/capabilities/prisma/architecture.md +5 -0
  73. package/playbooks/capabilities/prisma/migrations.md +11 -0
  74. package/playbooks/capabilities/prisma/prisma.manifest.json +14 -0
  75. package/playbooks/capabilities/prisma/runtime.md +5 -0
  76. package/playbooks/capabilities/prisma/schema.md +5 -0
  77. package/playbooks/capabilities/prisma/testing.md +5 -0
  78. package/playbooks/capabilities/supabase/architecture.md +5 -0
  79. package/playbooks/capabilities/supabase/authentication.md +5 -0
  80. package/playbooks/capabilities/supabase/expo.md +16 -0
  81. package/playbooks/capabilities/supabase/migrations.md +7 -0
  82. package/playbooks/capabilities/supabase/nextjs.md +24 -0
  83. package/playbooks/capabilities/supabase/rls.md +7 -0
  84. package/playbooks/capabilities/supabase/supabase.manifest.json +32 -0
  85. package/playbooks/capabilities/supabase/testing.md +5 -0
  86. package/playbooks/capabilities/supabase/vite.md +14 -0
  87. package/playbooks/concerns/axios.md +123 -0
  88. package/playbooks/concerns/next-safe-action.md +66 -0
  89. package/playbooks/concerns/next-themes.md +87 -0
  90. package/playbooks/concerns/nuqs.md +59 -0
  91. package/playbooks/concerns/t3-env.md +48 -0
  92. package/playbooks/concerns/tanstack-query.md +145 -0
  93. package/playbooks/concerns/zod.md +174 -0
  94. package/playbooks/concerns/zustand.md +93 -0
  95. package/playbooks/devops/makefile.manifest.json +10 -0
  96. package/playbooks/devops/makefile.md +556 -0
  97. package/playbooks/devops/pr-template.manifest.json +10 -0
  98. package/playbooks/devops/pr-template.md +106 -0
  99. package/playbooks/platform/laravel-ui/blade/architecture.md +5 -0
  100. package/playbooks/platform/laravel-ui/blade/runtime.md +5 -0
  101. package/playbooks/platform/laravel-ui/blade/security.md +5 -0
  102. package/playbooks/platform/laravel-ui/blade/structure.md +5 -0
  103. package/playbooks/platform/laravel-ui/blade/testing.md +5 -0
  104. package/playbooks/platform/laravel-ui/inertia-react/architecture.md +5 -0
  105. package/playbooks/platform/laravel-ui/inertia-react/runtime.md +5 -0
  106. package/playbooks/platform/laravel-ui/inertia-react/security.md +5 -0
  107. package/playbooks/platform/laravel-ui/inertia-react/structure.md +5 -0
  108. package/playbooks/platform/laravel-ui/inertia-react/testing.md +5 -0
  109. package/playbooks/platform/laravel-ui/livewire/architecture.md +5 -0
  110. package/playbooks/platform/laravel-ui/livewire/runtime.md +5 -0
  111. package/playbooks/platform/laravel-ui/livewire/security.md +5 -0
  112. package/playbooks/platform/laravel-ui/livewire/structure.md +5 -0
  113. package/playbooks/platform/laravel-ui/livewire/testing.md +5 -0
  114. package/playbooks/platform/mobile.manifest.json +17 -0
  115. package/playbooks/platform/mobile.md +15 -0
  116. package/playbooks/platform/web.manifest.json +17 -0
  117. package/playbooks/platform/web.md +17 -0
  118. package/playbooks/stack/expo/architecture.md +27 -0
  119. package/playbooks/stack/expo/runtime.md +14 -0
  120. package/playbooks/stack/expo/security.md +18 -0
  121. package/playbooks/stack/expo/structure.md +26 -0
  122. package/playbooks/stack/expo/testing.md +13 -0
  123. package/playbooks/stack/laravel/architecture.md +34 -0
  124. package/playbooks/stack/laravel/runtime.md +27 -0
  125. package/playbooks/stack/laravel/security.md +23 -0
  126. package/playbooks/stack/laravel/structure.md +29 -0
  127. package/playbooks/stack/laravel/testing.md +22 -0
  128. package/playbooks/stack/laravel-ui.manifest.json +42 -0
  129. package/playbooks/stack/laravel.manifest.json +35 -0
  130. package/playbooks/stack/nextjs/architecture.md +62 -0
  131. package/playbooks/stack/nextjs/runtime.md +31 -0
  132. package/playbooks/stack/nextjs/security.md +32 -0
  133. package/playbooks/stack/nextjs/structure.md +50 -0
  134. package/playbooks/stack/nextjs/testing.md +20 -0
  135. package/playbooks/stack/nextjs.manifest.json +49 -0
  136. package/playbooks/stack/no-frontend.manifest.json +25 -0
  137. package/playbooks/stack/none.manifest.json +21 -0
  138. package/playbooks/stack/react-native.manifest.json +49 -0
  139. package/playbooks/stack/react-vite/architecture.md +33 -0
  140. package/playbooks/stack/react-vite/runtime.md +13 -0
  141. package/playbooks/stack/react-vite/security.md +16 -0
  142. package/playbooks/stack/react-vite/structure.md +23 -0
  143. package/playbooks/stack/react-vite/testing.md +12 -0
  144. package/playbooks/stack/react-vite.manifest.json +46 -0
  145. package/playbooks/stack/springboot/architecture.md +40 -0
  146. package/playbooks/stack/springboot/runtime.md +28 -0
  147. package/playbooks/stack/springboot/security.md +28 -0
  148. package/playbooks/stack/springboot/structure.md +30 -0
  149. package/playbooks/stack/springboot/testing.md +22 -0
  150. package/playbooks/stack/springboot.manifest.json +38 -0
  151. package/playbooks/styling/css-modules-extensions.md +267 -0
  152. package/playbooks/styling/css-modules.manifest.json +11 -0
  153. package/playbooks/styling/native-styles.manifest.json +9 -0
  154. package/playbooks/styling/native-styles.md +29 -0
  155. package/playbooks/styling/tailwind-extensions.md +182 -0
  156. package/playbooks/styling/tailwind.manifest.json +11 -0
  157. package/playbooks/universal/accessibility.manifest.json +11 -0
  158. package/playbooks/universal/accessibility.md +45 -0
  159. package/playbooks/universal/coding-rules.manifest.json +12 -0
  160. package/playbooks/universal/coding-rules.md +281 -0
  161. package/playbooks/universal/error-handling.manifest.json +10 -0
  162. package/playbooks/universal/error-handling.md +21 -0
  163. package/playbooks/universal/git-conventions.manifest.json +10 -0
  164. package/playbooks/universal/git-conventions.md +186 -0
  165. package/playbooks/universal/observability.manifest.json +10 -0
  166. package/playbooks/universal/observability.md +29 -0
  167. package/playbooks/universal/security.manifest.json +11 -0
  168. package/playbooks/universal/security.md +30 -0
  169. package/playbooks/universal/typescript.manifest.json +11 -0
  170. package/playbooks/universal/typescript.md +272 -0
  171. package/scripts/compatibility-matrix.mjs +48 -0
  172. package/scripts/validate-content.mjs +76 -0
  173. package/scripts/verify-generated.mjs +153 -0
  174. package/templates/agents/nextjs.md +47 -0
  175. package/templates/agents/react-native.md +48 -0
  176. package/templates/agents/react-vite.md +48 -0
  177. package/templates/docker/compose/postgres.yml +29 -0
  178. package/templates/docker/compose/springboot.yml +69 -0
  179. package/templates/docker/compose/supabase.yml +18 -0
  180. package/templates/docker/compose-prod/springboot.yml +64 -0
  181. package/templates/docker/dockerfile/laravel-inertia.dev.dockerfile +21 -0
  182. package/templates/docker/dockerfile/laravel.dev.dockerfile +12 -0
  183. package/templates/docker/dockerfile/laravel.prod.dockerfile +13 -0
  184. package/templates/docker/dockerfile/nextjs.dev.dockerfile +6 -0
  185. package/templates/docker/dockerfile/nextjs.prod.dockerfile +18 -0
  186. package/templates/docker/dockerfile/springboot.dev.dockerfile +5 -0
  187. package/templates/docker/dockerfile/springboot.prod.dockerfile +10 -0
  188. package/templates/docker/dockerfile/vite.dev.dockerfile +6 -0
  189. package/templates/docker/dockerfile/vite.prod.dockerfile +12 -0
  190. package/templates/gitignore/base.gitignore +32 -0
  191. package/templates/gitignore/nextjs.gitignore +40 -0
  192. package/templates/gitignore/react-native.gitignore +43 -0
  193. package/templates/gitignore/react-vite.gitignore +39 -0
  194. package/templates/makefile/frontend.mk +31 -0
  195. package/templates/makefile/laravel.mk +47 -0
  196. package/templates/makefile/postgres.mk +40 -0
  197. package/templates/makefile/springboot.mk +125 -0
  198. package/templates/makefile/supabase.mk +42 -0
@@ -0,0 +1,142 @@
1
+ const IDENTIFIER = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
2
+ const DEFINITION_FIELDS = new Set(['id', 'kind', 'label', 'compatibleWith', 'capabilities', 'contributes'])
3
+
4
+ export const STACK_KINDS = Object.freeze(['frontend', 'backend', 'data'])
5
+ export const ARCHITECTURE_PROFILES = Object.freeze(['small', 'medium', 'large'])
6
+ export const APPLICATION_SHAPES = Object.freeze(['fullstack', 'separate', 'api', 'mobile', 'frontend'])
7
+ export const PROMPT_SLOTS = Object.freeze([
8
+ 'frontend-options',
9
+ 'backend-options',
10
+ 'stack-options',
11
+ 'authentication-options',
12
+ 'tooling-options',
13
+ ])
14
+
15
+ export const CONTRIBUTION_HOOKS = Object.freeze([
16
+ 'prompts',
17
+ 'authentication',
18
+ 'files',
19
+ 'environment',
20
+ 'install',
21
+ 'docker',
22
+ 'ci',
23
+ 'verification',
24
+ ])
25
+
26
+ const emptyContribution = () => []
27
+
28
+ function requireNonEmptyString(value, field) {
29
+ if (typeof value !== 'string' || !value.trim()) {
30
+ throw new Error(`Stack adapter ${field} must be a non-empty string`)
31
+ }
32
+ }
33
+
34
+ function validateStringList(values, field, allowedValues) {
35
+ if (!Array.isArray(values) || values.some((value) => typeof value !== 'string')) {
36
+ throw new Error(`Stack adapter ${field} must be an array of strings`)
37
+ }
38
+ if (new Set(values).size !== values.length) {
39
+ throw new Error(`Stack adapter ${field} must not contain duplicates`)
40
+ }
41
+ if (allowedValues) {
42
+ const unknown = values.find((value) => !allowedValues.includes(value))
43
+ if (unknown) throw new Error(`Stack adapter ${field} contains unsupported value: ${unknown}`)
44
+ }
45
+ }
46
+
47
+ function normalizeCompatibility(compatibleWith = {}) {
48
+ if (!compatibleWith || typeof compatibleWith !== 'object' || Array.isArray(compatibleWith)) {
49
+ throw new Error('Stack adapter compatibleWith must be an object')
50
+ }
51
+ const normalized = {}
52
+ for (const [kind, ids] of Object.entries(compatibleWith)) {
53
+ if (!STACK_KINDS.includes(kind)) {
54
+ throw new Error(`Stack adapter compatibleWith has unsupported kind: ${kind}`)
55
+ }
56
+ validateStringList(ids, `compatibleWith.${kind}`)
57
+ normalized[kind] = Object.freeze([...ids])
58
+ }
59
+ return Object.freeze(normalized)
60
+ }
61
+
62
+ function normalizeCapabilities(capabilities) {
63
+ if (!capabilities || typeof capabilities !== 'object' || Array.isArray(capabilities)) {
64
+ throw new Error('Stack adapter capabilities must be an object')
65
+ }
66
+ const {
67
+ applicationShapes = [],
68
+ architectureProfiles = ARCHITECTURE_PROFILES,
69
+ authenticationModels = [],
70
+ ...custom
71
+ } = capabilities
72
+ validateStringList(applicationShapes, 'capabilities.applicationShapes', APPLICATION_SHAPES)
73
+ validateStringList(architectureProfiles, 'capabilities.architectureProfiles', ARCHITECTURE_PROFILES)
74
+ validateStringList(authenticationModels, 'capabilities.authenticationModels')
75
+ if (architectureProfiles.length === 0) {
76
+ throw new Error('Stack adapter must support at least one architecture profile')
77
+ }
78
+ return Object.freeze({
79
+ ...custom,
80
+ applicationShapes: Object.freeze([...applicationShapes]),
81
+ architectureProfiles: Object.freeze([...architectureProfiles]),
82
+ authenticationModels: Object.freeze([...authenticationModels]),
83
+ })
84
+ }
85
+
86
+ function normalizeContributions(contributes = {}) {
87
+ if (!contributes || typeof contributes !== 'object' || Array.isArray(contributes)) {
88
+ throw new Error('Stack adapter contributes must be an object')
89
+ }
90
+ const unknown = Object.keys(contributes).find((name) => !CONTRIBUTION_HOOKS.includes(name))
91
+ if (unknown) throw new Error(`Unknown stack adapter contribution hook: ${unknown}`)
92
+
93
+ return Object.freeze(Object.fromEntries(CONTRIBUTION_HOOKS.map((name) => {
94
+ const contribution = contributes[name] || emptyContribution
95
+ if (typeof contribution !== 'function') {
96
+ throw new Error(`Stack adapter contribution ${name} must be a function`)
97
+ }
98
+ return [name, contribution]
99
+ })))
100
+ }
101
+
102
+ /**
103
+ * Defines the stable boundary between core orchestration and a stack.
104
+ *
105
+ * Adapters describe capabilities and return contributions. They do not write
106
+ * files, install tools, or own dependency versions; core code retains those
107
+ * side effects and compatibility profiles remain the only version source.
108
+ */
109
+ export function defineStackAdapter(definition) {
110
+ if (!definition || typeof definition !== 'object' || Array.isArray(definition)) {
111
+ throw new Error('Stack adapter definition must be an object')
112
+ }
113
+ const unknownField = Object.keys(definition).find((field) => !DEFINITION_FIELDS.has(field))
114
+ if (unknownField) throw new Error(`Unknown stack adapter field: ${unknownField}`)
115
+ requireNonEmptyString(definition.id, 'id')
116
+ if (!IDENTIFIER.test(definition.id)) {
117
+ throw new Error(`Invalid stack adapter id: ${definition.id}`)
118
+ }
119
+ if (!STACK_KINDS.includes(definition.kind)) {
120
+ throw new Error(`Invalid stack adapter kind for ${definition.id}: ${definition.kind}`)
121
+ }
122
+ requireNonEmptyString(definition.label, `${definition.id}.label`)
123
+
124
+ return Object.freeze({
125
+ id: definition.id,
126
+ kind: definition.kind,
127
+ label: definition.label,
128
+ compatibleWith: normalizeCompatibility(definition.compatibleWith),
129
+ capabilities: normalizeCapabilities(definition.capabilities),
130
+ contributes: normalizeContributions(definition.contributes),
131
+ })
132
+ }
133
+
134
+ export function isStackAdapter(value) {
135
+ return Boolean(
136
+ value &&
137
+ typeof value === 'object' &&
138
+ IDENTIFIER.test(value.id || '') &&
139
+ STACK_KINDS.includes(value.kind) &&
140
+ CONTRIBUTION_HOOKS.every((name) => typeof value.contributes?.[name] === 'function'),
141
+ )
142
+ }
@@ -0,0 +1,8 @@
1
+ import { createStackRegistry } from './registry.js'
2
+ import { laravelAdapter } from './laravel/index.js'
3
+
4
+ // This is the only place stack adapters are assembled. Entries stay explicit
5
+ // so adding a source file never silently changes supported generator behavior.
6
+ export const stackRegistry = createStackRegistry([
7
+ laravelAdapter,
8
+ ])
@@ -0,0 +1,27 @@
1
+ const php = (value) => `${value.trim()}\n`
2
+
3
+ export function buildLaravelArchitectureFiles(stack) {
4
+ if (stack.architecture === 'small') return {}
5
+ const files = {
6
+ 'app/Actions/GetSystemStatus.php': php(`<?php
7
+
8
+ namespace App\\Actions;
9
+
10
+ final class GetSystemStatus
11
+ {
12
+ /** @return array{status: string, architecture: string} */
13
+ public function handle(): array
14
+ {
15
+ return ['status' => 'ok', 'architecture' => '${stack.architecture}'];
16
+ }
17
+ }`),
18
+ }
19
+ if (stack.architecture === 'large') {
20
+ files['tests/Architecture/BoundariesTest.php'] = php(`<?php
21
+
22
+ arch('application does not depend on http adapters')
23
+ ->expect('App\\Actions')
24
+ ->not->toUse('App\\Http');`)
25
+ }
26
+ return files
27
+ }
@@ -0,0 +1,13 @@
1
+ export const laravelOidcAuthentication = Object.freeze({
2
+ id: 'laravel-oidc',
3
+ provider: 'auth0',
4
+ composerPackage: 'auth0/login',
5
+ guard: 'auth0-api',
6
+ apiMiddleware: 'auth',
7
+ environment: Object.freeze(['AUTH0_DOMAIN=', 'AUTH0_AUDIENCE=']),
8
+ acceptsRefreshTokens: false,
9
+ })
10
+
11
+ export function isLaravelOidc(authentication) {
12
+ return authentication === laravelOidcAuthentication.id
13
+ }
@@ -0,0 +1,28 @@
1
+ const php = (value) => `${value.trim()}\n`
2
+
3
+ export function buildPublicApiRoutes(authentication) {
4
+ if (authentication === 'public') return php(`<?php
5
+
6
+ use App\\Http\\Controllers\\HealthController;
7
+ use Illuminate\\Support\\Facades\\Route;
8
+
9
+ Route::get('/health', HealthController::class);`)
10
+
11
+ if (authentication === 'undecided') return php(`<?php
12
+
13
+ use App\\Http\\Controllers\\HealthController;
14
+ use Illuminate\\Support\\Facades\\Route;
15
+
16
+ Route::get('/health', HealthController::class);
17
+ Route::fallback(fn () => response()->json(['message' => 'Authentication is not configured.'], 403));`)
18
+
19
+ return null
20
+ }
21
+
22
+ export function buildUnauthenticatedWebRoutes(home) {
23
+ return php(`<?php
24
+
25
+ use Illuminate\\Support\\Facades\\Route;
26
+
27
+ ${home}`)
28
+ }
@@ -0,0 +1,14 @@
1
+ export const sanctumSpaAuthentication = Object.freeze({
2
+ id: 'sanctum-spa',
3
+ apiMiddleware: 'auth:sanctum',
4
+ composerPackage: 'laravel/sanctum',
5
+ middleware: ' $middleware->statefulApi();',
6
+ environment: Object.freeze([
7
+ 'SANCTUM_STATEFUL_DOMAINS=localhost:5173',
8
+ 'CORS_ALLOWED_ORIGINS=http://localhost:5173',
9
+ ]),
10
+ })
11
+
12
+ export function isSanctumSpa(authentication) {
13
+ return authentication === sanctumSpaAuthentication.id
14
+ }
@@ -0,0 +1,17 @@
1
+ export const laravelSessionAuthentication = Object.freeze({
2
+ id: 'laravel-session',
3
+ apiMiddleware: 'auth',
4
+ environment: Object.freeze([
5
+ 'SESSION_DRIVER=database',
6
+ 'SESSION_DOMAIN=localhost',
7
+ 'SESSION_SECURE_COOKIE=false',
8
+ ]),
9
+ })
10
+
11
+ export function usesLaravelSession(authentication) {
12
+ return authentication === laravelSessionAuthentication.id || authentication === 'sanctum-spa'
13
+ }
14
+
15
+ export function sessionEnvironment(authentication) {
16
+ return usesLaravelSession(authentication) ? laravelSessionAuthentication.environment : []
17
+ }
@@ -0,0 +1,50 @@
1
+ import { composerPackageVersion } from '../../compatibility.js'
2
+ import { usesLaravelSession } from './auth/session.js'
3
+ import { isSanctumSpa, sanctumSpaAuthentication } from './auth/sanctum.js'
4
+ import { isLaravelOidc, laravelOidcAuthentication } from './auth/oidc.js'
5
+ import { getLaravelUi } from './ui/index.js'
6
+
7
+ export function buildLaravelComposer(answers, stack) {
8
+ const version = (name) => composerPackageVersion(stack.profile, name, 'laravel scaffold')
9
+ const require = {
10
+ php: stack.profile.runtimes.php,
11
+ 'laravel/framework': version('laravel/framework'),
12
+ }
13
+ if (isSanctumSpa(stack.authentication)) {
14
+ require[sanctumSpaAuthentication.composerPackage] = version(sanctumSpaAuthentication.composerPackage)
15
+ }
16
+ if (isLaravelOidc(stack.authentication)) {
17
+ require[laravelOidcAuthentication.composerPackage] = version(laravelOidcAuthentication.composerPackage)
18
+ }
19
+ if (stack.frontendKey === 'laravel-ui') {
20
+ for (const name of getLaravelUi(stack.laravelUi).composerPackages) require[name] = version(name)
21
+ }
22
+ const requireDev = {
23
+ 'laravel/pint': version('laravel/pint'),
24
+ 'larastan/larastan': version('larastan/larastan'),
25
+ 'mockery/mockery': version('mockery/mockery'),
26
+ 'pestphp/pest': version('pestphp/pest'),
27
+ 'pestphp/pest-plugin-laravel': version('pestphp/pest-plugin-laravel'),
28
+ }
29
+ if (usesLaravelSession(stack.authentication)) requireDev['fakerphp/faker'] = version('fakerphp/faker')
30
+ return `${JSON.stringify({
31
+ name: `app/${answers.projectName}`,
32
+ type: 'project',
33
+ description: answers.projectDescription,
34
+ require,
35
+ 'require-dev': requireDev,
36
+ autoload: { 'psr-4': { 'App\\': 'app/', 'Database\\Factories\\': 'database/factories/', 'Database\\Seeders\\': 'database/seeders/' } },
37
+ 'autoload-dev': { 'psr-4': { 'Tests\\': 'tests/' } },
38
+ scripts: {
39
+ 'post-autoload-dump': ['Illuminate\\Foundation\\ComposerScripts::postAutoloadDump', '@php artisan package:discover --ansi'],
40
+ test: 'php artisan test',
41
+ format: 'pint',
42
+ 'format:check': 'pint --test',
43
+ analyse: 'phpstan analyse --memory-limit=1G',
44
+ check: ['@format:check', '@analyse', '@test'],
45
+ },
46
+ config: { 'optimize-autoloader': true, 'preferred-install': 'dist', 'sort-packages': true, 'allow-plugins': { 'pestphp/pest-plugin': true } },
47
+ 'minimum-stability': 'stable',
48
+ 'prefer-stable': true,
49
+ }, null, 2)}\n`
50
+ }
@@ -0,0 +1,362 @@
1
+ import { buildLaravelComposer } from './composer.js'
2
+ import { buildLaravelArchitectureFiles } from './architecture.js'
3
+ import { buildPublicApiRoutes, buildUnauthenticatedWebRoutes } from './auth/public.js'
4
+ import { laravelSessionAuthentication, sessionEnvironment, usesLaravelSession } from './auth/session.js'
5
+ import { isSanctumSpa, sanctumSpaAuthentication } from './auth/sanctum.js'
6
+ import { isLaravelOidc, laravelOidcAuthentication } from './auth/oidc.js'
7
+ import { getLaravelUi } from './ui/index.js'
8
+
9
+ const php = (value) => `${value.trim()}\n`
10
+
11
+ function authFiles(stack) {
12
+ const loginEnabled = usesLaravelSession(stack.authentication)
13
+ if (isLaravelOidc(stack.authentication)) return {
14
+ 'config/auth0.php': php(`<?php
15
+
16
+ use Auth0\\Laravel\\Configuration;
17
+ use Auth0\\SDK\\Configuration\\SdkConfiguration;
18
+
19
+ return Configuration::VERSION_2 + [
20
+ 'registerGuards' => true,
21
+ 'registerMiddleware' => true,
22
+ 'registerAuthenticationRoutes' => false,
23
+ 'configurationPath' => null,
24
+ 'guards' => [
25
+ 'default' => [
26
+ Configuration::CONFIG_STRATEGY => SdkConfiguration::STRATEGY_NONE,
27
+ Configuration::CONFIG_DOMAIN => Configuration::get(Configuration::CONFIG_DOMAIN),
28
+ Configuration::CONFIG_AUDIENCE => Configuration::get(Configuration::CONFIG_AUDIENCE),
29
+ ],
30
+ 'api' => [Configuration::CONFIG_STRATEGY => SdkConfiguration::STRATEGY_API],
31
+ ],
32
+ 'routes' => [],
33
+ ];`),
34
+ 'tests/Feature/OidcAuthorizationTest.php': php(`<?php
35
+
36
+ it('rejects a request without an access token', function () {
37
+ $this->getJson('/api/me')->assertUnauthorized();
38
+ });`),
39
+ }
40
+ if (!loginEnabled) return {}
41
+ return {
42
+ 'app/Models/User.php': php(`<?php
43
+
44
+ namespace App\\Models;
45
+
46
+ use Database\\Factories\\UserFactory;
47
+ use Illuminate\\Database\\Eloquent\\Factories\\HasFactory;
48
+ use Illuminate\\Foundation\\Auth\\User as Authenticatable;
49
+ use Illuminate\\Notifications\\Notifiable;
50
+
51
+ final class User extends Authenticatable
52
+ {
53
+ /** @use HasFactory<UserFactory> */
54
+ use HasFactory, Notifiable;
55
+
56
+ protected $fillable = ['name', 'email', 'password'];
57
+
58
+ protected $hidden = ['password', 'remember_token'];
59
+
60
+ protected function casts(): array
61
+ {
62
+ return ['email_verified_at' => 'datetime', 'password' => 'hashed'];
63
+ }
64
+ }`),
65
+ 'app/Http/Controllers/AuthController.php': php(`<?php
66
+
67
+ namespace App\\Http\\Controllers;
68
+
69
+ use App\\Models\\User;
70
+ use Illuminate\\Http\\JsonResponse;
71
+ use Illuminate\\Http\\Request;
72
+ use Illuminate\\Support\\Facades\\Auth;
73
+ use Illuminate\\Validation\\ValidationException;
74
+
75
+ final class AuthController
76
+ {
77
+ public function register(Request $request): JsonResponse
78
+ {
79
+ $data = $request->validate(['name' => ['required', 'string', 'max:100'], 'email' => ['required', 'email', 'max:255', 'unique:users,email'], 'password' => ['required', 'string', 'min:12', 'confirmed']]);
80
+ $user = User::create($data);
81
+ Auth::login($user);
82
+ $request->session()->regenerate();
83
+
84
+ return response()->json(['user' => $user->only(['id', 'name', 'email'])], 201);
85
+ }
86
+
87
+ public function login(Request $request): JsonResponse
88
+ {
89
+ $credentials = $request->validate(['email' => ['required', 'email'], 'password' => ['required', 'string']]);
90
+ if (! Auth::attempt($credentials)) {
91
+ throw ValidationException::withMessages(['email' => ['The supplied credentials are invalid.']]);
92
+ }
93
+ $request->session()->regenerate();
94
+
95
+ return response()->json(['user' => $request->user()->only(['id', 'name', 'email'])]);
96
+ }
97
+
98
+ public function logout(Request $request): JsonResponse
99
+ {
100
+ Auth::guard('web')->logout();
101
+ $request->session()->invalidate();
102
+ $request->session()->regenerateToken();
103
+
104
+ return response()->json(null, 204);
105
+ }
106
+ }`),
107
+ 'config/auth.php': php(`<?php
108
+
109
+ use App\\Models\\User;
110
+
111
+ return ['defaults' => ['guard' => 'web', 'passwords' => 'users'], 'guards' => ['web' => ['driver' => 'session', 'provider' => 'users']], 'providers' => ['users' => ['driver' => 'eloquent', 'model' => User::class]], 'passwords' => ['users' => ['provider' => 'users', 'table' => 'password_reset_tokens', 'expire' => 60, 'throttle' => 60]], 'password_timeout' => 10800];`),
112
+ 'config/session.php': php(`<?php
113
+
114
+ return ['driver' => env('SESSION_DRIVER', 'database'), 'lifetime' => (int) env('SESSION_LIFETIME', 120), 'expire_on_close' => false, 'encrypt' => true, 'files' => storage_path('framework/sessions'), 'connection' => env('SESSION_CONNECTION'), 'table' => 'sessions', 'store' => env('SESSION_STORE'), 'lottery' => [2, 100], 'cookie' => env('SESSION_COOKIE', 'app_session'), 'path' => '/', 'domain' => env('SESSION_DOMAIN'), 'secure' => env('SESSION_SECURE_COOKIE', true), 'http_only' => true, 'same_site' => env('SESSION_SAME_SITE', 'lax')];`),
115
+ 'database/migrations/0001_01_01_000000_create_users_table.php': php(`<?php
116
+
117
+ use Illuminate\\Database\\Migrations\\Migration;
118
+ use Illuminate\\Database\\Schema\\Blueprint;
119
+ use Illuminate\\Support\\Facades\\Schema;
120
+
121
+ return new class extends Migration
122
+ {
123
+ public function up(): void
124
+ {
125
+ Schema::create('users', function (Blueprint $table) {
126
+ $table->id();
127
+ $table->string('name');
128
+ $table->string('email')->unique();
129
+ $table->timestamp('email_verified_at')->nullable();
130
+ $table->string('password');
131
+ $table->rememberToken();
132
+ $table->timestamps();
133
+ });
134
+ Schema::create('password_reset_tokens', function (Blueprint $table) {
135
+ $table->string('email')->primary();
136
+ $table->string('token');
137
+ $table->timestamp('created_at')->nullable();
138
+ });
139
+ Schema::create('sessions', function (Blueprint $table) {
140
+ $table->string('id')->primary();
141
+ $table->foreignId('user_id')->nullable()->index();
142
+ $table->string('ip_address', 45)->nullable();
143
+ $table->text('user_agent')->nullable();
144
+ $table->longText('payload');
145
+ $table->integer('last_activity')->index();
146
+ });
147
+ }
148
+
149
+ public function down(): void
150
+ {
151
+ Schema::dropIfExists('sessions');
152
+ Schema::dropIfExists('password_reset_tokens');
153
+ Schema::dropIfExists('users');
154
+ }
155
+ };`),
156
+ 'database/factories/UserFactory.php': php(`<?php
157
+
158
+ namespace Database\\Factories;
159
+
160
+ use Illuminate\\Database\\Eloquent\\Factories\\Factory;
161
+ use Illuminate\\Support\\Facades\\Hash;
162
+ use Illuminate\\Support\\Str;
163
+
164
+ final class UserFactory extends Factory
165
+ {
166
+ public function definition(): array
167
+ {
168
+ return ['name' => fake()->name(), 'email' => fake()->unique()->safeEmail(), 'email_verified_at' => now(), 'password' => Hash::make('a-secure-test-password'), 'remember_token' => Str::random(10)];
169
+ }
170
+ }`),
171
+ 'tests/Feature/AuthenticationTest.php': php(`<?php
172
+
173
+ use App\\Models\\User;
174
+ use Illuminate\\Foundation\\Testing\\RefreshDatabase;
175
+
176
+ uses(RefreshDatabase::class);
177
+
178
+ it('rotates the session after login', function () {
179
+ $user = User::factory()->create(['password' => 'a-secure-test-password']);
180
+ $before = session()->getId();
181
+ $this->postJson('/login', ['email' => $user->email, 'password' => 'a-secure-test-password'])->assertOk();
182
+ expect(session()->getId())->not->toBe($before);
183
+ });
184
+
185
+ it('rejects an unauthenticated protected request', function () {
186
+ $this->getJson('/api/me')->assertUnauthorized();
187
+ });
188
+
189
+ it('invalidates authentication on logout', function () {
190
+ $user = User::factory()->create();
191
+ $this->actingAs($user)->postJson('/logout')->assertNoContent();
192
+ $this->getJson('/api/me')->assertUnauthorized();
193
+ });`),
194
+ }
195
+ }
196
+
197
+ function apiRoutes(stack) {
198
+ const publicRoutes = buildPublicApiRoutes(stack.authentication)
199
+ if (publicRoutes) return publicRoutes
200
+ const middleware = isSanctumSpa(stack.authentication)
201
+ ? sanctumSpaAuthentication.apiMiddleware
202
+ : laravelSessionAuthentication.apiMiddleware
203
+ if (usesLaravelSession(stack.authentication)) {
204
+ return php(`<?php
205
+
206
+ use App\\Http\\Controllers\\HealthController;
207
+ use Illuminate\\Http\\Request;
208
+ use Illuminate\\Support\\Facades\\Route;
209
+
210
+ Route::get('/health', HealthController::class);
211
+ Route::get('/me', fn (Request $request) => response()->json(['user' => $request->user()->only(['id', 'name', 'email'])]))->middleware('${middleware}');`)
212
+ }
213
+ if (isLaravelOidc(stack.authentication)) return php(`<?php
214
+
215
+ use Illuminate\\Http\\Request;
216
+ use Illuminate\\Support\\Facades\\Auth;
217
+ use Illuminate\\Support\\Facades\\Route;
218
+
219
+ Route::get('/health', fn () => response()->json(['status' => 'ok']));
220
+ Auth::shouldUse('${laravelOidcAuthentication.guard}');
221
+ Route::get('/me', fn (Request $request) => response()->json(['subject' => $request->user()?->getAuthIdentifier()]))->middleware('${laravelOidcAuthentication.apiMiddleware}');`)
222
+ throw new Error(`Unsupported Laravel authentication model: ${stack.authentication}`)
223
+ }
224
+
225
+ function webRoutes(stack) {
226
+ const home = stack.frontendKey !== 'laravel-ui'
227
+ ? "Route::get('/', fn () => response()->json(['application' => config('app.name')]));"
228
+ : getLaravelUi(stack.laravelUi).homeRoute
229
+ if (!usesLaravelSession(stack.authentication)) {
230
+ return buildUnauthenticatedWebRoutes(home)
231
+ }
232
+ return php(`<?php
233
+
234
+ use App\\Http\\Controllers\\AuthController;
235
+ use Illuminate\\Support\\Facades\\Route;
236
+
237
+ ${home}
238
+ Route::view('/login', 'auth.login')->middleware('guest')->name('login');
239
+ Route::post('/register', [AuthController::class, 'register'])->middleware('throttle:6,1');
240
+ Route::post('/login', [AuthController::class, 'login'])->middleware('throttle:6,1');
241
+ Route::post('/logout', [AuthController::class, 'logout'])->middleware('auth');`)
242
+ }
243
+
244
+ function uiFiles(answers, stack) {
245
+ if (stack.frontendKey !== 'laravel-ui') return {}
246
+ return getLaravelUi(stack.laravelUi).files(answers, stack)
247
+ }
248
+
249
+ export function buildLaravelFiles(answers, stack) {
250
+ if (stack.backendKey !== 'laravel') return {}
251
+ const root = stack.frontendKey === 'laravel-ui' || stack.frontendKey === 'no-frontend' ? '' : 'backend/'
252
+ const at = (path) => `${root}${path}`
253
+ const files = {
254
+ [at('composer.json')]: buildLaravelComposer(answers, stack),
255
+ [at('artisan')]: php(`#!/usr/bin/env php
256
+ <?php
257
+
258
+ define('LARAVEL_START', microtime(true));
259
+ require __DIR__.'/vendor/autoload.php';
260
+ $status = (require_once __DIR__.'/bootstrap/app.php')->handleCommand(new Symfony\\Component\\Console\\Input\\ArgvInput);
261
+ exit($status);`),
262
+ [at('bootstrap/app.php')]: (() => {
263
+ const middlewareLines = []
264
+ const ui = stack.frontendKey === 'laravel-ui' ? getLaravelUi(stack.laravelUi) : null
265
+ if (isSanctumSpa(stack.authentication)) middlewareLines.push(sanctumSpaAuthentication.middleware)
266
+ if (ui?.middleware) middlewareLines.push(ui.middleware)
267
+ const middlewareBlock = middlewareLines.length ? `function (Middleware $middleware): void {\n${middlewareLines.join('\n')}\n }` : 'function (Middleware $middleware): void {}'
268
+ return php(`<?php
269
+
270
+ ${ui?.middlewareImport ? `${ui.middlewareImport}\n` : ''}use Illuminate\\Foundation\\Application;
271
+ use Illuminate\\Foundation\\Configuration\\Exceptions;
272
+ use Illuminate\\Foundation\\Configuration\\Middleware;
273
+
274
+ return Application::configure(basePath: dirname(__DIR__))
275
+ ->withRouting(web: __DIR__.'/../routes/web.php', api: __DIR__.'/../routes/api.php', commands: __DIR__.'/../routes/console.php', health: '/up')
276
+ ->withMiddleware(${middlewareBlock})
277
+ ->withExceptions(fn (Exceptions $exceptions) => null)
278
+ ->create();`);
279
+ })(),
280
+ [at('bootstrap/providers.php')]: php(`<?php
281
+
282
+ use App\\Providers\\AppServiceProvider;
283
+
284
+ return [AppServiceProvider::class];`),
285
+ [at('public/index.php')]: php(`<?php
286
+
287
+ use Illuminate\\Http\\Request;
288
+
289
+ define('LARAVEL_START', microtime(true));
290
+ require __DIR__.'/../vendor/autoload.php';
291
+ (require_once __DIR__.'/../bootstrap/app.php')->handleRequest(Request::capture());`),
292
+ [at('app/Providers/AppServiceProvider.php')]: php(`<?php
293
+
294
+ namespace App\\Providers;
295
+
296
+ use Illuminate\\Support\\ServiceProvider;
297
+
298
+ final class AppServiceProvider extends ServiceProvider
299
+ {
300
+ public function register(): void {}
301
+
302
+ public function boot(): void {}
303
+ }`),
304
+ [at('app/Http/Controllers/HealthController.php')]: php(`<?php
305
+
306
+ namespace App\\Http\\Controllers;
307
+
308
+ ${stack.architecture === 'small' ? '' : 'use App\\Actions\\GetSystemStatus;\n'}use Illuminate\\Http\\JsonResponse;
309
+
310
+ final class HealthController
311
+ {
312
+ public function __invoke(): JsonResponse
313
+ {
314
+ ${stack.architecture === 'small' ? "return response()->json(['status' => 'ok']);" : 'return response()->json(app(GetSystemStatus::class)->handle());'}
315
+ }
316
+ }`),
317
+ [at('routes/api.php')]: apiRoutes(stack),
318
+ [at('routes/web.php')]: webRoutes(stack),
319
+ [at('routes/console.php')]: php(`<?php
320
+
321
+ use Illuminate\\Support\\Facades\\Artisan;
322
+
323
+ Artisan::command('about:starter', fn () => $this->info('Generated by create-win-project'));`),
324
+ [at('config/app.php')]: php(`<?php
325
+
326
+ return ['name' => env('APP_NAME', '${answers.projectName}'), 'env' => env('APP_ENV', 'production'), 'debug' => (bool) env('APP_DEBUG', false), 'url' => env('APP_URL', 'http://localhost:8000'), 'key' => env('APP_KEY'), 'cipher' => 'AES-256-CBC'];`),
327
+ [at('config/database.php')]: php(`<?php
328
+
329
+ return ['default' => 'pgsql', 'connections' => ['pgsql' => ['driver' => 'pgsql', 'host' => env('DB_HOST', '127.0.0.1'), 'port' => env('DB_PORT', '5432'), 'database' => env('DB_DATABASE', '${answers.projectName.replaceAll('-', '_')}'), 'username' => env('DB_USERNAME', 'postgres'), 'password' => env('DB_PASSWORD', '')]], 'migrations' => ['table' => 'migrations']];`),
330
+ [at('phpunit.xml')]: `<?xml version="1.0" encoding="UTF-8"?>\n<phpunit bootstrap="vendor/autoload.php" colors="true"><testsuites><testsuite name="Application"><directory>tests</directory></testsuite></testsuites><php><env name="APP_ENV" value="testing"/><env name="APP_KEY" value="base64:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="/><env name="SESSION_DRIVER" value="array"/><env name="CACHE_STORE" value="array"/><env name="QUEUE_CONNECTION" value="sync"/></php></phpunit>\n`,
331
+ [at('phpstan.neon')]: `includes:\n - vendor/larastan/larastan/extension.neon\nparameters:\n paths:\n - app\n level: 6\n`,
332
+ [at('tests/TestCase.php')]: php(`<?php
333
+
334
+ namespace Tests;
335
+
336
+ use Illuminate\\Foundation\\Testing\\TestCase as BaseTestCase;
337
+
338
+ abstract class TestCase extends BaseTestCase {}`),
339
+ [at('tests/Pest.php')]: php(`<?php
340
+
341
+ use Tests\\TestCase;
342
+
343
+ pest()->extend(TestCase::class)->in('Feature');`),
344
+ [at('tests/Feature/HealthTest.php')]: php(`<?php
345
+
346
+ it('reports application health', function () {
347
+ $this->getJson('/api/health')->assertOk()->assertJsonPath('status', 'ok');
348
+ });`),
349
+ [at('.env.example')]: `APP_NAME=${answers.projectName}\nAPP_ENV=local\nAPP_KEY=\nAPP_DEBUG=true\nAPP_URL=http://localhost:8000\nDB_CONNECTION=pgsql\nDB_HOST=127.0.0.1\nDB_PORT=5432\nDB_DATABASE=${answers.projectName.replaceAll('-', '_')}\nDB_USERNAME=postgres\nDB_PASSWORD=change-me\n${sessionEnvironment(stack.authentication).map((line) => `${line}\n`).join('')}${isSanctumSpa(stack.authentication) ? sanctumSpaAuthentication.environment.map((line) => `${line}\n`).join('') : ''}${isLaravelOidc(stack.authentication) ? laravelOidcAuthentication.environment.map((line) => `${line}\n`).join('') : ''}`,
350
+ [at('.gitignore')]: `/vendor/\n/node_modules/\n.env\n.phpunit.result.cache\n/public/build/\n/storage/*.key\n`,
351
+ [at('storage/framework/cache/.gitignore')]: "*\n!.gitignore\n",
352
+ [at('storage/framework/sessions/.gitignore')]: "*\n!.gitignore\n",
353
+ [at('storage/framework/views/.gitignore')]: "*\n!.gitignore\n",
354
+ [at('storage/logs/.gitignore')]: "*\n!.gitignore\n",
355
+ [at('bootstrap/cache/.gitignore')]: "*\n!.gitignore\n",
356
+ }
357
+ for (const [path, content] of Object.entries(buildLaravelArchitectureFiles(stack))) files[at(path)] = content
358
+ for (const [path, content] of Object.entries(authFiles(stack))) files[at(path)] = content
359
+ for (const [path, content] of Object.entries(uiFiles(answers, stack))) files[at(path)] = content
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
+ return files
362
+ }