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
|
@@ -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 = ['springboot', 'fastapi'].includes(stack.backendKey) ? [...publicNames, ...serverNames] : publicNames
|
|
6
|
+
return { '.env.example': renderEnvironment(names, answers) }
|
|
7
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { defineStackAdapter } from '../../rules.js'
|
|
2
|
+
import { ciContributions } from './ci.js'
|
|
3
|
+
import { dockerContributions } from './docker.js'
|
|
4
|
+
import { buildReactNativeFiles } from './create-files.js'
|
|
5
|
+
|
|
6
|
+
export const reactNativeAdapter = defineStackAdapter({
|
|
7
|
+
id: 'react-native',
|
|
8
|
+
kind: 'frontend',
|
|
9
|
+
label: 'React Native (Expo)',
|
|
10
|
+
compatibleWith: { backend: ['none', 'supabase', 'springboot', 'laravel', 'fastapi'] },
|
|
11
|
+
capabilities: {
|
|
12
|
+
applicationShapes: ['mobile'],
|
|
13
|
+
architectureProfiles: ['small', 'medium', 'large'],
|
|
14
|
+
authenticationModels: ['public', 'undecided', 'supabase', 'oidc', 'laravel-oidc'],
|
|
15
|
+
runtime: 'node',
|
|
16
|
+
},
|
|
17
|
+
contributes: {
|
|
18
|
+
files: ({ answers, stack, shared }) => Object.entries(buildReactNativeFiles(answers, stack, shared)),
|
|
19
|
+
environment: ({ backend }) => backend.id === 'none' ? [] : ['API_URL'],
|
|
20
|
+
install: () => [{ cwd: '.', command: 'npm', args: ['install'] }],
|
|
21
|
+
docker: dockerContributions,
|
|
22
|
+
ci: ciContributions,
|
|
23
|
+
verification: () => [
|
|
24
|
+
{ backend: 'none', architecture: 'small', authentication: 'public' },
|
|
25
|
+
{ backend: 'supabase', architecture: 'medium', authentication: 'supabase' },
|
|
26
|
+
{ backend: 'springboot', architecture: 'large', authentication: 'oidc' },
|
|
27
|
+
{ backend: 'laravel', architecture: 'medium', authentication: 'laravel-oidc' },
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { html, json } from '../../shared/javascript-package.js'
|
|
2
|
+
import { addTestingFiles } from '../../shared/testing-files.js'
|
|
3
|
+
import { packageFile } from './dependencies.js'
|
|
4
|
+
|
|
5
|
+
export function buildReactViteFiles(answers, stack, shared) {
|
|
6
|
+
const root = 'frontend'
|
|
7
|
+
const files = {}
|
|
8
|
+
files[`${root}/package.json`] = packageFile(answers, stack)
|
|
9
|
+
files[`${root}/.node-version`] = `${stack.profile.runtimes.node}\n`
|
|
10
|
+
files[`${root}/.npmrc`] = 'engine-strict=true\n'
|
|
11
|
+
files[`${root}/tsconfig.json`] = json({ compilerOptions: { target: 'ES2022', useDefineForClassFields: true, lib: ['ES2022', 'DOM', 'DOM.Iterable'], allowJs: false, skipLibCheck: true, esModuleInterop: true, allowSyntheticDefaultImports: true, strict: true, forceConsistentCasingInFileNames: true, module: 'ESNext', moduleResolution: 'Bundler', resolveJsonModule: true, isolatedModules: true, noEmit: true, jsx: 'react-jsx', paths: { '@/*': ['./src/*'] } }, include: ['src', 'vite.config.ts', 'vitest.config.ts'] })
|
|
12
|
+
files[`${root}/index.html`] = `<!doctype html>\n<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="description" content="${html(answers.projectDescription)}" /><title>${html(answers.projectName)}</title></head><body><div id="root"></div><script type="module" src="/src/main.tsx"></script></body></html>\n`
|
|
13
|
+
files[`${root}/vite.config.ts`] = `import { fileURLToPath, URL } from 'node:url'\nimport { defineConfig } from 'vite'\nimport react from '@vitejs/plugin-react'\n${stack.styleId === 'tailwind' ? "import tailwindcss from '@tailwindcss/vite'\n" : ''}\nexport default defineConfig({\n plugins: [react()${stack.styleId === 'tailwind' ? ', tailwindcss()' : ''}],\n resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } },\n})\n`
|
|
14
|
+
files[`${root}/eslint.config.js`] = `import js from '@eslint/js'\nimport tseslint from 'typescript-eslint'\n\nexport default tseslint.config(\n { ignores: ['dist', 'coverage', 'playwright-report'] },\n js.configs.recommended,\n ...tseslint.configs.recommended,\n { files: ['**/*.{ts,tsx}'], languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } } },\n)\n`
|
|
15
|
+
files[`${root}/src/vite-env.d.ts`] = "/// <reference types=\"vite/client\" />\n"
|
|
16
|
+
files[`${root}/src/main.tsx`] = `import { StrictMode } from 'react'\nimport { createRoot } from 'react-dom/client'\nimport { App } from './App'\nimport './styles.css'\n\ncreateRoot(document.getElementById('root')!).render(<StrictMode><App /></StrictMode>)\n`
|
|
17
|
+
files[`${root}/src/App.tsx`] = stack.architecture === 'small'
|
|
18
|
+
? `export function App() {\n return <main><p>create-win-project</p><h1>Your starter is running</h1><p>{${JSON.stringify(answers.projectDescription)}}</p><p>Read <code>AGENTS.md</code> before your first agent-assisted change.</p></main>\n}\n`
|
|
19
|
+
: `${stack.architecture === 'large'
|
|
20
|
+
? "import { getStarterStatus, StarterStatus } from '@/features/status'"
|
|
21
|
+
: "import { StarterStatus } from '@/features/status/components/StarterStatus'\nimport { getStarterStatus } from '@/features/status/services/getStarterStatus'"}\n\nexport function App() {\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`
|
|
22
|
+
files[`${root}/src/styles.css`] = `${stack.styleId === 'tailwind' ? '@import "tailwindcss";\n' : ''}:root { color-scheme: light dark; font-family: system-ui, sans-serif; }\n* { box-sizing: border-box; }\nbody { margin: 0; }\nmain { max-width: 48rem; margin: 0 auto; padding: 4rem 1.5rem; }\n`
|
|
23
|
+
if ((answers.testing || 'basic') !== 'none') files[`${root}/src/App.test.tsx`] = `import { expect, test } from 'vitest'\nimport { render, screen } from '@testing-library/react'\nimport { App } from './App'\n\ntest('renders the starter heading', () => {\n render(<App />)\n expect(screen.getByRole('heading', { name: 'Your starter is running' })).toBeInTheDocument()\n})\n`
|
|
24
|
+
addTestingFiles(files, root, stack, answers.testing || 'basic')
|
|
25
|
+
Object.assign(files, shared.statusFeatureFiles(root, stack))
|
|
26
|
+
return files
|
|
27
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { partitionEnvironment, renderEnvironment } from '../../shared/environment.js'
|
|
2
|
+
|
|
3
|
+
export function buildEnvironmentFiles(answers, stack) {
|
|
4
|
+
const { publicNames, serverNames } = partitionEnvironment(stack)
|
|
5
|
+
const files = { 'frontend/.env.example': renderEnvironment(publicNames, answers) }
|
|
6
|
+
if (['springboot', 'fastapi'].includes(stack.backendKey)) files['.env.example'] = renderEnvironment(serverNames, answers)
|
|
7
|
+
return files
|
|
8
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { defineStackAdapter } from '../../rules.js'
|
|
2
|
+
import { ciContributions } from './ci.js'
|
|
3
|
+
import { dockerContributions } from './docker.js'
|
|
4
|
+
import { buildReactViteFiles } from './create-files.js'
|
|
5
|
+
|
|
6
|
+
const verificationCases = Object.freeze([
|
|
7
|
+
Object.freeze({ backend: 'none', styling: 'css-modules', architecture: 'small', authentication: 'public' }),
|
|
8
|
+
Object.freeze({ backend: 'supabase', styling: 'tailwind', architecture: 'medium', authentication: 'supabase' }),
|
|
9
|
+
Object.freeze({ backend: 'springboot', styling: 'css-modules', architecture: 'large', authentication: 'session' }),
|
|
10
|
+
Object.freeze({ backend: 'laravel', styling: 'tailwind', architecture: 'medium', authentication: 'sanctum-spa' }),
|
|
11
|
+
])
|
|
12
|
+
|
|
13
|
+
export const reactViteAdapter = defineStackAdapter({
|
|
14
|
+
id: 'react',
|
|
15
|
+
kind: 'frontend',
|
|
16
|
+
label: 'React + Vite',
|
|
17
|
+
compatibleWith: { backend: ['none', 'supabase', 'springboot', 'laravel', 'fastapi'] },
|
|
18
|
+
capabilities: {
|
|
19
|
+
applicationShapes: ['separate', 'frontend'],
|
|
20
|
+
architectureProfiles: ['small', 'medium', 'large'],
|
|
21
|
+
authenticationModels: ['public', 'undecided', 'supabase', 'session', 'oidc', 'sanctum-spa', 'laravel-oidc'],
|
|
22
|
+
runtime: 'node',
|
|
23
|
+
},
|
|
24
|
+
contributes: {
|
|
25
|
+
files: ({ answers, stack, shared }) => Object.entries(buildReactViteFiles(answers, stack, shared)),
|
|
26
|
+
environment: ({ backend }) => backend.id === 'none' ? [] : ['API_URL'],
|
|
27
|
+
install: () => [{ cwd: 'frontend', command: 'npm', args: ['install'] }],
|
|
28
|
+
docker: dockerContributions,
|
|
29
|
+
ci: ciContributions,
|
|
30
|
+
verification: () => verificationCases,
|
|
31
|
+
},
|
|
32
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const json = (value) => `${JSON.stringify(value, null, 2)}\n`
|
|
2
|
+
|
|
3
|
+
export function capabilityPackFiles(answers, stack) {
|
|
4
|
+
const files = {}
|
|
5
|
+
if (answers.uploads === 'object-storage') {
|
|
6
|
+
files['config/capabilities/uploads.json'] = json({
|
|
7
|
+
visibility: 'private', authorizeEveryOperation: true, maximumBytes: 10_485_760,
|
|
8
|
+
acceptedTypes: [], generatedObjectNames: true, verifySignature: true,
|
|
9
|
+
quarantineBeforeUse: true, malwareScanRequired: true, abandonedUploadCleanupHours: 24,
|
|
10
|
+
})
|
|
11
|
+
files['docs/capabilities/uploads.md'] = '# Private uploads\n\nAuthorize initiation, completion, download, and deletion. Validate declared type and file signature, generate object names, enforce size limits, quarantine until scanning succeeds, and remove abandoned or rejected objects. Tests must cover unauthorized access, oversize and signature rejection, quarantine, scan failure, and cleanup.\n'
|
|
12
|
+
}
|
|
13
|
+
if (answers.backgroundJobs === 'queue') {
|
|
14
|
+
files['config/capabilities/queue.json'] = json({
|
|
15
|
+
adapter: stack.backendKey === 'laravel' ? 'laravel-queue' : 'spring-task',
|
|
16
|
+
attempts: 5, timeoutSeconds: 30, exponentialBackoff: true,
|
|
17
|
+
idempotencyRequired: true, failedJobStore: true, correlationIds: true,
|
|
18
|
+
})
|
|
19
|
+
files['docs/capabilities/queue.md'] = '# Durable background jobs\n\nJobs require stable idempotency keys, bounded exponential retry with jitter, explicit timeouts, a failed-job store, correlation IDs, metrics, and an operator replay procedure. Tests cover duplicate delivery, timeout, retry exhaustion, and safe replay.\n'
|
|
20
|
+
}
|
|
21
|
+
if (answers.offline && answers.offline !== 'none') {
|
|
22
|
+
files['config/capabilities/offline.json'] = json({
|
|
23
|
+
mode: answers.offline, owner: 'mobile-client', containsSensitiveData: false,
|
|
24
|
+
keyNamespace: `${answers.projectName}:v1`, ttlSeconds: 3600,
|
|
25
|
+
invalidation: answers.offline === 'sync' ? 'server-version-and-user-signout' : 'ttl-and-user-signout',
|
|
26
|
+
conflictPolicy: answers.offline === 'sync' ? 'server-authoritative-with-explicit-user-resolution' : 'not-applicable',
|
|
27
|
+
})
|
|
28
|
+
files['docs/capabilities/offline.md'] = `# Offline ${answers.offline}\n\nThe mobile client owns this store. Namespace keys by user and schema version, purge on sign-out, exclude secrets, enforce TTL, expose offline/stale/synchronizing/error states, and test reconnect behavior. ${answers.offline === 'sync' ? 'Synchronize through idempotent server operations and surface conflicts; never silently overwrite divergent user data.' : 'This is a measured read cache, not a source of truth.'}\n`
|
|
29
|
+
}
|
|
30
|
+
return files
|
|
31
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CONTRIBUTION_HOOKS, PROMPT_SLOTS } from '../
|
|
1
|
+
import { CONTRIBUTION_HOOKS, PROMPT_SLOTS } from '../rules.js'
|
|
2
2
|
|
|
3
3
|
export function collectContributions(adapters, hook, context) {
|
|
4
4
|
if (!CONTRIBUTION_HOOKS.includes(hook)) throw new Error(`Unknown contribution hook: ${hook}`)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const environmentHeader = '# Copy to .env or .env.local. Never commit real credentials.\n\n'
|
|
2
|
+
|
|
3
|
+
export function environmentLine(name, answers) {
|
|
4
|
+
const isFastApi = answers.backend === 'fastapi'
|
|
5
|
+
const apiPort = isFastApi ? '8000' : '8080'
|
|
6
|
+
const defaults = {
|
|
7
|
+
NEXT_PUBLIC_API_URL: `http://localhost:${apiPort}`,
|
|
8
|
+
VITE_API_URL: `http://localhost:${apiPort}`,
|
|
9
|
+
EXPO_PUBLIC_API_URL: `http://localhost:${apiPort}`,
|
|
10
|
+
DATABASE_URL: isFastApi
|
|
11
|
+
? `postgresql+asyncpg://localhost:5432/${answers.projectName.replaceAll('-', '_')}`
|
|
12
|
+
: `jdbc:postgresql://localhost:5432/${answers.projectName.replaceAll('-', '_')}`,
|
|
13
|
+
POSTGRES_USER: 'postgres',
|
|
14
|
+
POSTGRES_PASSWORD: 'change-me',
|
|
15
|
+
POSTGRES_DB: answers.projectName.replaceAll('-', '_'),
|
|
16
|
+
SPRING_PROFILES_ACTIVE: 'dev',
|
|
17
|
+
SPRING_SECURITY_USER_NAME: 'developer',
|
|
18
|
+
SPRING_SECURITY_USER_PASSWORD: 'change-me-before-production',
|
|
19
|
+
OIDC_ISSUER_URI: 'http://localhost:9090/realms/app',
|
|
20
|
+
OIDC_ISSUER: '',
|
|
21
|
+
OIDC_AUDIENCE: 'api',
|
|
22
|
+
OIDC_ALGORITHMS: 'RS256',
|
|
23
|
+
OIDC_JWKS_URL: '',
|
|
24
|
+
CORS_ALLOWED_ORIGINS: 'http://localhost:3000',
|
|
25
|
+
}
|
|
26
|
+
return `${name}=${defaults[name] || ''}`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function renderEnvironment(names, answers) {
|
|
30
|
+
return `${environmentHeader}${names.map((name) => environmentLine(name, answers)).join('\n')}\n`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function partitionEnvironment(stack) {
|
|
34
|
+
return {
|
|
35
|
+
publicNames: stack.env.filter((name) => name.startsWith(stack.envPrefix)),
|
|
36
|
+
serverNames: stack.env.filter((name) => !name.startsWith(stack.envPrefix)),
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
export function packageVersion(profile, name, capability) {
|
|
2
|
+
const configured = profile.packages[name]
|
|
3
|
+
const resolved = typeof configured === 'string'
|
|
4
|
+
? configured
|
|
5
|
+
: configured?.overrides?.[capability] || configured?.default
|
|
6
|
+
if (!resolved) throw new Error(`${capability} requires ${name} in compatibility profile ${profile.id}`)
|
|
7
|
+
return resolved
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function composerPackageVersion(profile, name, owner = name) {
|
|
11
|
+
const resolved = profile.composerPackages?.[name]
|
|
12
|
+
if (!resolved) throw new Error(`${owner} requires ${name} in compatibility profile ${profile.id}`)
|
|
13
|
+
return resolved
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function pythonPackageVersion(profile, name, owner = name) {
|
|
17
|
+
const resolved = profile.pythonPackages?.[name]
|
|
18
|
+
if (!resolved) throw new Error(`${owner} requires ${name} in compatibility profile ${profile.id}`)
|
|
19
|
+
return resolved
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function json(value) {
|
|
23
|
+
return `${JSON.stringify(value, null, 2)}\n`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function html(value) {
|
|
27
|
+
return String(value).replaceAll('&', '&').replaceAll('"', '"').replaceAll('<', '<').replaceAll('>', '>')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function buildJavaScriptPackage(answers, stack, owner) {
|
|
31
|
+
const level = answers.testing || 'basic'
|
|
32
|
+
const scripts = { ...stack.scripts, start: owner === 'nextjs' ? 'next start' : undefined }
|
|
33
|
+
const devDependencies = { ...stack.devDeps }
|
|
34
|
+
|
|
35
|
+
if (level === 'none') delete scripts.test
|
|
36
|
+
if (level !== 'none') {
|
|
37
|
+
if (owner === 'react-native') {
|
|
38
|
+
Object.assign(devDependencies, {
|
|
39
|
+
jest: packageVersion(stack.profile, 'jest', owner),
|
|
40
|
+
'@types/jest': packageVersion(stack.profile, '@types/jest', owner),
|
|
41
|
+
'jest-expo': packageVersion(stack.profile, 'jest-expo', owner),
|
|
42
|
+
'@testing-library/react-native': packageVersion(stack.profile, '@testing-library/react-native', owner),
|
|
43
|
+
})
|
|
44
|
+
} else {
|
|
45
|
+
Object.assign(devDependencies, {
|
|
46
|
+
vitest: packageVersion(stack.profile, 'vitest', owner),
|
|
47
|
+
jsdom: packageVersion(stack.profile, 'jsdom', owner),
|
|
48
|
+
'@testing-library/react': packageVersion(stack.profile, '@testing-library/react', owner),
|
|
49
|
+
'@testing-library/jest-dom': packageVersion(stack.profile, '@testing-library/jest-dom', owner),
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (level === 'full' && owner !== 'react-native') {
|
|
54
|
+
scripts['test:e2e'] = 'playwright test'
|
|
55
|
+
devDependencies['@playwright/test'] = packageVersion(stack.profile, '@playwright/test', owner)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
scripts.typecheck = 'tsc --noEmit'
|
|
59
|
+
scripts.format = 'prettier --write .'
|
|
60
|
+
scripts['format:check'] = 'prettier --check .'
|
|
61
|
+
if (owner !== 'react-native') scripts.lint = 'eslint .'
|
|
62
|
+
if (stack.architecture === 'large') scripts['check:boundaries'] = 'node scripts/check-boundaries.mjs'
|
|
63
|
+
if (stack.backendKey === 'supabase') {
|
|
64
|
+
const workdir = owner === 'react' ? ' --workdir ..' : ''
|
|
65
|
+
scripts['supabase:start'] = `supabase${workdir} start`
|
|
66
|
+
scripts['supabase:stop'] = `supabase${workdir} stop`
|
|
67
|
+
scripts['supabase:status'] = `supabase${workdir} status`
|
|
68
|
+
scripts['supabase:reset'] = `supabase${workdir} db reset`
|
|
69
|
+
scripts['supabase:test'] = `supabase${workdir} test db`
|
|
70
|
+
const typePath = owner === 'react-native' ? 'types/database.types.ts' : 'src/types/database.types.ts'
|
|
71
|
+
scripts['supabase:types'] = `supabase${workdir} gen types typescript --local > ${typePath}`
|
|
72
|
+
}
|
|
73
|
+
if (stack.backendKey === 'postgres') {
|
|
74
|
+
scripts['prisma:generate'] = 'prisma generate'
|
|
75
|
+
scripts['db:migrate'] = 'prisma migrate dev'
|
|
76
|
+
scripts['db:deploy'] = 'prisma migrate deploy'
|
|
77
|
+
scripts['db:reset'] = 'prisma migrate reset --force'
|
|
78
|
+
scripts['db:studio'] = 'prisma studio'
|
|
79
|
+
scripts.dev = 'prisma generate && next dev'
|
|
80
|
+
scripts.build = 'prisma generate && next build'
|
|
81
|
+
scripts.typecheck = 'prisma generate && tsc --noEmit'
|
|
82
|
+
}
|
|
83
|
+
const boundary = stack.architecture === 'large' ? ' && npm run check:boundaries' : ''
|
|
84
|
+
scripts.check = owner === 'react-native'
|
|
85
|
+
? `npm run format:check && npm run typecheck${level === 'none' ? '' : ' && npm test -- --runInBand'}${boundary}`
|
|
86
|
+
: `npm run format:check && npm run lint && npm run typecheck${level === 'none' ? '' : ' && npm test'}${boundary}`
|
|
87
|
+
Object.keys(scripts).forEach((key) => scripts[key] === undefined && delete scripts[key])
|
|
88
|
+
|
|
89
|
+
if (stack.styleId === 'tailwind') {
|
|
90
|
+
devDependencies.tailwindcss = packageVersion(stack.profile, 'tailwindcss', owner)
|
|
91
|
+
if (owner === 'nextjs') devDependencies['@tailwindcss/postcss'] = packageVersion(stack.profile, '@tailwindcss/postcss', owner)
|
|
92
|
+
if (owner === 'react') devDependencies['@tailwindcss/vite'] = packageVersion(stack.profile, '@tailwindcss/vite', owner)
|
|
93
|
+
}
|
|
94
|
+
const dependencies = { ...stack.deps }
|
|
95
|
+
if (owner !== 'nextjs') delete dependencies['@supabase/ssr']
|
|
96
|
+
if (owner === 'react-native' && stack.authentication === 'supabase') {
|
|
97
|
+
dependencies['expo-secure-store'] = packageVersion(stack.profile, 'expo-secure-store', owner)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const packageJson = {
|
|
101
|
+
name: answers.projectName,
|
|
102
|
+
version: '0.1.0',
|
|
103
|
+
private: true,
|
|
104
|
+
type: 'module',
|
|
105
|
+
packageManager: `npm@${stack.profile.runtimes.npmMinimum}`,
|
|
106
|
+
engines: {
|
|
107
|
+
node: `>=${stack.profile.runtimes.nodeMinimum}`,
|
|
108
|
+
npm: `>=${stack.profile.runtimes.npmMinimum}`,
|
|
109
|
+
},
|
|
110
|
+
main: owner === 'react-native' ? 'expo-router/entry' : undefined,
|
|
111
|
+
scripts,
|
|
112
|
+
dependencies,
|
|
113
|
+
devDependencies,
|
|
114
|
+
}
|
|
115
|
+
if (!packageJson.main) delete packageJson.main
|
|
116
|
+
return json(packageJson)
|
|
117
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
|
|
3
|
+
export function addTestingFiles(files, root, stack, level) {
|
|
4
|
+
if (level === 'none') return
|
|
5
|
+
if (stack.isMobile) {
|
|
6
|
+
files[path.join(root, 'jest.config.js')] = "export default { preset: 'jest-expo' }\n"
|
|
7
|
+
files[path.join(root, 'app/index.test.tsx')] = `import { render } from '@testing-library/react-native'\nimport HomeScreen from './index'\n\ntest('renders the starter heading', async () => {\n const view = await render(<HomeScreen />)\n expect(view.getByText('Your starter is running')).toBeTruthy()\n})\n`
|
|
8
|
+
return
|
|
9
|
+
}
|
|
10
|
+
files[path.join(root, 'vitest.config.ts')] = `import { fileURLToPath, URL } from 'node:url'\nimport { defineConfig } from 'vitest/config'\n\nexport default defineConfig({\n resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } },\n test: { environment: 'jsdom', setupFiles: './src/test/setup.ts', exclude: ['e2e/**', 'node_modules/**'] },\n})\n`
|
|
11
|
+
files[path.join(root, 'src/test/setup.ts')] = "import '@testing-library/jest-dom/vitest'\n"
|
|
12
|
+
if (level === 'full') {
|
|
13
|
+
files[path.join(root, 'playwright.config.ts')] = `import { defineConfig, devices } from '@playwright/test'\n\nexport default defineConfig({\n testDir: './e2e',\n use: { baseURL: 'http://127.0.0.1:${stack.frontendPort}' },\n webServer: { command: 'npm run dev -- ${stack.frontendKey === 'nextjs' ? '--hostname' : '--host'} 127.0.0.1 --port ${stack.frontendPort}', url: 'http://127.0.0.1:${stack.frontendPort}', reuseExistingServer: true, timeout: 120_000 },\n projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],\n})\n`
|
|
14
|
+
files[path.join(root, 'e2e/home.spec.ts')] = `import { expect, test } from '@playwright/test'\n\ntest('loads the starter', async ({ page }) => {\n await page.goto('/')\n await expect(page.getByRole('heading', { name: 'Your starter is running' })).toBeVisible()\n})\n`
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
name: CI — Backend
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [dev, main]
|
|
6
|
+
paths:
|
|
7
|
+
- backend/**
|
|
8
|
+
pull_request:
|
|
9
|
+
branches: [dev, main]
|
|
10
|
+
paths:
|
|
11
|
+
- backend/**
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
ci-backend:
|
|
18
|
+
name: Test, Build
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
defaults:
|
|
21
|
+
run:
|
|
22
|
+
working-directory: backend
|
|
23
|
+
services:
|
|
24
|
+
postgres:
|
|
25
|
+
image: {{POSTGRES_IMAGE}}
|
|
26
|
+
env:
|
|
27
|
+
POSTGRES_DB: app_test
|
|
28
|
+
POSTGRES_USER: postgres
|
|
29
|
+
POSTGRES_PASSWORD: postgres
|
|
30
|
+
ports:
|
|
31
|
+
- 5432:5432
|
|
32
|
+
options: >-
|
|
33
|
+
--health-cmd "pg_isready -U postgres -d app_test"
|
|
34
|
+
--health-interval 5s
|
|
35
|
+
--health-timeout 5s
|
|
36
|
+
--health-retries 10
|
|
37
|
+
env:
|
|
38
|
+
DATABASE_URL: postgresql+asyncpg://postgres:postgres@127.0.0.1:5432/app_test
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
|
41
|
+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
|
42
|
+
with:
|
|
43
|
+
python-version: "{{PYTHON_VERSION}}"
|
|
44
|
+
- uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v6
|
|
45
|
+
with:
|
|
46
|
+
version: "{{UV_VERSION}}"
|
|
47
|
+
- name: Install dependencies
|
|
48
|
+
run: uv sync --frozen
|
|
49
|
+
- name: Apply migrations
|
|
50
|
+
run: uv run alembic upgrade head
|
|
51
|
+
- name: Check migrations are current
|
|
52
|
+
run: uv run alembic check
|
|
53
|
+
- name: Run Ruff
|
|
54
|
+
run: uv run ruff check .
|
|
55
|
+
- name: Check formatting
|
|
56
|
+
run: uv run ruff format --check .
|
|
57
|
+
- name: Run mypy
|
|
58
|
+
run: uv run mypy .
|
|
59
|
+
- name: Run tests
|
|
60
|
+
run: uv run pytest
|
|
61
|
+
- name: Build image
|
|
62
|
+
run: docker build -f Dockerfile .
|
|
@@ -7,7 +7,7 @@ services:
|
|
|
7
7
|
context: {{FRONTEND_DIR}}
|
|
8
8
|
dockerfile: {{FRONTEND_DOCKERFILE}}
|
|
9
9
|
ports:
|
|
10
|
-
- "{{FRONTEND_PORT}}:{{FRONTEND_PORT}}"
|
|
10
|
+
- "${FRONTEND_HOST_PORT:-{{FRONTEND_PORT}}}:{{FRONTEND_PORT}}"
|
|
11
11
|
volumes:
|
|
12
12
|
- ./{{FRONTEND_DIR}}:/app
|
|
13
13
|
- /app/node_modules
|
|
@@ -24,7 +24,7 @@ services:
|
|
|
24
24
|
context: ./backend
|
|
25
25
|
dockerfile: Dockerfile.dev
|
|
26
26
|
ports:
|
|
27
|
-
- "8080:8080"
|
|
27
|
+
- "${BACKEND_HOST_PORT:-8080}:8080"
|
|
28
28
|
volumes:
|
|
29
29
|
- ./backend:/app
|
|
30
30
|
- maven-cache:/root/.m2
|
|
@@ -43,7 +43,7 @@ services:
|
|
|
43
43
|
container_name: {{PROJECT_NAME}}-db
|
|
44
44
|
image: {{POSTGRES_IMAGE}}
|
|
45
45
|
ports:
|
|
46
|
-
- "5432:5432"
|
|
46
|
+
- "${POSTGRES_HOST_PORT:-5432}:5432"
|
|
47
47
|
environment:
|
|
48
48
|
- POSTGRES_USER=${POSTGRES_USER}
|
|
49
49
|
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# docker-compose.prod.yml — {{PROJECT_NAME}} ({{STACK}}) — Prod
|
|
2
|
+
services:
|
|
3
|
+
|
|
4
|
+
backend:
|
|
5
|
+
container_name: {{PROJECT_NAME}}-backend
|
|
6
|
+
build:
|
|
7
|
+
context: ./backend
|
|
8
|
+
dockerfile: Dockerfile
|
|
9
|
+
ports:
|
|
10
|
+
- "${BACKEND_HOST_PORT:-8000}:8000"
|
|
11
|
+
environment:
|
|
12
|
+
- DATABASE_URL=${DATABASE_URL}
|
|
13
|
+
- OIDC_ISSUER=${OIDC_ISSUER}
|
|
14
|
+
- OIDC_AUDIENCE=${OIDC_AUDIENCE}
|
|
15
|
+
- OIDC_ALGORITHMS=${OIDC_ALGORITHMS}
|
|
16
|
+
- OIDC_JWKS_URL=${OIDC_JWKS_URL}
|
|
17
|
+
- CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS}
|
|
18
|
+
depends_on:
|
|
19
|
+
db:
|
|
20
|
+
condition: service_healthy
|
|
21
|
+
networks:
|
|
22
|
+
- app-network
|
|
23
|
+
healthcheck:
|
|
24
|
+
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8000/health')\""]
|
|
25
|
+
interval: 30s
|
|
26
|
+
timeout: 5s
|
|
27
|
+
retries: 3
|
|
28
|
+
|
|
29
|
+
db:
|
|
30
|
+
container_name: {{PROJECT_NAME}}-db
|
|
31
|
+
image: {{POSTGRES_IMAGE}}
|
|
32
|
+
environment:
|
|
33
|
+
- POSTGRES_USER=${POSTGRES_USER}
|
|
34
|
+
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
|
35
|
+
- POSTGRES_DB=${POSTGRES_DB}
|
|
36
|
+
volumes:
|
|
37
|
+
- postgres-data:/var/lib/postgresql/data
|
|
38
|
+
networks:
|
|
39
|
+
- app-network
|
|
40
|
+
healthcheck:
|
|
41
|
+
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
|
42
|
+
interval: 10s
|
|
43
|
+
timeout: 5s
|
|
44
|
+
retries: 5
|
|
45
|
+
|
|
46
|
+
volumes:
|
|
47
|
+
postgres-data:
|
|
48
|
+
name: {{PROJECT_NAME}}-postgres-data
|
|
49
|
+
|
|
50
|
+
networks:
|
|
51
|
+
app-network:
|
|
52
|
+
name: {{PROJECT_NAME}}-network
|
|
@@ -16,6 +16,11 @@ services:
|
|
|
16
16
|
networks:
|
|
17
17
|
- app-network
|
|
18
18
|
restart: always
|
|
19
|
+
read_only: true
|
|
20
|
+
tmpfs: [/tmp]
|
|
21
|
+
deploy:
|
|
22
|
+
resources:
|
|
23
|
+
limits: { cpus: "1.0", memory: 512M }
|
|
19
24
|
|
|
20
25
|
backend:
|
|
21
26
|
container_name: {{PROJECT_NAME}}-backend-prod
|
|
@@ -24,7 +29,7 @@ services:
|
|
|
24
29
|
dockerfile: Dockerfile
|
|
25
30
|
target: production
|
|
26
31
|
ports:
|
|
27
|
-
- "8080:8080"
|
|
32
|
+
- "${BACKEND_HOST_PORT:-8080}:8080"
|
|
28
33
|
environment:
|
|
29
34
|
- SPRING_PROFILES_ACTIVE=prod
|
|
30
35
|
- DATABASE_URL=jdbc:postgresql://db:5432/${POSTGRES_DB}
|
|
@@ -35,7 +40,17 @@ services:
|
|
|
35
40
|
condition: service_healthy
|
|
36
41
|
networks:
|
|
37
42
|
- app-network
|
|
43
|
+
- data-network
|
|
38
44
|
restart: always
|
|
45
|
+
stop_grace_period: 30s
|
|
46
|
+
healthcheck:
|
|
47
|
+
test: ["CMD", "wget", "-qO-", "http://localhost:8080/api/health"]
|
|
48
|
+
interval: 10s
|
|
49
|
+
timeout: 3s
|
|
50
|
+
retries: 5
|
|
51
|
+
deploy:
|
|
52
|
+
resources:
|
|
53
|
+
limits: { cpus: "1.0", memory: 768M }
|
|
39
54
|
|
|
40
55
|
db:
|
|
41
56
|
container_name: {{PROJECT_NAME}}-db-prod
|
|
@@ -47,7 +62,7 @@ services:
|
|
|
47
62
|
volumes:
|
|
48
63
|
- postgres-data-prod:/var/lib/postgresql/data
|
|
49
64
|
networks:
|
|
50
|
-
-
|
|
65
|
+
- data-network
|
|
51
66
|
restart: always
|
|
52
67
|
healthcheck:
|
|
53
68
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
|
@@ -62,3 +77,6 @@ volumes:
|
|
|
62
77
|
networks:
|
|
63
78
|
app-network:
|
|
64
79
|
name: {{PROJECT_NAME}}-network-prod
|
|
80
|
+
data-network:
|
|
81
|
+
name: {{PROJECT_NAME}}-data-network-prod
|
|
82
|
+
internal: true
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
FROM {{PYTHON_IMAGE}}
|
|
2
|
+
WORKDIR /app
|
|
3
|
+
COPY --from=ghcr.io/astral-sh/uv:{{UV_VERSION}} /uv /usr/local/bin/uv
|
|
4
|
+
COPY pyproject.toml uv.lock .python-version README.md ./
|
|
5
|
+
RUN uv sync --frozen --no-install-project
|
|
6
|
+
COPY . .
|
|
7
|
+
RUN uv sync --frozen
|
|
8
|
+
EXPOSE 8000
|
|
9
|
+
CMD ["uv", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
FROM {{PYTHON_IMAGE}}
|
|
2
|
+
WORKDIR /app
|
|
3
|
+
COPY --from=ghcr.io/astral-sh/uv:{{UV_VERSION}} /uv /usr/local/bin/uv
|
|
4
|
+
COPY pyproject.toml uv.lock .python-version README.md ./
|
|
5
|
+
RUN uv sync --frozen --no-install-project
|
|
6
|
+
COPY . .
|
|
7
|
+
RUN uv sync --frozen
|
|
8
|
+
RUN useradd --create-home --no-log-init appuser && chown -R appuser:appuser /app
|
|
9
|
+
EXPOSE 8000
|
|
10
|
+
USER appuser
|
|
11
|
+
CMD ["/app/.venv/bin/uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
|
@@ -5,6 +5,9 @@ RUN mvn --batch-mode package -DskipTests
|
|
|
5
5
|
|
|
6
6
|
FROM {{JAVA_IMAGE}} AS production
|
|
7
7
|
WORKDIR /app
|
|
8
|
+
RUN addgroup -S app && adduser -S -G app app
|
|
8
9
|
COPY --from=builder /app/target/*.jar app.jar
|
|
10
|
+
RUN chown app:app app.jar
|
|
11
|
+
USER app
|
|
9
12
|
EXPOSE 8080
|
|
10
|
-
CMD ["java", "-jar", "app.jar"]
|
|
13
|
+
CMD ["java", "-XX:MaxRAMPercentage=75", "-jar", "app.jar"]
|