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.
- package/LICENSE +21 -0
- package/README.md +187 -0
- package/ci/expo.yml +28 -0
- package/ci/laravel.yml +49 -0
- package/ci/nextjs.yml +29 -0
- package/ci/springboot.yml +33 -0
- package/ci/vite.yml +36 -0
- package/compatibility/profiles.json +237 -0
- package/docs/ARCHITECTURE.md +118 -0
- package/docs/CONTENT_MODEL.md +36 -0
- package/docs/DEPENDENCY_MAINTENANCE.md +31 -0
- package/index.js +431 -0
- package/lib/application-shapes.js +95 -0
- package/lib/banner.js +45 -0
- package/lib/catalog.js +343 -0
- package/lib/compatibility.js +116 -0
- package/lib/constants.js +3 -0
- package/lib/doctor.js +45 -0
- package/lib/files.js +158 -0
- package/lib/generator.js +366 -0
- package/lib/interview.js +77 -0
- package/lib/laravel-scaffold.js +3 -0
- package/lib/playbooks.js +172 -0
- package/lib/project-location.js +18 -0
- package/lib/scaffold.js +467 -0
- package/lib/stacks/context.js +27 -0
- package/lib/stacks/contract.js +142 -0
- package/lib/stacks/index.js +8 -0
- package/lib/stacks/laravel/architecture.js +27 -0
- package/lib/stacks/laravel/auth/oidc.js +13 -0
- package/lib/stacks/laravel/auth/public.js +28 -0
- package/lib/stacks/laravel/auth/sanctum.js +14 -0
- package/lib/stacks/laravel/auth/session.js +17 -0
- package/lib/stacks/laravel/composer.js +50 -0
- package/lib/stacks/laravel/generate.js +362 -0
- package/lib/stacks/laravel/index.js +18 -0
- package/lib/stacks/laravel/ui/blade.js +15 -0
- package/lib/stacks/laravel/ui/index.js +29 -0
- package/lib/stacks/laravel/ui/inertia-react.js +35 -0
- package/lib/stacks/laravel/ui/livewire.js +39 -0
- package/lib/stacks/laravel/ui/shared.js +14 -0
- package/lib/stacks/registry.js +42 -0
- package/lib/stacks/shared/contributions.js +25 -0
- package/lib/template.js +62 -0
- package/package.json +49 -0
- package/playbooks/INDEX.md +58 -0
- package/playbooks/capabilities/auth/oidc-resource-server.md +15 -0
- package/playbooks/capabilities/auth/spring-session.md +15 -0
- package/playbooks/capabilities/ci/github-actions.manifest.json +10 -0
- package/playbooks/capabilities/ci/github-actions.md +61 -0
- package/playbooks/capabilities/docker/docker.manifest.json +11 -0
- package/playbooks/capabilities/docker/overview.md +55 -0
- package/playbooks/capabilities/flyway/environments.md +5 -0
- package/playbooks/capabilities/flyway/flyway.manifest.json +12 -0
- package/playbooks/capabilities/flyway/migrations.md +7 -0
- package/playbooks/capabilities/flyway/testing.md +5 -0
- package/playbooks/capabilities/laravel/database.md +8 -0
- package/playbooks/capabilities/laravel/migrations.md +15 -0
- package/playbooks/capabilities/laravel/observability.md +8 -0
- package/playbooks/capabilities/laravel/oidc-resource-server.md +10 -0
- package/playbooks/capabilities/laravel/queues.md +8 -0
- package/playbooks/capabilities/laravel/sanctum-spa.md +8 -0
- package/playbooks/capabilities/laravel/scheduler.md +7 -0
- package/playbooks/capabilities/laravel/session-auth.md +8 -0
- package/playbooks/capabilities/laravel/storage-uploads.md +7 -0
- package/playbooks/capabilities/postgresql/architecture.md +5 -0
- package/playbooks/capabilities/postgresql/migrations.md +5 -0
- package/playbooks/capabilities/postgresql/postgresql.manifest.json +15 -0
- package/playbooks/capabilities/postgresql/schema-design.md +7 -0
- package/playbooks/capabilities/postgresql/security.md +5 -0
- package/playbooks/capabilities/postgresql/testing.md +5 -0
- package/playbooks/capabilities/prisma/architecture.md +5 -0
- package/playbooks/capabilities/prisma/migrations.md +11 -0
- package/playbooks/capabilities/prisma/prisma.manifest.json +14 -0
- package/playbooks/capabilities/prisma/runtime.md +5 -0
- package/playbooks/capabilities/prisma/schema.md +5 -0
- package/playbooks/capabilities/prisma/testing.md +5 -0
- package/playbooks/capabilities/supabase/architecture.md +5 -0
- package/playbooks/capabilities/supabase/authentication.md +5 -0
- package/playbooks/capabilities/supabase/expo.md +16 -0
- package/playbooks/capabilities/supabase/migrations.md +7 -0
- package/playbooks/capabilities/supabase/nextjs.md +24 -0
- package/playbooks/capabilities/supabase/rls.md +7 -0
- package/playbooks/capabilities/supabase/supabase.manifest.json +32 -0
- package/playbooks/capabilities/supabase/testing.md +5 -0
- package/playbooks/capabilities/supabase/vite.md +14 -0
- package/playbooks/concerns/axios.md +123 -0
- package/playbooks/concerns/next-safe-action.md +66 -0
- package/playbooks/concerns/next-themes.md +87 -0
- package/playbooks/concerns/nuqs.md +59 -0
- package/playbooks/concerns/t3-env.md +48 -0
- package/playbooks/concerns/tanstack-query.md +145 -0
- package/playbooks/concerns/zod.md +174 -0
- package/playbooks/concerns/zustand.md +93 -0
- package/playbooks/devops/makefile.manifest.json +10 -0
- package/playbooks/devops/makefile.md +556 -0
- package/playbooks/devops/pr-template.manifest.json +10 -0
- package/playbooks/devops/pr-template.md +106 -0
- package/playbooks/platform/laravel-ui/blade/architecture.md +5 -0
- package/playbooks/platform/laravel-ui/blade/runtime.md +5 -0
- package/playbooks/platform/laravel-ui/blade/security.md +5 -0
- package/playbooks/platform/laravel-ui/blade/structure.md +5 -0
- package/playbooks/platform/laravel-ui/blade/testing.md +5 -0
- package/playbooks/platform/laravel-ui/inertia-react/architecture.md +5 -0
- package/playbooks/platform/laravel-ui/inertia-react/runtime.md +5 -0
- package/playbooks/platform/laravel-ui/inertia-react/security.md +5 -0
- package/playbooks/platform/laravel-ui/inertia-react/structure.md +5 -0
- package/playbooks/platform/laravel-ui/inertia-react/testing.md +5 -0
- package/playbooks/platform/laravel-ui/livewire/architecture.md +5 -0
- package/playbooks/platform/laravel-ui/livewire/runtime.md +5 -0
- package/playbooks/platform/laravel-ui/livewire/security.md +5 -0
- package/playbooks/platform/laravel-ui/livewire/structure.md +5 -0
- package/playbooks/platform/laravel-ui/livewire/testing.md +5 -0
- package/playbooks/platform/mobile.manifest.json +17 -0
- package/playbooks/platform/mobile.md +15 -0
- package/playbooks/platform/web.manifest.json +17 -0
- package/playbooks/platform/web.md +17 -0
- package/playbooks/stack/expo/architecture.md +27 -0
- package/playbooks/stack/expo/runtime.md +14 -0
- package/playbooks/stack/expo/security.md +18 -0
- package/playbooks/stack/expo/structure.md +26 -0
- package/playbooks/stack/expo/testing.md +13 -0
- package/playbooks/stack/laravel/architecture.md +34 -0
- package/playbooks/stack/laravel/runtime.md +27 -0
- package/playbooks/stack/laravel/security.md +23 -0
- package/playbooks/stack/laravel/structure.md +29 -0
- package/playbooks/stack/laravel/testing.md +22 -0
- package/playbooks/stack/laravel-ui.manifest.json +42 -0
- package/playbooks/stack/laravel.manifest.json +35 -0
- package/playbooks/stack/nextjs/architecture.md +62 -0
- package/playbooks/stack/nextjs/runtime.md +31 -0
- package/playbooks/stack/nextjs/security.md +32 -0
- package/playbooks/stack/nextjs/structure.md +50 -0
- package/playbooks/stack/nextjs/testing.md +20 -0
- package/playbooks/stack/nextjs.manifest.json +49 -0
- package/playbooks/stack/no-frontend.manifest.json +25 -0
- package/playbooks/stack/none.manifest.json +21 -0
- package/playbooks/stack/react-native.manifest.json +49 -0
- package/playbooks/stack/react-vite/architecture.md +33 -0
- package/playbooks/stack/react-vite/runtime.md +13 -0
- package/playbooks/stack/react-vite/security.md +16 -0
- package/playbooks/stack/react-vite/structure.md +23 -0
- package/playbooks/stack/react-vite/testing.md +12 -0
- package/playbooks/stack/react-vite.manifest.json +46 -0
- package/playbooks/stack/springboot/architecture.md +40 -0
- package/playbooks/stack/springboot/runtime.md +28 -0
- package/playbooks/stack/springboot/security.md +28 -0
- package/playbooks/stack/springboot/structure.md +30 -0
- package/playbooks/stack/springboot/testing.md +22 -0
- package/playbooks/stack/springboot.manifest.json +38 -0
- package/playbooks/styling/css-modules-extensions.md +267 -0
- package/playbooks/styling/css-modules.manifest.json +11 -0
- package/playbooks/styling/native-styles.manifest.json +9 -0
- package/playbooks/styling/native-styles.md +29 -0
- package/playbooks/styling/tailwind-extensions.md +182 -0
- package/playbooks/styling/tailwind.manifest.json +11 -0
- package/playbooks/universal/accessibility.manifest.json +11 -0
- package/playbooks/universal/accessibility.md +45 -0
- package/playbooks/universal/coding-rules.manifest.json +12 -0
- package/playbooks/universal/coding-rules.md +281 -0
- package/playbooks/universal/error-handling.manifest.json +10 -0
- package/playbooks/universal/error-handling.md +21 -0
- package/playbooks/universal/git-conventions.manifest.json +10 -0
- package/playbooks/universal/git-conventions.md +186 -0
- package/playbooks/universal/observability.manifest.json +10 -0
- package/playbooks/universal/observability.md +29 -0
- package/playbooks/universal/security.manifest.json +11 -0
- package/playbooks/universal/security.md +30 -0
- package/playbooks/universal/typescript.manifest.json +11 -0
- package/playbooks/universal/typescript.md +272 -0
- package/scripts/compatibility-matrix.mjs +48 -0
- package/scripts/validate-content.mjs +76 -0
- package/scripts/verify-generated.mjs +153 -0
- package/templates/agents/nextjs.md +47 -0
- package/templates/agents/react-native.md +48 -0
- package/templates/agents/react-vite.md +48 -0
- package/templates/docker/compose/postgres.yml +29 -0
- package/templates/docker/compose/springboot.yml +69 -0
- package/templates/docker/compose/supabase.yml +18 -0
- package/templates/docker/compose-prod/springboot.yml +64 -0
- package/templates/docker/dockerfile/laravel-inertia.dev.dockerfile +21 -0
- package/templates/docker/dockerfile/laravel.dev.dockerfile +12 -0
- package/templates/docker/dockerfile/laravel.prod.dockerfile +13 -0
- package/templates/docker/dockerfile/nextjs.dev.dockerfile +6 -0
- package/templates/docker/dockerfile/nextjs.prod.dockerfile +18 -0
- package/templates/docker/dockerfile/springboot.dev.dockerfile +5 -0
- package/templates/docker/dockerfile/springboot.prod.dockerfile +10 -0
- package/templates/docker/dockerfile/vite.dev.dockerfile +6 -0
- package/templates/docker/dockerfile/vite.prod.dockerfile +12 -0
- package/templates/gitignore/base.gitignore +32 -0
- package/templates/gitignore/nextjs.gitignore +40 -0
- package/templates/gitignore/react-native.gitignore +43 -0
- package/templates/gitignore/react-vite.gitignore +39 -0
- package/templates/makefile/frontend.mk +31 -0
- package/templates/makefile/laravel.mk +47 -0
- package/templates/makefile/postgres.mk +40 -0
- package/templates/makefile/springboot.mk +125 -0
- 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
|
+
}
|