create-win-project 1.4.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +54 -147
- package/checks/check-compatibility.js +37 -8
- package/checks/check-generated-project.js +27 -3
- package/checks/check-library.js +13 -3
- package/checks/check-package.js +15 -0
- package/checks/classify-changes.js +5 -15
- package/checks/run-compatibility-shard.js +21 -0
- package/docs/README.md +20 -0
- package/docs/capabilities.md +13 -0
- package/docs/compatibility.md +13 -0
- package/docs/generated-project.md +12 -0
- package/docs/getting-started.md +26 -0
- package/docs/{ARCHITECTURE.md → maintainers/architecture.md} +3 -3
- package/docs/maintainers/ci-strategy.md +46 -0
- package/docs/{CONTRIBUTING.md → maintainers/contributing.md} +7 -7
- package/docs/migration-v2.md +13 -0
- package/docs/production-contract.md +24 -0
- package/library/INDEX.md +1 -1
- package/library/compatibility-impact.json +14 -0
- package/library/development-tools/devops/makefile/commands.md +11 -0
- package/library/development-tools/devops/makefile/definition.json +2 -2
- package/library/development-tools/devops/makefile/docker.md +9 -0
- package/library/development-tools/devops/makefile/validation.md +5 -0
- package/library/features/sqlalchemy-alembic.md +17 -0
- package/library/optional-features/concerns/zod/errors.md +5 -0
- package/library/optional-features/concerns/zod/testing.md +5 -0
- package/library/optional-features/concerns/zod/validation.md +21 -0
- package/library/optional-features/styling/css-modules/definition.json +3 -2
- package/library/optional-features/styling/css-modules/responsive.md +5 -0
- package/library/optional-features/styling/css-modules/theme.md +7 -0
- package/library/stacks/expo/definition.json +2 -2
- package/library/stacks/fastapi/architecture.md +40 -0
- package/library/stacks/fastapi/definition.json +39 -0
- package/library/stacks/fastapi/runtime.md +25 -0
- package/library/stacks/fastapi/security.md +26 -0
- package/library/stacks/fastapi/structure.md +27 -0
- package/library/stacks/fastapi/testing.md +23 -0
- package/library/stacks/nextjs/definition.json +2 -2
- package/library/stacks/no-frontend/definition.json +1 -1
- package/library/stacks/react-vite/definition.json +2 -2
- package/library/tested-versions.json +42 -2
- package/library/universal/coding-rules/definition.json +3 -3
- package/library/universal/coding-rules/hygiene.md +9 -0
- package/library/universal/coding-rules/naming.md +17 -0
- package/library/universal/git-conventions/branches.md +5 -0
- package/library/universal/git-conventions/commits.md +7 -0
- package/library/universal/git-conventions/definition.json +4 -2
- package/library/universal/git-conventions/workflow.md +5 -0
- package/library/universal/typescript/boundaries.md +13 -0
- package/library/universal/typescript/definition.json +3 -3
- package/library/universal/typescript/errors.md +5 -0
- package/library/universal/typescript/patterns.md +7 -0
- package/package.json +4 -6
- package/src/cli/arguments.js +11 -0
- package/src/cli/main.js +18 -0
- package/src/cli/questions.js +16 -15
- package/src/cli/system-check.js +22 -2
- package/src/engine/load-library.js +3 -2
- package/src/engine/project-files.js +10 -1
- package/src/engine/project-guidance.js +2 -1
- package/src/engine/project-shapes.js +4 -4
- package/src/engine/render-templates.js +3 -0
- package/src/engine/tested-versions.js +21 -2
- package/src/engine/upgrade-report.js +20 -0
- package/src/stacks/available-stacks.js +2 -0
- package/src/stacks/backends/fastapi/ci.js +3 -0
- package/src/stacks/backends/fastapi/create-files.js +874 -0
- package/src/stacks/backends/fastapi/docker.js +75 -0
- package/src/stacks/backends/fastapi/environment.js +3 -0
- package/src/stacks/backends/fastapi/index.js +32 -0
- package/src/stacks/compose-files.js +19 -1
- package/src/stacks/create-project.js +128 -7
- package/src/stacks/frontends/nextjs/index.js +1 -1
- package/src/stacks/frontends/react-native/create-files.js +4 -1
- package/src/stacks/frontends/react-native/environment.js +1 -1
- package/src/stacks/frontends/react-native/index.js +1 -1
- package/src/stacks/frontends/react-vite/environment.js +1 -1
- package/src/stacks/frontends/react-vite/index.js +1 -1
- package/src/stacks/shared/capability-packs.js +31 -0
- package/src/stacks/shared/environment.js +12 -4
- package/src/stacks/shared/javascript-package.js +6 -0
- package/templates/ci/fastapi.yml +62 -0
- package/templates/docker/compose-prod/fastapi.yml +52 -0
- package/templates/docker/compose-prod/springboot.yml +19 -1
- package/templates/docker/dockerfile/fastapi.dev.dockerfile +9 -0
- package/templates/docker/dockerfile/fastapi.prod.dockerfile +11 -0
- package/templates/docker/dockerfile/nextjs.prod.dockerfile +1 -0
- package/templates/docker/dockerfile/springboot.prod.dockerfile +4 -1
- package/templates/docker/dockerfile/vite.prod.dockerfile +2 -1
- package/templates/makefile/fastapi.mk +97 -0
- package/library/development-tools/devops/makefile/makefile.md +0 -556
- package/library/optional-features/concerns/zod.md +0 -174
- package/library/optional-features/styling/css-modules/css-modules-extensions.md +0 -267
- package/library/universal/coding-rules/coding-rules.md +0 -281
- package/library/universal/git-conventions/git-conventions.md +0 -186
- package/library/universal/typescript/typescript.md +0 -272
- /package/docs/{CONTENT_MODEL.md → maintainers/content-model.md} +0 -0
- /package/docs/{DEPENDENCY_MAINTENANCE.md → maintainers/dependencies.md} +0 -0
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
"maven": "3.9.16",
|
|
16
16
|
"postgres": "16",
|
|
17
17
|
"php": "8.5.10",
|
|
18
|
-
"composer": "2.10.3"
|
|
18
|
+
"composer": "2.10.3",
|
|
19
|
+
"python": "3.14.7",
|
|
20
|
+
"uv": "0.12.10"
|
|
19
21
|
},
|
|
20
22
|
"images": {
|
|
21
23
|
"node": {
|
|
@@ -45,8 +47,26 @@
|
|
|
45
47
|
"composer": {
|
|
46
48
|
"repository": "composer",
|
|
47
49
|
"tag": "2.10.3"
|
|
50
|
+
},
|
|
51
|
+
"python": {
|
|
52
|
+
"repository": "python",
|
|
53
|
+
"tag": "3.14.7-slim"
|
|
48
54
|
}
|
|
49
55
|
},
|
|
56
|
+
"pythonPackages": {
|
|
57
|
+
"alembic": "1.19.2",
|
|
58
|
+
"asyncpg": "0.31.0",
|
|
59
|
+
"fastapi": "0.141.1",
|
|
60
|
+
"httpx": "0.28.1",
|
|
61
|
+
"mypy": "2.3.1",
|
|
62
|
+
"pydantic-settings": "2.15.0",
|
|
63
|
+
"pyjwt": "2.13.0",
|
|
64
|
+
"pytest": "9.1.1",
|
|
65
|
+
"pytest-asyncio": "1.4.0",
|
|
66
|
+
"ruff": "0.16.6",
|
|
67
|
+
"sqlalchemy": "2.0.52",
|
|
68
|
+
"uvicorn": "0.52.4"
|
|
69
|
+
},
|
|
50
70
|
"composerPackages": {
|
|
51
71
|
"auth0/login": "7.22.0",
|
|
52
72
|
"fakerphp/faker": "1.24.1",
|
|
@@ -133,7 +153,9 @@
|
|
|
133
153
|
"maven": "3.9.16",
|
|
134
154
|
"postgres": "16",
|
|
135
155
|
"php": "8.5.10",
|
|
136
|
-
"composer": "2.10.3"
|
|
156
|
+
"composer": "2.10.3",
|
|
157
|
+
"python": "3.13.15",
|
|
158
|
+
"uv": "0.12.10"
|
|
137
159
|
},
|
|
138
160
|
"images": {
|
|
139
161
|
"node": {
|
|
@@ -163,8 +185,26 @@
|
|
|
163
185
|
"composer": {
|
|
164
186
|
"repository": "composer",
|
|
165
187
|
"tag": "2.10.3"
|
|
188
|
+
},
|
|
189
|
+
"python": {
|
|
190
|
+
"repository": "python",
|
|
191
|
+
"tag": "3.13.15-slim"
|
|
166
192
|
}
|
|
167
193
|
},
|
|
194
|
+
"pythonPackages": {
|
|
195
|
+
"alembic": "1.19.2",
|
|
196
|
+
"asyncpg": "0.31.0",
|
|
197
|
+
"fastapi": "0.141.1",
|
|
198
|
+
"httpx": "0.28.1",
|
|
199
|
+
"mypy": "2.3.1",
|
|
200
|
+
"pydantic-settings": "2.15.0",
|
|
201
|
+
"pyjwt": "2.13.0",
|
|
202
|
+
"pytest": "9.1.1",
|
|
203
|
+
"pytest-asyncio": "1.4.0",
|
|
204
|
+
"ruff": "0.16.6",
|
|
205
|
+
"sqlalchemy": "2.0.52",
|
|
206
|
+
"uvicorn": "0.52.4"
|
|
207
|
+
},
|
|
168
208
|
"composerPackages": {
|
|
169
209
|
"auth0/login": "7.22.0",
|
|
170
210
|
"fakerphp/faker": "1.24.1",
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
"id": "coding-rules",
|
|
3
3
|
"kind": "universal",
|
|
4
4
|
"label": "Coding Rules",
|
|
5
|
-
"
|
|
5
|
+
"playbooks": ["universal/coding-rules/naming.md", "universal/coding-rules/hygiene.md"],
|
|
6
6
|
"required": true,
|
|
7
7
|
"folders": [],
|
|
8
8
|
"devDeps": ["prettier"],
|
|
9
9
|
"concerns": [
|
|
10
|
-
{ "id": "naming", "required": true, "sections": ["Naming", "Functions", "Imports", "Constants"] },
|
|
11
|
-
{ "id": "no-debug", "required": true, "sections": ["No Debug Code in Commits", "One Thing Per File"] }
|
|
10
|
+
{ "id": "naming", "required": true, "playbook": "universal/coding-rules/naming.md", "sections": ["Naming", "Functions", "Imports", "Constants"] },
|
|
11
|
+
{ "id": "no-debug", "required": true, "playbook": "universal/coding-rules/hygiene.md", "sections": ["No Debug Code in Commits", "One Thing Per File"] }
|
|
12
12
|
]
|
|
13
13
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Code hygiene
|
|
2
|
+
|
|
3
|
+
## No Debug Code in Commits
|
|
4
|
+
|
|
5
|
+
Remove temporary logging, breakpoints, fixtures, bypasses, and commented-out implementations. Retain structured operational logging only when it has defined level, fields, privacy handling, and ownership.
|
|
6
|
+
|
|
7
|
+
## One Thing Per File
|
|
8
|
+
|
|
9
|
+
A file should have one clear reason to change. Keep tightly coupled helpers beside their owner, but split unrelated components, routes, services, or policies. Do not create one-line files merely to satisfy this rule.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Naming and structure
|
|
2
|
+
|
|
3
|
+
## Naming
|
|
4
|
+
|
|
5
|
+
Use names that describe domain intent. Avoid unexplained abbreviations, generic `data`/`utils` modules, and names that repeat their containing folder.
|
|
6
|
+
|
|
7
|
+
## Functions
|
|
8
|
+
|
|
9
|
+
Keep functions focused, make side effects visible, validate at boundaries, and return early for invalid states.
|
|
10
|
+
|
|
11
|
+
## Imports
|
|
12
|
+
|
|
13
|
+
Group platform, external, and local imports consistently. Respect architecture boundaries and remove unused imports.
|
|
14
|
+
|
|
15
|
+
## Constants
|
|
16
|
+
|
|
17
|
+
Name shared constants by meaning, keep feature-local values near their owner, and do not disguise mutable state as a constant.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Git commits
|
|
2
|
+
|
|
3
|
+
## Commit Convention
|
|
4
|
+
|
|
5
|
+
Use `type(scope): imperative summary`. Each commit should represent one coherent, testable change and must not include secrets, generated noise, debug code, or unrelated formatting.
|
|
6
|
+
|
|
7
|
+
Explain motivation and migration impact in the body when the diff alone is insufficient.
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
"id": "git-conventions",
|
|
3
3
|
"kind": "universal",
|
|
4
4
|
"label": "Git Conventions",
|
|
5
|
-
"
|
|
5
|
+
"playbooks": ["universal/git-conventions/branches.md", "universal/git-conventions/commits.md", "universal/git-conventions/workflow.md"],
|
|
6
6
|
"required": true,
|
|
7
7
|
"folders": [],
|
|
8
8
|
"concerns": [
|
|
9
|
-
{ "id": "git", "required": true, "
|
|
9
|
+
{ "id": "git-branches", "required": true, "playbook": "universal/git-conventions/branches.md", "sections": ["Branch Structure"] },
|
|
10
|
+
{ "id": "git-commits", "required": true, "playbook": "universal/git-conventions/commits.md", "sections": ["Commit Convention"] },
|
|
11
|
+
{ "id": "git-workflow", "required": true, "playbook": "universal/git-conventions/workflow.md", "sections": ["Daily Workflow"] }
|
|
10
12
|
]
|
|
11
13
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Git workflow
|
|
2
|
+
|
|
3
|
+
## Daily Workflow
|
|
4
|
+
|
|
5
|
+
Inspect status and upstream changes before editing. Preserve user work, stage deliberately, review the staged diff, and run risk-appropriate checks before committing. Do not push, merge, rewrite shared history, or create releases without authorization.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# TypeScript boundaries
|
|
2
|
+
|
|
3
|
+
## Strict Mode — Always On
|
|
4
|
+
|
|
5
|
+
Keep `strict` enabled. Model inputs at trust boundaries as `unknown`, validate them, then pass typed values inward. Do not weaken compiler options for one failing file.
|
|
6
|
+
|
|
7
|
+
## No any
|
|
8
|
+
|
|
9
|
+
Avoid `any`. Prefer `unknown`, generics, discriminated unions, or a narrow local type. If an external library forces an escape hatch, isolate and document it at the adapter boundary.
|
|
10
|
+
|
|
11
|
+
## Type vs Interface
|
|
12
|
+
|
|
13
|
+
Use interfaces for extendable object contracts and types for unions, intersections, mapped types, and aliases. Consistency within a feature matters more than stylistic conversion.
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"id": "typescript",
|
|
3
3
|
"kind": "universal",
|
|
4
4
|
"label": "TypeScript",
|
|
5
|
-
"
|
|
5
|
+
"playbooks": ["universal/typescript/boundaries.md", "universal/typescript/errors.md", "universal/typescript/patterns.md"],
|
|
6
6
|
"required": true,
|
|
7
7
|
"folders": [],
|
|
8
8
|
"concerns": [
|
|
9
|
-
{ "id": "typescript-strict", "required": true, "sections": ["Strict Mode — Always On", "No any", "Type vs Interface"] },
|
|
10
|
-
{ "id": "validation", "required": false, "when": "Project validates external/runtime input", "sections": ["Zod for Runtime Validation"] }
|
|
9
|
+
{ "id": "typescript-strict", "required": true, "playbook": "universal/typescript/boundaries.md", "sections": ["Strict Mode — Always On", "No any", "Type vs Interface"] },
|
|
10
|
+
{ "id": "validation", "required": false, "when": "Project validates external/runtime input", "playbook": "universal/typescript/patterns.md", "sections": ["Zod for Runtime Validation"] }
|
|
11
11
|
]
|
|
12
12
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# TypeScript patterns
|
|
2
|
+
|
|
3
|
+
## Zod for Runtime Validation
|
|
4
|
+
|
|
5
|
+
Static types do not validate runtime data. Parse environment variables, requests, storage, and third-party responses at entry points. Export the inferred type from the schema instead of maintaining a duplicate interface.
|
|
6
|
+
|
|
7
|
+
Prefer small pure functions, explicit return types on public APIs, `satisfies` for configuration, and immutable inputs where mutation is not part of the contract.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-win-project",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Project scaffolding CLI by Win",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -18,10 +18,7 @@
|
|
|
18
18
|
"templates",
|
|
19
19
|
"checks",
|
|
20
20
|
"public/logo.svg",
|
|
21
|
-
"docs
|
|
22
|
-
"docs/CONTENT_MODEL.md",
|
|
23
|
-
"docs/DEPENDENCY_MAINTENANCE.md",
|
|
24
|
-
"docs/CONTRIBUTING.md",
|
|
21
|
+
"docs",
|
|
25
22
|
"README.md",
|
|
26
23
|
"LICENSE"
|
|
27
24
|
],
|
|
@@ -30,6 +27,7 @@
|
|
|
30
27
|
"doctor": "node index.js doctor",
|
|
31
28
|
"test": "vitest run",
|
|
32
29
|
"validate:content": "node checks/check-library.js",
|
|
30
|
+
"package:check": "node checks/check-package.js",
|
|
33
31
|
"matrix:smoke": "node checks/check-compatibility.js --scope=smoke",
|
|
34
32
|
"matrix:full": "node checks/check-compatibility.js --scope=full",
|
|
35
33
|
"verify:generated": "node checks/check-generated-project.js"
|
|
@@ -45,6 +43,6 @@
|
|
|
45
43
|
"ora": "7.0.1"
|
|
46
44
|
},
|
|
47
45
|
"devDependencies": {
|
|
48
|
-
"vitest": "
|
|
46
|
+
"vitest": "5.0.0"
|
|
49
47
|
}
|
|
50
48
|
}
|
package/src/cli/arguments.js
CHANGED
|
@@ -6,6 +6,7 @@ function option(args, name) {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
export function parseArguments(args) {
|
|
9
|
+
const upgradeReportIndex = args.indexOf('upgrade-report')
|
|
9
10
|
const profile = option(args, 'profile')
|
|
10
11
|
const shape = option(args, 'shape')
|
|
11
12
|
const frontendValue = option(args, 'frontend')
|
|
@@ -16,6 +17,9 @@ export function parseArguments(args) {
|
|
|
16
17
|
const authentication = option(args, 'authentication')
|
|
17
18
|
const authAudience = option(args, 'auth-audience')
|
|
18
19
|
const laravelUi = option(args, 'laravel-ui')
|
|
20
|
+
const uploads = option(args, 'uploads')
|
|
21
|
+
const backgroundJobs = option(args, 'background-jobs')
|
|
22
|
+
const offline = option(args, 'offline')
|
|
19
23
|
const install = args.includes('--install')
|
|
20
24
|
const noInstall = args.includes('--no-install')
|
|
21
25
|
|
|
@@ -34,6 +38,9 @@ export function parseArguments(args) {
|
|
|
34
38
|
if (laravelUi && !laravelUis.some((ui) => ui.id === laravelUi)) {
|
|
35
39
|
throw new Error('--laravel-ui must be blade, livewire, or inertia-react')
|
|
36
40
|
}
|
|
41
|
+
if (uploads && !['none', 'object-storage'].includes(uploads)) throw new Error('--uploads must be none or object-storage')
|
|
42
|
+
if (backgroundJobs && !['none', 'queue'].includes(backgroundJobs)) throw new Error('--background-jobs must be none or queue')
|
|
43
|
+
if (offline && !['none', 'cache', 'sync'].includes(offline)) throw new Error('--offline must be none, cache, or sync')
|
|
37
44
|
if (install && noInstall) throw new Error('Use either --install or --no-install, not both')
|
|
38
45
|
|
|
39
46
|
return Object.freeze({
|
|
@@ -45,8 +52,12 @@ export function parseArguments(args) {
|
|
|
45
52
|
authentication,
|
|
46
53
|
authAudience,
|
|
47
54
|
laravelUi,
|
|
55
|
+
uploads,
|
|
56
|
+
backgroundJobs,
|
|
57
|
+
offline,
|
|
48
58
|
install,
|
|
49
59
|
noInstall,
|
|
50
60
|
doctor: args[0] === 'doctor' || args.includes('--doctor'),
|
|
61
|
+
upgradeReportPath: upgradeReportIndex >= 0 ? (args[upgradeReportIndex + 1] || '.') : undefined,
|
|
51
62
|
})
|
|
52
63
|
}
|
package/src/cli/main.js
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
startProjectSpinner,
|
|
27
27
|
} from './display.js'
|
|
28
28
|
import { parseArguments } from './arguments.js'
|
|
29
|
+
import { createUpgradeReport } from '../engine/upgrade-report.js'
|
|
29
30
|
import {
|
|
30
31
|
loadCatalog, resolveStack,
|
|
31
32
|
} from '../engine/load-library.js'
|
|
@@ -50,8 +51,16 @@ const architectureArg = args.architecture
|
|
|
50
51
|
const authenticationArg = args.authentication
|
|
51
52
|
const authAudienceArg = args.authAudience
|
|
52
53
|
const laravelUiArg = args.laravelUi
|
|
54
|
+
const uploadsArg = args.uploads
|
|
55
|
+
const backgroundJobsArg = args.backgroundJobs
|
|
56
|
+
const offlineArg = args.offline
|
|
53
57
|
const wantsInstall = args.install
|
|
54
58
|
const skipsInstall = args.noInstall
|
|
59
|
+
if (args.upgradeReportPath) {
|
|
60
|
+
const report = await createUpgradeReport(args.upgradeReportPath, path.join(projectRoot, 'library/tested-versions.json'))
|
|
61
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`)
|
|
62
|
+
return
|
|
63
|
+
}
|
|
55
64
|
const { profile } = await loadCompatibility(
|
|
56
65
|
path.join(projectRoot, 'library/tested-versions.json'),
|
|
57
66
|
profileArg,
|
|
@@ -85,6 +94,11 @@ answers.architecture = architectureArg || answers.architecture || 'medium'
|
|
|
85
94
|
answers.authentication = authenticationArg || answers.authentication || 'not-yet'
|
|
86
95
|
answers.authAudience = authAudienceArg || answers.authAudience || (catalog.byId[answers.frontend]?.platform === 'mobile' ? 'multi-client' : 'website')
|
|
87
96
|
answers.laravelUi = laravelUiArg || answers.laravelUi || (answers.frontend === 'laravel-ui' ? 'blade' : undefined)
|
|
97
|
+
answers.uploads = uploadsArg || answers.uploads || 'none'
|
|
98
|
+
answers.backgroundJobs = backgroundJobsArg || answers.backgroundJobs || 'none'
|
|
99
|
+
answers.offline = offlineArg || answers.offline || 'none'
|
|
100
|
+
answers.testing = answers.frontend === 'react-native' ? 'basic' : 'full'
|
|
101
|
+
answers.githubActions = true
|
|
88
102
|
if (wantsInstall) answers.installDependencies = true
|
|
89
103
|
if (skipsInstall) answers.installDependencies = false
|
|
90
104
|
|
|
@@ -157,6 +171,10 @@ try {
|
|
|
157
171
|
const laravelRoot = ['laravel-ui', 'no-frontend'].includes(stack.frontendKey) ? generatedProjectRoot : path.join(generatedProjectRoot, 'backend')
|
|
158
172
|
steps.push({ command: process.platform === 'win32' ? 'composer.bat' : 'composer', args: ['install'], cwd: laravelRoot, retry: `cd ${path.relative(process.cwd(), laravelRoot)} && composer install` })
|
|
159
173
|
}
|
|
174
|
+
if (stack.backendKey === 'fastapi') {
|
|
175
|
+
const apiRoot = stack.frontendKey === 'no-frontend' ? generatedProjectRoot : path.join(generatedProjectRoot, 'backend')
|
|
176
|
+
steps.push({ command: 'uv', args: ['sync'], cwd: apiRoot, retry: `cd ${path.relative(process.cwd(), apiRoot)} && uv sync` })
|
|
177
|
+
}
|
|
160
178
|
const needsNpm = stack.frontendKey !== 'no-frontend' && (stack.frontendKey !== 'laravel-ui' || answers.laravelUi === 'inertia-react')
|
|
161
179
|
if (needsNpm) {
|
|
162
180
|
const npmRoot = stack.frontendKey === 'react' ? path.join(generatedProjectRoot, 'frontend') : generatedProjectRoot
|
package/src/cli/questions.js
CHANGED
|
@@ -106,12 +106,12 @@ export function buildQuestions({ args, catalog }) {
|
|
|
106
106
|
},
|
|
107
107
|
{
|
|
108
108
|
type: 'list', name: 'authentication',
|
|
109
|
-
message: (answers) => ['supabase', 'springboot', 'laravel'].includes(answers.backend)
|
|
109
|
+
message: (answers) => ['supabase', 'springboot', 'laravel', 'fastapi'].includes(answers.backend)
|
|
110
110
|
? 'User authentication'
|
|
111
111
|
: `User authentication ${chalk.yellow(`(generation unavailable for ${answers.backend === 'none' ? 'a frontend-only project' : 'a PostgreSQL-only backend'})`)}`,
|
|
112
112
|
choices: (answers) => {
|
|
113
113
|
const choices = []
|
|
114
|
-
if (['supabase', 'springboot', 'laravel'].includes(answers.backend)) {
|
|
114
|
+
if (['supabase', 'springboot', 'laravel', 'fastapi'].includes(answers.backend)) {
|
|
115
115
|
choices.push(choice('Yes', 'yes'))
|
|
116
116
|
}
|
|
117
117
|
choices.push(
|
|
@@ -130,22 +130,23 @@ export function buildQuestions({ args, catalog }) {
|
|
|
130
130
|
{ name: 'Website and mobile — use a trusted identity provider for every client', value: 'multi-client' },
|
|
131
131
|
],
|
|
132
132
|
default: 'website',
|
|
133
|
-
when: (answers) => ['springboot', 'laravel'].includes(answers.backend) && answers.frontend !== 'laravel-ui' &&
|
|
133
|
+
when: (answers) => ['springboot', 'laravel', 'fastapi'].includes(answers.backend) && answers.frontend !== 'laravel-ui' &&
|
|
134
134
|
(args.authentication || answers.authentication) === 'yes' && !args.authAudience,
|
|
135
135
|
},
|
|
136
136
|
{
|
|
137
|
-
type: 'list', name: '
|
|
138
|
-
choices:
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
137
|
+
type: 'list', name: 'uploads', message: 'User-provided file uploads',
|
|
138
|
+
choices: [{ name: 'None', value: 'none' }, { name: 'Private object storage', value: 'object-storage' }],
|
|
139
|
+
default: 'none', when: () => !args.uploads,
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
type: 'list', name: 'backgroundJobs', message: 'Background work',
|
|
143
|
+
choices: [{ name: 'None', value: 'none' }, { name: 'Durable queue', value: 'queue' }],
|
|
144
|
+
default: 'none', when: (answers) => catalog.byId[answers.frontend]?.platform !== 'mobile' && !args.backgroundJobs,
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
type: 'list', name: 'offline', message: 'Offline behavior',
|
|
148
|
+
choices: [{ name: 'None', value: 'none' }, { name: 'Local cache', value: 'cache' }, { name: 'Synchronization', value: 'sync' }],
|
|
149
|
+
default: 'none', when: (answers) => catalog.byId[answers.frontend]?.platform === 'mobile' && !args.offline,
|
|
149
150
|
},
|
|
150
151
|
{
|
|
151
152
|
type: 'confirm', name: 'docker', message: 'Add optional Docker development files', default: false,
|
package/src/cli/system-check.js
CHANGED
|
@@ -30,6 +30,8 @@ export function detectSystemVersions({ run = spawnSync, nodeVersion = process.ve
|
|
|
30
30
|
npm: probe(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['--version'], run),
|
|
31
31
|
php: probe('php', ['--version'], run),
|
|
32
32
|
composer: probe(process.platform === 'win32' ? 'composer.bat' : 'composer', ['--version'], run),
|
|
33
|
+
python: probe('python', ['--version'], run),
|
|
34
|
+
uv: probe('uv', ['--version'], run),
|
|
33
35
|
}
|
|
34
36
|
}
|
|
35
37
|
|
|
@@ -58,6 +60,14 @@ export function installationIssues(stack, profile, versions) {
|
|
|
58
60
|
issues.push({ tool: 'Composer', found: versions.composer, required: profile.runtimes.composer })
|
|
59
61
|
}
|
|
60
62
|
}
|
|
63
|
+
if (stack.backendKey === 'fastapi') {
|
|
64
|
+
if (!versionAtLeast(versions.python, profile.runtimes.python)) {
|
|
65
|
+
issues.push({ tool: 'Python', found: versions.python, required: profile.runtimes.python })
|
|
66
|
+
}
|
|
67
|
+
if (!versionAtLeast(versions.uv, profile.runtimes.uv)) {
|
|
68
|
+
issues.push({ tool: 'uv', found: versions.uv, required: profile.runtimes.uv })
|
|
69
|
+
}
|
|
70
|
+
}
|
|
61
71
|
return issues
|
|
62
72
|
}
|
|
63
73
|
|
|
@@ -91,6 +101,12 @@ export function runtimeSetupInstructions(profile, issues = []) {
|
|
|
91
101
|
'Verify: php --version && composer --version',
|
|
92
102
|
)
|
|
93
103
|
}
|
|
104
|
+
if (tools.has('Python') || tools.has('uv')) {
|
|
105
|
+
instructions.push(
|
|
106
|
+
`Install Python ${profile.runtimes.python} and uv ${profile.runtimes.uv} (https://docs.astral.sh/uv/), or use the generated Docker workflow.`,
|
|
107
|
+
'Verify: python --version && uv --version',
|
|
108
|
+
)
|
|
109
|
+
}
|
|
94
110
|
return instructions
|
|
95
111
|
}
|
|
96
112
|
|
|
@@ -116,19 +132,23 @@ export function collectDiagnostics(profile) {
|
|
|
116
132
|
const dockerVersion = probeDoctor('docker')
|
|
117
133
|
const composeVersion = dockerVersion ? probeDoctor('docker', ['compose', 'version']) : null
|
|
118
134
|
const javaVersion = probeDoctor('java', ['-version'])
|
|
135
|
+
const pythonVersion = probeDoctor('python')
|
|
136
|
+
const uvVersion = probeDoctor('uv')
|
|
119
137
|
return [
|
|
120
138
|
{ name: 'Node.js', found: nodeVersion, expected: profile.runtimes.node, required: false },
|
|
121
139
|
{ name: 'npm', found: npmVersion, required: false },
|
|
122
140
|
{ name: 'Docker', found: dockerVersion, required: false },
|
|
123
141
|
{ name: 'Docker Compose', found: composeVersion, required: false },
|
|
124
142
|
{ name: 'Java', found: javaVersion, expected: profile.runtimes.java, required: false },
|
|
143
|
+
{ name: 'Python', found: pythonVersion, expected: profile.runtimes.python, required: false },
|
|
144
|
+
{ name: 'uv', found: uvVersion, expected: profile.runtimes.uv, required: false },
|
|
125
145
|
]
|
|
126
146
|
}
|
|
127
147
|
|
|
128
|
-
export function printDoctor(profile, output = console.log) {
|
|
148
|
+
export function printDoctor(profile, output = console.log, diagnostics = collectDiagnostics(profile)) {
|
|
129
149
|
output(`create-win-project doctor (profile ${profile.id})`)
|
|
130
150
|
output('Use either the Node/npm lane or the Docker/Compose lane; Java is only needed for host-run Spring projects.')
|
|
131
|
-
for (const item of
|
|
151
|
+
for (const item of diagnostics) {
|
|
132
152
|
const expectation = item.expected ? ` (tested: ${item.expected})` : ''
|
|
133
153
|
output(`${item.found ? '✓' : '○'} ${item.name}: ${item.found || 'not found'}${expectation}`)
|
|
134
154
|
}
|
|
@@ -190,7 +190,8 @@ export function resolveStack(answers, catalog) {
|
|
|
190
190
|
if (fe.id === 'laravel-ui') authentication = 'laravel-session'
|
|
191
191
|
else authentication = authAudience === 'website' && fe.platform === 'web' ? 'sanctum-spa' : 'laravel-oidc'
|
|
192
192
|
}
|
|
193
|
-
else
|
|
193
|
+
else if (be.id === 'fastapi') authentication = 'oidc'
|
|
194
|
+
else throw new Error(`Authentication generation requires Supabase, Spring Boot, Laravel, or FastAPI for ${fe.label}`)
|
|
194
195
|
}
|
|
195
196
|
|
|
196
197
|
// ── Collect selected manifests ───────────────────────────────────────────
|
|
@@ -277,7 +278,7 @@ export function resolveStack(answers, catalog) {
|
|
|
277
278
|
// Apply env prefix: clientEnv vars get fe.envPrefix, others stay as-is
|
|
278
279
|
const envPrefix = fe.envPrefix || ''
|
|
279
280
|
if (authentication === 'session') rawEnv.push('SPRING_SECURITY_USER_NAME', 'SPRING_SECURITY_USER_PASSWORD')
|
|
280
|
-
if (authentication === 'oidc') rawEnv.push('OIDC_ISSUER_URI', 'OIDC_AUDIENCE')
|
|
281
|
+
if (authentication === 'oidc' && be.id === 'springboot') rawEnv.push('OIDC_ISSUER_URI', 'OIDC_AUDIENCE')
|
|
281
282
|
if (authentication === 'laravel-session') rawEnv.push('SESSION_DOMAIN')
|
|
282
283
|
if (authentication === 'sanctum-spa') rawEnv.push('SESSION_DOMAIN', 'SANCTUM_STATEFUL_DOMAINS', 'CORS_ALLOWED_ORIGINS')
|
|
283
284
|
if (authentication === 'laravel-oidc') rawEnv.push('AUTH0_DOMAIN', 'AUTH0_AUDIENCE')
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Static helpers only — no stack identity checks. Stack-specific files are
|
|
4
4
|
// now template-driven via templates/ and readTemplate().
|
|
5
5
|
|
|
6
|
-
export function contextMd(vars, expectedConcerns) {
|
|
6
|
+
export function contextMd(vars, expectedConcerns, capabilities = {}) {
|
|
7
7
|
const concernsBlock = (expectedConcerns && expectedConcerns.length)
|
|
8
8
|
? expectedConcerns.map((c) => `- ${c}`).join('\n')
|
|
9
9
|
: '- (none selected — all optional concerns remain available)'
|
|
@@ -23,6 +23,15 @@ export function contextMd(vars, expectedConcerns) {
|
|
|
23
23
|
## Key Decisions
|
|
24
24
|
<!-- Architecture and tech decisions made during the project -->
|
|
25
25
|
|
|
26
|
+
- Production baseline: generated default; deviations require approval.
|
|
27
|
+
- Uploads: ${capabilities.uploads || 'none'}
|
|
28
|
+
- Background jobs: ${capabilities.backgroundJobs || 'none'}
|
|
29
|
+
- Offline: ${capabilities.offline || 'none'}
|
|
30
|
+
|
|
31
|
+
## Approved Deviations
|
|
32
|
+
<!-- Record date, approver, rationale, affected files, and recovery path. -->
|
|
33
|
+
- (none)
|
|
34
|
+
|
|
26
35
|
## Out of Scope
|
|
27
36
|
<!-- What this project explicitly does NOT do -->
|
|
28
37
|
|
|
@@ -37,7 +37,7 @@ export async function resolvePlaybook(libraryDir, file) {
|
|
|
37
37
|
const rest = restParts.join('/')
|
|
38
38
|
const name = path.basename(file, '.md')
|
|
39
39
|
const candidates = {
|
|
40
|
-
universal: [path.join(libraryDir, 'universal', name, path.basename(file))],
|
|
40
|
+
universal: [path.join(libraryDir, 'universal', rest), path.join(libraryDir, 'universal', name, path.basename(file))],
|
|
41
41
|
platform: [path.join(libraryDir, 'platforms', rest), path.join(libraryDir, 'platforms', name, path.basename(file))],
|
|
42
42
|
stack: [path.join(libraryDir, 'stacks', rest)],
|
|
43
43
|
capabilities: [
|
|
@@ -46,6 +46,7 @@ export async function resolvePlaybook(libraryDir, file) {
|
|
|
46
46
|
],
|
|
47
47
|
concerns: [path.join(libraryDir, 'optional-features', 'concerns', rest)],
|
|
48
48
|
styling: [
|
|
49
|
+
path.join(libraryDir, 'optional-features', 'styling', rest),
|
|
49
50
|
path.join(libraryDir, 'optional-features', 'styling', name, path.basename(file)),
|
|
50
51
|
path.join(libraryDir, 'optional-features', 'styling', name.replace(/-extensions$/, ''), path.basename(file)),
|
|
51
52
|
],
|
|
@@ -29,13 +29,13 @@ const VALID_COMBINATIONS = Object.freeze({
|
|
|
29
29
|
['laravel-ui', 'laravel'],
|
|
30
30
|
],
|
|
31
31
|
separate: [
|
|
32
|
-
['nextjs', 'springboot'], ['nextjs', 'laravel'],
|
|
33
|
-
['react', 'springboot'], ['react', 'laravel'], ['react', 'supabase'],
|
|
32
|
+
['nextjs', 'springboot'], ['nextjs', 'laravel'], ['nextjs', 'fastapi'],
|
|
33
|
+
['react', 'springboot'], ['react', 'laravel'], ['react', 'supabase'], ['react', 'fastapi'],
|
|
34
34
|
],
|
|
35
|
-
api: [['no-frontend', 'springboot'], ['no-frontend', 'laravel']],
|
|
35
|
+
api: [['no-frontend', 'springboot'], ['no-frontend', 'laravel'], ['no-frontend', 'fastapi']],
|
|
36
36
|
mobile: [
|
|
37
37
|
['react-native', 'none'], ['react-native', 'supabase'],
|
|
38
|
-
['react-native', 'springboot'], ['react-native', 'laravel'],
|
|
38
|
+
['react-native', 'springboot'], ['react-native', 'laravel'], ['react-native', 'fastapi'],
|
|
39
39
|
],
|
|
40
40
|
frontend: [['react', 'none']],
|
|
41
41
|
})
|
|
@@ -54,6 +54,8 @@ export function buildVars(answers, stack) {
|
|
|
54
54
|
SPRING_MODULITH_VERSION: profile.springModulith,
|
|
55
55
|
PHP_VERSION: profile.runtimes.php,
|
|
56
56
|
COMPOSER_VERSION: profile.runtimes.composer,
|
|
57
|
+
PYTHON_VERSION: profile.runtimes.python,
|
|
58
|
+
UV_VERSION: profile.runtimes.uv,
|
|
57
59
|
LARAVEL_VERSION: profile.composerPackages['laravel/framework'],
|
|
58
60
|
LARAVEL_DIR: ['laravel-ui', 'no-frontend'].includes(stack.frontendKey) ? '.' : 'backend',
|
|
59
61
|
TESTCONTAINERS_VERSION: profile.packages["testcontainers-junit-jupiter"] || profile.packages["testcontainers-postgresql"] || "1.19.8",
|
|
@@ -65,6 +67,7 @@ export function buildVars(answers, stack) {
|
|
|
65
67
|
NGINX_IMAGE: `${profile.images.nginx.repository}:${profile.images.nginx.tag}`,
|
|
66
68
|
PHP_IMAGE: `${profile.images.php.repository}:${profile.images.php.tag}`,
|
|
67
69
|
COMPOSER_IMAGE: `${profile.images.composer.repository}:${profile.images.composer.tag}`,
|
|
70
|
+
PYTHON_IMAGE: `${profile.images.python.repository}:${profile.images.python.tag}`,
|
|
68
71
|
COMPATIBILITY_PROFILE: profile.id,
|
|
69
72
|
}
|
|
70
73
|
}
|
|
@@ -20,6 +20,7 @@ export function validateCompatibility(catalog) {
|
|
|
20
20
|
const statuses = new Map()
|
|
21
21
|
let packageNames
|
|
22
22
|
let composerPackageNames
|
|
23
|
+
let pythonPackageNames
|
|
23
24
|
for (const [id, profile] of Object.entries(catalog.profiles)) {
|
|
24
25
|
if (!/^\d{4}\.\d{2}$/.test(id)) throw new Error(`Invalid compatibility profile id: ${id}`)
|
|
25
26
|
if (!['current', 'previous'].includes(profile.status)) throw new Error(`Invalid status for profile ${id}`)
|
|
@@ -30,10 +31,10 @@ export function validateCompatibility(catalog) {
|
|
|
30
31
|
statuses.set(profile.status, id)
|
|
31
32
|
if (!EXACT_VERSION.test(profile.springBoot || '')) throw new Error(`Profile ${id} needs an exact Spring Boot version`)
|
|
32
33
|
if (!EXACT_VERSION.test(profile.springModulith || '')) throw new Error(`Profile ${id} needs an exact Spring Modulith version`)
|
|
33
|
-
for (const key of ['node', 'java', 'maven', 'postgres', 'php', 'composer']) {
|
|
34
|
+
for (const key of ['node', 'java', 'maven', 'postgres', 'php', 'composer', 'python', 'uv']) {
|
|
34
35
|
if (!profile.runtimes?.[key]) throw new Error(`Profile ${id} is missing runtime ${key}`)
|
|
35
36
|
}
|
|
36
|
-
for (const key of ['node', 'maven', 'java', 'postgres', 'nginx', 'php', 'composer']) {
|
|
37
|
+
for (const key of ['node', 'maven', 'java', 'postgres', 'nginx', 'php', 'composer', 'python']) {
|
|
37
38
|
const image = profile.images?.[key]
|
|
38
39
|
if (!image?.repository || !image?.tag || image.tag === 'latest') {
|
|
39
40
|
throw new Error(`Profile ${id} has an invalid ${key} image`)
|
|
@@ -57,6 +58,9 @@ export function validateCompatibility(catalog) {
|
|
|
57
58
|
if (profile.images.composer.repository !== 'composer' || profile.images.composer.tag !== profile.runtimes.composer) {
|
|
58
59
|
throw new Error(`Profile ${id} Composer image must match runtime ${profile.runtimes.composer}`)
|
|
59
60
|
}
|
|
61
|
+
if (profile.images.python.repository !== 'python' || !profile.images.python.tag.startsWith(`${profile.runtimes.python}-`)) {
|
|
62
|
+
throw new Error(`Profile ${id} Python image must match runtime ${profile.runtimes.python}`)
|
|
63
|
+
}
|
|
60
64
|
const names = Object.keys(profile.packages || {}).sort()
|
|
61
65
|
if (!names.length) throw new Error(`Profile ${id} has no package versions`)
|
|
62
66
|
for (const [name, value] of Object.entries(profile.packages)) {
|
|
@@ -80,6 +84,15 @@ export function validateCompatibility(catalog) {
|
|
|
80
84
|
throw new Error(`Profile ${id} does not own the same Composer package set as the current profile`)
|
|
81
85
|
}
|
|
82
86
|
composerPackageNames = composerNames
|
|
87
|
+
const pythonNames = Object.keys(profile.pythonPackages || {}).sort()
|
|
88
|
+
if (!pythonNames.length) throw new Error(`Profile ${id} has no Python package versions`)
|
|
89
|
+
for (const [name, version] of Object.entries(profile.pythonPackages)) {
|
|
90
|
+
if (!EXACT_VERSION.test(version || '')) throw new Error(`Profile ${id} Python package ${name} must use an exact version`)
|
|
91
|
+
}
|
|
92
|
+
if (pythonPackageNames && JSON.stringify(pythonNames) !== JSON.stringify(pythonPackageNames)) {
|
|
93
|
+
throw new Error(`Profile ${id} does not own the same Python package set as the current profile`)
|
|
94
|
+
}
|
|
95
|
+
pythonPackageNames = pythonNames
|
|
83
96
|
}
|
|
84
97
|
if (!statuses.has('current') || !statuses.has('previous')) {
|
|
85
98
|
throw new Error('Exactly one current and one previous compatibility profile are required')
|
|
@@ -114,3 +127,9 @@ export function composerPackageVersion(profile, name, owner = name) {
|
|
|
114
127
|
if (!version) throw new Error(`${owner} requires ${name}, missing from compatibility profile ${profile.id}`)
|
|
115
128
|
return version
|
|
116
129
|
}
|
|
130
|
+
|
|
131
|
+
export function pythonPackageVersion(profile, name, owner = name) {
|
|
132
|
+
const version = profile.pythonPackages?.[name]
|
|
133
|
+
if (!version) throw new Error(`${owner} requires ${name}, missing from compatibility profile ${profile.id}`)
|
|
134
|
+
return version
|
|
135
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import fs from 'fs-extra'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { loadCompatibility } from './tested-versions.js'
|
|
4
|
+
|
|
5
|
+
export async function createUpgradeReport(projectPath, compatibilityFile) {
|
|
6
|
+
const profilePath = path.join(path.resolve(projectPath), 'create-win-project.profile.json')
|
|
7
|
+
const existing = await fs.readJson(profilePath).catch(() => { throw new Error(`No create-win-project profile found at ${profilePath}`) })
|
|
8
|
+
const { profile: current } = await loadCompatibility(compatibilityFile)
|
|
9
|
+
const previousId = existing.compatibilityProfile?.id
|
|
10
|
+
const differences = []
|
|
11
|
+
if (existing.schemaVersion !== 2) differences.push({ area: 'migration', current: existing.schemaVersion, supported: 2 })
|
|
12
|
+
if (previousId !== current.id) differences.push({ area: 'compatibility-profile', current: previousId, supported: current.id })
|
|
13
|
+
for (const [runtime, version] of Object.entries(current.runtimes)) {
|
|
14
|
+
if (existing.runtimes?.[runtime] !== version) differences.push({ area: `runtime.${runtime}`, current: existing.runtimes?.[runtime], supported: version })
|
|
15
|
+
}
|
|
16
|
+
for (const contract of ['tests', 'continuousIntegration', 'securityRules', 'operationsDocumentation']) {
|
|
17
|
+
if (existing.productionBaseline?.[contract] !== true) differences.push({ area: `security-contract.${contract}`, current: existing.productionBaseline?.[contract], supported: true })
|
|
18
|
+
}
|
|
19
|
+
return { readOnly: true, project: path.resolve(projectPath), stack: existing.stack, profile: previousId, supportedProfile: current.id, differences }
|
|
20
|
+
}
|