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