@starci/hfs 4.0.7 → 4.0.9
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/CHANGELOG.md +22 -0
- package/README.md +86 -43
- package/emit/contracts.mjs +56 -9
- package/emit/db-types.mjs +169 -0
- package/emit/events.mjs +41 -0
- package/emit/operations-worker.mjs +1 -1
- package/emit/operations.mjs +2 -2
- package/emit/schema-worker.mjs +1 -1
- package/lint/bound-sys.cjs +50 -0
- package/lint/run.mjs +54 -20
- package/package.json +14 -9
- package/report/sonar.mjs +8 -8
- package/runtime/engine/config.mjs +494 -0
- package/runtime/engine/invalid-config.mjs +15 -0
- package/runtime/engine/orca-config.mjs +27 -0
- package/runtime/engine/runtime-root.mjs +5 -0
- package/runtime/engine/yaml.mjs +3 -3
- package/runtime/knowledge/hfs/canon-pins.yaml +163 -32
- package/runtime/knowledge/hfs/peer-integrations.yaml +2 -2
- package/runtime/knowledge/hfs/rules.yaml +2059 -0
- package/runtime/knowledge/hfs/slots.yaml +915 -123
- package/runtime/knowledge/patterns/be/api.yaml +369 -0
- package/runtime/knowledge/patterns/be/cli.yaml +297 -0
- package/runtime/knowledge/patterns/be/domain.yaml +113 -0
- package/runtime/knowledge/patterns/be/event-bus.yaml +285 -0
- package/runtime/knowledge/patterns/be/jobs.yaml +261 -0
- package/runtime/knowledge/patterns/be/projections.yaml +178 -0
- package/runtime/knowledge/patterns/be/queues.yaml +236 -0
- package/runtime/knowledge/patterns/be/reactors.yaml +140 -0
- package/runtime/knowledge/patterns/be/realtime.yaml +222 -0
- package/runtime/knowledge/patterns/be/saga.yaml +248 -0
- package/runtime/knowledge/patterns/be/webhooks.yaml +221 -0
- package/runtime/knowledge/patterns/fe/folder.yaml +133 -27
- package/runtime/knowledge/sonar-gate.yaml +53 -19
- package/runtime/modules/kernel/failure-codes.yaml +580 -50
- package/runtime/modules/models/registry.yaml +591 -0
- package/runtime/modules/models/runtimes.yaml +701 -0
- package/runtime/scripts/api/fs/forbidden-root.mjs +51 -0
- package/runtime/scripts/api/fs/is-link-like.mjs +25 -0
- package/runtime/scripts/api/fs/lib.mjs +14 -0
- package/runtime/scripts/api/fs/safe-remove.mjs +104 -0
- package/runtime/scripts/api/git/config-get.mjs +10 -0
- package/runtime/scripts/api/git/diff.mjs +5 -0
- package/runtime/scripts/api/git/lib.mjs +17 -27
- package/runtime/scripts/api/git/log.mjs +5 -0
- package/runtime/scripts/api/git/ls-files.mjs +6 -0
- package/runtime/scripts/api/git/ls-tree.mjs +5 -0
- package/runtime/scripts/api/git/merge-base.mjs +9 -0
- package/runtime/scripts/api/git/remote.mjs +5 -0
- package/runtime/scripts/api/git/rev-parse-query.mjs +5 -0
- package/runtime/scripts/api/git/show.mjs +6 -0
- package/runtime/scripts/api/sops/decrypt.mjs +10 -6
- package/runtime/scripts/api/sops/lib.mjs +35 -0
- package/runtime/scripts/api/sops/seal.mjs +18 -0
- package/runtime/scripts/{lib/hfs-allows.mjs → hfs/allows.mjs} +3 -3
- package/runtime/scripts/hfs/architecture/ast-walks.mjs +220 -0
- package/runtime/scripts/{checks → hfs}/architecture/backend.mjs +17 -24
- package/runtime/scripts/{checks → hfs}/architecture/background-unowned.mjs +16 -34
- package/runtime/scripts/{checks → hfs}/architecture/client-reaches-server.mjs +3 -1
- package/runtime/scripts/{checks → hfs}/architecture/clones.mjs +4 -4
- package/runtime/scripts/{checks → hfs}/architecture/config.mjs +11 -8
- package/runtime/scripts/{checks → hfs}/architecture/connection-map.mjs +21 -9
- package/runtime/scripts/hfs/architecture/context-coupling.mjs +88 -0
- package/runtime/scripts/hfs/architecture/context-map.mjs +237 -0
- package/runtime/scripts/hfs/architecture/context-owner.mjs +82 -0
- package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +47 -0
- package/runtime/scripts/hfs/architecture/context-transaction.mjs +60 -0
- package/runtime/scripts/{checks → hfs}/architecture/contract-fixture-guard.mjs +5 -11
- package/runtime/scripts/{checks → hfs}/architecture/contracts.mjs +33 -162
- package/runtime/scripts/{checks → hfs}/architecture/dead-exports.mjs +58 -29
- package/runtime/scripts/{checks → hfs}/architecture/doc-language.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/entrypoint.mjs +2 -1
- package/runtime/scripts/{checks → hfs}/architecture/fe-slot-allows.mjs +4 -4
- package/runtime/scripts/{checks → hfs}/architecture/feature-shape.mjs +6 -3
- package/runtime/scripts/{checks → hfs}/architecture/framework-pinned.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/frontend.mjs +17 -43
- package/runtime/scripts/{checks → hfs}/architecture/hfs-graph.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/hfs.mjs +27 -42
- package/runtime/scripts/{checks → hfs}/architecture/i18n-keys.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/index.mjs +17 -4
- package/runtime/scripts/{checks → hfs}/architecture/machine-ast.mjs +44 -11
- package/runtime/scripts/hfs/architecture/managed-scripts.mjs +27 -0
- package/runtime/scripts/{checks → hfs}/architecture/module-per-transport.mjs +30 -6
- package/runtime/scripts/hfs/architecture/next-data-contract.mjs +96 -0
- package/runtime/scripts/{checks → hfs}/architecture/next-data.mjs +40 -168
- package/runtime/scripts/{checks → hfs}/architecture/owners.mjs +35 -15
- package/runtime/scripts/{checks → hfs}/architecture/register-once.mjs +2 -1
- package/runtime/scripts/{checks → hfs}/architecture/registration.mjs +23 -81
- package/runtime/scripts/{checks → hfs}/architecture/required-files.mjs +21 -13
- package/runtime/scripts/{checks → hfs}/architecture/schema-owner.mjs +17 -114
- package/runtime/scripts/hfs/architecture/server-action.mjs +24 -0
- package/runtime/scripts/hfs/architecture/side-boundary.mjs +16 -0
- package/runtime/scripts/{checks → hfs}/architecture/source-names.mjs +27 -147
- package/runtime/scripts/{checks → hfs}/architecture/sql-owner.mjs +59 -18
- package/runtime/scripts/hfs/architecture/sql-returning.mjs +64 -0
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +111 -0
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +131 -0
- package/runtime/scripts/hfs/architecture/supabase-tables.mjs +42 -0
- package/runtime/scripts/hfs/architecture/supabase.mjs +434 -0
- package/runtime/scripts/{checks → hfs}/architecture/surface.mjs +10 -4
- package/runtime/scripts/{checks → hfs}/architecture/test-world-files.mjs +11 -21
- package/runtime/scripts/{checks → hfs}/architecture/tiers.mjs +47 -5
- package/runtime/scripts/{checks → hfs}/architecture/transport-owner.mjs +3 -3
- package/runtime/scripts/{checks → hfs}/architecture/typescript.mjs +13 -27
- package/runtime/scripts/{checks → hfs}/architecture.mjs +5 -5
- package/runtime/scripts/{lib/hfs-check.mjs → hfs/check.mjs} +95 -84
- package/runtime/scripts/hfs/coverage-scope.mjs +162 -0
- package/runtime/scripts/hfs/declaration-shape.mjs +62 -0
- package/runtime/scripts/hfs/declaration-slots.mjs +75 -0
- package/runtime/scripts/hfs/edition-slots.mjs +66 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +247 -0
- package/runtime/scripts/hfs/param-names.mjs +18 -0
- package/runtime/scripts/{lib/hfs-path-findings.mjs → hfs/path-findings.mjs} +7 -7
- package/runtime/scripts/{lib → hfs}/repo-identity.mjs +9 -6
- package/runtime/scripts/hfs/rule-params-shape.mjs +50 -0
- package/runtime/scripts/hfs/rules/app-root.mjs +51 -0
- package/runtime/scripts/hfs/rules/cli.mjs +37 -0
- package/runtime/scripts/hfs/rules/contract-compat.mjs +71 -0
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/contract.mjs +11 -9
- package/runtime/scripts/hfs/rules/database-config.mjs +102 -0
- package/runtime/scripts/hfs/rules/database-constants.mjs +13 -0
- package/runtime/scripts/hfs/rules/database-migrations.mjs +111 -0
- package/runtime/scripts/hfs/rules/database-plpgsql.mjs +233 -0
- package/runtime/scripts/hfs/rules/database-sql.mjs +336 -0
- package/runtime/scripts/hfs/rules/database.mjs +68 -0
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/deps.mjs +1 -1
- package/runtime/scripts/hfs/rules/docker.mjs +259 -0
- package/runtime/scripts/hfs/rules/edition.mjs +111 -0
- package/runtime/scripts/hfs/rules/event-bus.mjs +153 -0
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +66 -0
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/fe-no-tests.mjs +6 -5
- package/runtime/scripts/{lib/hfs-rules/frontend.mjs → hfs/rules/frontend-tree.mjs} +9 -9
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/integration-specs.mjs +3 -6
- package/runtime/scripts/hfs/rules/kinds.mjs +84 -0
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/lint-suppression.mjs +1 -1
- package/runtime/scripts/hfs/rules/monorepo.mjs +179 -0
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/peer-integrations.mjs +3 -3
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/pipeline.mjs +11 -21
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/proof-commands.mjs +2 -2
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/read.mjs +1 -1
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/repo-local-checks.mjs +2 -2
- package/runtime/scripts/hfs/rules/saga.mjs +170 -0
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/secrets.mjs +4 -4
- package/runtime/scripts/hfs/rules/services.mjs +220 -0
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/spec-placement.mjs +1 -1
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/stacks.mjs +5 -5
- package/runtime/scripts/hfs/rules/supabase-secrets.mjs +89 -0
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/test-topology.mjs +2 -2
- package/runtime/scripts/hfs/secret.mjs +175 -0
- package/runtime/scripts/{lib/hfs-slots.mjs → hfs/slots.mjs} +114 -150
- package/runtime/scripts/hfs/sql/pg-parse.mjs +46 -0
- package/runtime/scripts/hfs/test-world-slot.mjs +6 -0
- package/runtime/scripts/{lib/hfs-tree.mjs → hfs/tree.mjs} +6 -5
- package/runtime/scripts/{checks → hfs}/typescript-programs.mjs +0 -5
- package/runtime/scripts/{lib/hfs-view.mjs → hfs/view.mjs} +6 -5
- package/runtime/scripts/lib/dockerfile.mjs +124 -0
- package/runtime/scripts/lib/env.mjs +10 -0
- package/runtime/scripts/lib/event-contract.mjs +103 -0
- package/runtime/scripts/lib/git.mjs +36 -0
- package/runtime/scripts/lib/graphql-contract.mjs +429 -0
- package/runtime/scripts/lib/i18n.mjs +86 -0
- package/runtime/scripts/lib/is-main.mjs +13 -0
- package/runtime/scripts/lib/json.mjs +13 -3
- package/runtime/scripts/lib/language.mjs +26 -7
- package/runtime/scripts/lib/list.mjs +20 -0
- package/runtime/scripts/lib/package-at.mjs +37 -0
- package/runtime/scripts/lib/path-key.mjs +59 -2
- package/runtime/scripts/lib/read-text.mjs +34 -0
- package/runtime/scripts/lib/regex.mjs +4 -0
- package/runtime/scripts/lib/same-text.mjs +5 -0
- package/runtime/scripts/lib/secret-patterns.mjs +4 -3
- package/runtime/scripts/lib/sleep-sync.mjs +2 -1
- package/runtime/scripts/lib/sops-envelope.mjs +58 -0
- package/runtime/scripts/lib/stack-declaration.mjs +2 -2
- package/runtime/scripts/lib/stack-services.mjs +7 -31
- package/runtime/scripts/lib/ts-ast.mjs +35 -0
- package/runtime/scripts/{checks/common.mjs → lib/walk.mjs} +5 -15
- package/scaffold/add-app.mjs +174 -0
- package/scaffold/add-cli-lite.mjs +257 -0
- package/scaffold/add-inbox-lite.mjs +314 -0
- package/scaffold/add-table.mjs +344 -0
- package/scaffold/add.mjs +299 -0
- package/scaffold/app.mjs +359 -70
- package/scaffold/edition-gate.mjs +25 -0
- package/scaffold/image.mjs +19 -0
- package/scaffold/lite-exports.mjs +186 -0
- package/scaffold/service.mjs +33 -27
- package/scaffold/supabase-ports.mjs +26 -0
- package/src/main.mjs +327 -0
- package/sync/cli.mjs +6 -9
- package/sync/format.mjs +1 -1
- package/sync/hygiene.mjs +17 -17
- package/sync/index.mjs +171 -71
- package/sync/managed.mjs +19 -11
- package/sync/sonar-key.mjs +5 -0
- package/templates/app/ci-workflows/github/workflows/ci.yml +14 -14
- package/templates/app/ci-workflows/github/workflows/images.yml +37 -0
- package/templates/app/ci-workflows/sonar-steps.yml +9 -0
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +66 -0
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +31 -0
- package/templates/app/ci-workflows-lite/github/workflows/images.yml +37 -0
- package/templates/app/codegen-lite/scripts/codegen.mjs +32 -0
- package/templates/app/docker-ignore/dockerignore +50 -0
- package/templates/app/gitignore-lite +35 -0
- package/templates/app/hooks/husky/pre-commit +3 -8
- package/templates/app/hooks/husky/pre-push +45 -6
- package/templates/app/package-scripts/core.json +8 -0
- package/templates/app/package-scripts/package.json +3 -10
- package/templates/app/package-scripts-lite/package.json +15 -0
- package/templates/app/quality-config/codecov.yml +15 -3
- package/templates/app/quality-config/sonar-base.properties +4 -0
- package/templates/app/quality-config/sonar-project.properties +2 -5
- package/templates/app/quality-config-lite/sonar-project.properties +5 -0
- package/templates/app/skeleton/.starcistacks/dev/seeds/primary-notes.sql +5 -0
- package/templates/app/skeleton/.starciwork/features/system-health/index.yaml +4 -0
- package/templates/app/skeleton/.starciwork/{features/index.yaml → index.yaml} +2 -2
- package/templates/app/skeleton/README.md +7 -5
- package/templates/app/skeleton-lite/.sops.yaml +3 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +5 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/secrets/.gitkeep +1 -0
- package/templates/app/skeleton-lite/.starciwork/shell/index.yaml +55 -0
- package/templates/app/skeleton-lite/README.md +40 -0
- package/templates/app/skeleton-lite/supabase/config.toml +41 -0
- package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +56 -0
- package/templates/app/skeleton-lite/supabase/seed.sql +0 -0
- package/templates/app/task-graph/turbo.json +23 -0
- package/templates/app/tool-config-lite/prettierignore +2 -0
- package/templates/app/tool-config-lite/prettierrc +1 -0
- package/templates/app/upgrade-full/.starcistacks/application-stacks.yaml +38 -0
- package/templates/app/upgrade-full/.starcistacks/dev/README.md +10 -0
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/api.yaml +13 -0
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/compose.yaml +3 -0
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/postgres.yaml +12 -0
- package/templates/app/upgrade-full/.starcistacks/dev/infra/metadata.json +7 -0
- package/templates/be/common/main-listen.ts.partial +14 -0
- package/templates/be/common/sql-ident.ts.partial +7 -0
- package/templates/be/common/sql-tag.ts.partial +5 -0
- package/templates/be/image/api/Dockerfile +38 -0
- package/templates/be/image/cli/Dockerfile +38 -0
- package/templates/be/image/worker/Dockerfile +36 -0
- package/templates/be/inbox/database-client.ts.tpl +50 -0
- package/templates/be/inbox/index.ts.tpl +2 -0
- package/templates/be/inbox/migration.sql.tpl +29 -0
- package/templates/be/inbox/module-definition.ts.tpl +6 -0
- package/templates/be/inbox/module.ts.tpl +14 -0
- package/templates/be/inbox/options.ts.tpl +2 -0
- package/templates/be/inbox/service.ts.tpl +33 -0
- package/templates/be/inbox/sql.ts.tpl +16 -0
- package/templates/be/inbox/webhook-door.ts.tpl +36 -0
- package/templates/be/patterns/api/application.command.http.ts.tpl +10 -0
- package/templates/be/patterns/api/application.command.ts.tpl +10 -0
- package/templates/be/patterns/api/application.contracts.ts.tpl +11 -0
- package/templates/be/patterns/api/application.handler.http.ts.tpl +22 -0
- package/templates/be/patterns/api/application.handler.ts.tpl +22 -0
- package/templates/be/patterns/api/feature.index.http.ts.tpl +1 -0
- package/templates/be/patterns/api/feature.index.ts.tpl +1 -0
- package/templates/be/patterns/api/feature.module.ts.tpl +6 -0
- package/templates/be/patterns/api/graphql.input.ts.tpl +13 -0
- package/templates/be/patterns/api/graphql.mapper.ts.tpl +9 -0
- package/templates/be/patterns/api/graphql.module.ts.tpl +7 -0
- package/templates/be/patterns/api/graphql.resolver.ts.tpl +27 -0
- package/templates/be/patterns/api/graphql.type.ts.tpl +9 -0
- package/templates/be/patterns/api/http.controller.ts.tpl +27 -0
- package/templates/be/patterns/api/http.mapper.ts.tpl +9 -0
- package/templates/be/patterns/api/http.module.ts.tpl +7 -0
- package/templates/be/patterns/api/http.request.ts.tpl +10 -0
- package/templates/be/patterns/api/http.response.ts.tpl +5 -0
- package/templates/be/patterns/cli/group.cli.spec.ts.tpl +15 -0
- package/templates/be/patterns/cli/group.cli.ts.tpl +11 -0
- package/templates/be/patterns/cli/group.module.ts.tpl +7 -0
- package/templates/be/patterns/cli/sub.run.cli.spec.ts.tpl +17 -0
- package/templates/be/patterns/cli/sub.run.cli.ts.tpl +15 -0
- package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +22 -0
- package/templates/be/patterns/event-bus/platform/event-bus.config.ts.tpl +19 -0
- package/templates/be/patterns/event-bus/platform/event-bus.contracts.ts.tpl +58 -0
- package/templates/be/patterns/event-bus/platform/event-bus.decorators.ts.tpl +46 -0
- package/templates/be/patterns/event-bus/platform/event-bus.log-events.ts.tpl +12 -0
- package/templates/be/patterns/event-bus/platform/event-bus.module-definition.ts.tpl +10 -0
- package/templates/be/patterns/event-bus/platform/event-bus.module.ts.tpl +52 -0
- package/templates/be/patterns/event-bus/platform/event-bus.options.ts.tpl +19 -0
- package/templates/be/patterns/event-bus/platform/event-bus.port.ts.tpl +28 -0
- package/templates/be/patterns/event-bus/platform/event-bus.service.spec.ts.tpl +165 -0
- package/templates/be/patterns/event-bus/platform/event-bus.service.ts.tpl +93 -0
- package/templates/be/patterns/event-bus/platform/event-envelope.policy.spec.ts.tpl +90 -0
- package/templates/be/patterns/event-bus/platform/event-envelope.policy.ts.tpl +58 -0
- package/templates/be/patterns/event-bus/platform/event-relay.service.spec.ts.tpl +136 -0
- package/templates/be/patterns/event-bus/platform/event-relay.service.ts.tpl +56 -0
- package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +272 -0
- package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +128 -0
- package/templates/be/patterns/event-bus/platform/event-transport.port.ts.tpl +180 -0
- package/templates/be/patterns/event-bus/platform/event.policy.spec.ts.tpl +67 -0
- package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +38 -0
- package/templates/be/patterns/event-bus/platform/index.ts.tpl +11 -0
- package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.spec.ts.tpl +348 -0
- package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +241 -0
- package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +15 -0
- package/templates/be/patterns/event-bus/platform/persistence/connection.ts.tpl +8 -0
- package/templates/be/patterns/event-bus/platform/persistence/entities/event-outbox.entity.ts.tpl +37 -0
- package/templates/be/patterns/event-bus/platform/persistence/event-bus.rows.ts.tpl +11 -0
- package/templates/be/patterns/event-bus/platform/persistence/event-bus.sql.ts.tpl +12 -0
- package/templates/be/patterns/event-bus/platform/persistence/migrations/1789800009000-create-event-outbox.ts.tpl +26 -0
- package/templates/be/patterns/jobs/job.index.ts.tpl +1 -0
- package/templates/be/patterns/jobs/job.module.ts.tpl +6 -0
- package/templates/be/patterns/jobs/job.processor.ts.tpl +20 -0
- package/templates/be/patterns/jobs/job.queue.module.ts.tpl +20 -0
- package/templates/be/patterns/jobs/job.step.ts.tpl +19 -0
- package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +31 -0
- package/templates/be/patterns/jobs/platform/fenced.processor.ts.tpl +13 -0
- package/templates/be/patterns/jobs/platform/job-claim.service.spec.ts.tpl +150 -0
- package/templates/be/patterns/jobs/platform/job-claim.service.ts.tpl +100 -0
- package/templates/be/patterns/jobs/platform/job-runner.service.spec.ts.tpl +138 -0
- package/templates/be/patterns/jobs/platform/job-runner.service.ts.tpl +69 -0
- package/templates/be/patterns/jobs/platform/jobs.config.ts.tpl +11 -0
- package/templates/be/patterns/jobs/platform/jobs.contracts.ts.tpl +62 -0
- package/templates/be/patterns/jobs/platform/jobs.decorators.ts.tpl +31 -0
- package/templates/be/patterns/jobs/platform/jobs.error.ts.tpl +22 -0
- package/templates/be/patterns/jobs/platform/jobs.index.ts.tpl +11 -0
- package/templates/be/patterns/jobs/platform/jobs.log-events.ts.tpl +9 -0
- package/templates/be/patterns/jobs/platform/jobs.messages.ts.tpl +15 -0
- package/templates/be/patterns/jobs/platform/jobs.module-definition.ts.tpl +10 -0
- package/templates/be/patterns/jobs/platform/jobs.module.ts.tpl +32 -0
- package/templates/be/patterns/jobs/platform/jobs.options.ts.tpl +11 -0
- package/templates/be/patterns/jobs/platform/jobs.port.ts.tpl +36 -0
- package/templates/be/patterns/jobs/platform/persistence.connection.ts.tpl +8 -0
- package/templates/be/patterns/jobs/platform/persistence.job.entity.ts.tpl +53 -0
- package/templates/be/patterns/jobs/platform/persistence.jobs.rows.ts.tpl +13 -0
- package/templates/be/patterns/jobs/platform/persistence.jobs.sql.ts.tpl +33 -0
- package/templates/be/patterns/jobs/platform/persistence.migration.ts.tpl +29 -0
- package/templates/be/patterns/outbox/platform/index.ts.tpl +2 -0
- package/templates/be/patterns/outbox/platform/outbox-relay.policy.spec.ts.tpl +96 -0
- package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +78 -0
- package/templates/be/patterns/outbox/platform/outbox.log-events.ts.tpl +5 -0
- package/templates/be/patterns/projections/migration.ts.tpl +18 -0
- package/templates/be/patterns/projections/persistence.connection.ts.tpl +8 -0
- package/templates/be/patterns/projections/projection.entity.ts.tpl +9 -0
- package/templates/be/patterns/projections/projection.index.ts.tpl +3 -0
- package/templates/be/patterns/projections/projection.module-definition.ts.tpl +7 -0
- package/templates/be/patterns/projections/projection.module.ts.tpl +18 -0
- package/templates/be/patterns/projections/projection.options.ts.tpl +2 -0
- package/templates/be/patterns/projections/projection.projection.spec.ts.tpl +55 -0
- package/templates/be/patterns/projections/projection.projection.ts.tpl +21 -0
- package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.spec.ts.tpl +142 -0
- package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.ts.tpl +81 -0
- package/templates/be/patterns/queues/platform/persistence.connection.ts.tpl +8 -0
- package/templates/be/patterns/queues/platform/persistence.migration.ts.tpl +25 -0
- package/templates/be/patterns/queues/platform/persistence.queue-outbox.entity.ts.tpl +25 -0
- package/templates/be/patterns/queues/platform/persistence.queue.rows.ts.tpl +9 -0
- package/templates/be/patterns/queues/platform/persistence.queue.sql.ts.tpl +12 -0
- package/templates/be/patterns/queues/platform/queue-outbox.service.spec.ts.tpl +48 -0
- package/templates/be/patterns/queues/platform/queue-outbox.service.ts.tpl +17 -0
- package/templates/be/patterns/queues/platform/queue-relay.service.spec.ts.tpl +128 -0
- package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +48 -0
- package/templates/be/patterns/queues/platform/queue-transport.port.ts.tpl +115 -0
- package/templates/be/patterns/queues/platform/queue-worker.service.spec.ts.tpl +91 -0
- package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +45 -0
- package/templates/be/patterns/queues/platform/queue.config.ts.tpl +15 -0
- package/templates/be/patterns/queues/platform/queue.contracts.ts.tpl +31 -0
- package/templates/be/patterns/queues/platform/queue.decorators.ts.tpl +45 -0
- package/templates/be/patterns/queues/platform/queue.index.ts.tpl +7 -0
- package/templates/be/patterns/queues/platform/queue.log-events.ts.tpl +5 -0
- package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +10 -0
- package/templates/be/patterns/queues/platform/queue.module.ts.tpl +62 -0
- package/templates/be/patterns/queues/platform/queue.options.ts.tpl +22 -0
- package/templates/be/patterns/queues/platform/queue.policy.spec.ts.tpl +19 -0
- package/templates/be/patterns/queues/platform/queue.policy.ts.tpl +11 -0
- package/templates/be/patterns/queues/platform/queue.port.ts.tpl +14 -0
- package/templates/be/patterns/queues/queue.index.ts.tpl +3 -0
- package/templates/be/patterns/queues/queue.module.ts.tpl +6 -0
- package/templates/be/patterns/queues/queue.queue.ts.tpl +23 -0
- package/templates/be/patterns/reactors/application.command.ts.tpl +10 -0
- package/templates/be/patterns/reactors/application.contracts.ts.tpl +12 -0
- package/templates/be/patterns/reactors/application.handler.ts.tpl +22 -0
- package/templates/be/patterns/reactors/consumer.ts.tpl +19 -0
- package/templates/be/patterns/reactors/message.module.ts.tpl +20 -0
- package/templates/be/patterns/reactors/reactor.index.ts.tpl +1 -0
- package/templates/be/patterns/reactors/reactor.module.ts.tpl +6 -0
- package/templates/be/patterns/realtime/channel.graphql.module.ts.tpl +6 -0
- package/templates/be/patterns/realtime/channel.index.ts.tpl +1 -0
- package/templates/be/patterns/realtime/channel.input.ts.tpl +12 -0
- package/templates/be/patterns/realtime/channel.subscription.spec.ts.tpl +51 -0
- package/templates/be/patterns/realtime/channel.subscription.ts.tpl +32 -0
- package/templates/be/patterns/realtime/channel.type.ts.tpl +9 -0
- package/templates/be/patterns/saga/saga.compensate.command.ts.tpl +11 -0
- package/templates/be/patterns/saga/saga.compensate.contracts.ts.tpl +7 -0
- package/templates/be/patterns/saga/saga.compensate.handler.ts.tpl +22 -0
- package/templates/be/patterns/saga/saga.compensation.ts.tpl +21 -0
- package/templates/be/patterns/saga/saga.complete.command.ts.tpl +11 -0
- package/templates/be/patterns/saga/saga.complete.contracts.ts.tpl +7 -0
- package/templates/be/patterns/saga/saga.complete.handler.ts.tpl +22 -0
- package/templates/be/patterns/saga/saga.done.consumer.ts.tpl +21 -0
- package/templates/be/patterns/saga/saga.failed.consumer.ts.tpl +21 -0
- package/templates/be/patterns/saga/saga.index.ts.tpl +1 -0
- package/templates/be/patterns/saga/saga.message.module.ts.tpl +23 -0
- package/templates/be/patterns/saga/saga.module.ts.tpl +20 -0
- package/templates/be/patterns/saga/saga.service.spec.ts.tpl +70 -0
- package/templates/be/patterns/saga/saga.service.ts.tpl +44 -0
- package/templates/be/patterns/saga/saga.state.ts.tpl +14 -0
- package/templates/be/patterns/saga/saga.step.ts.tpl +11 -0
- package/templates/be/patterns/saga/saga.undo.command.ts.tpl +10 -0
- package/templates/be/patterns/saga/saga.undo.contracts.ts.tpl +5 -0
- package/templates/be/patterns/saga/saga.undo.handler.ts.tpl +21 -0
- package/templates/be/patterns/webhooks/webhook.door.spec.ts.tpl +72 -0
- package/templates/be/patterns/webhooks/webhook.door.ts.tpl +36 -0
- package/templates/be/patterns/webhooks/webhook.http.module.ts.tpl +6 -0
- package/templates/be/patterns/webhooks/webhook.index.ts.tpl +1 -0
- package/templates/be/patterns/webhooks/webhook.request.ts.tpl +9 -0
- package/templates/be/skeleton/apps/__app__/src/__app__.options.ts +3 -0
- package/templates/be/skeleton/apps/__app__/src/app.module.ts +15 -2
- package/templates/be/skeleton/apps/__app__/src/main.ts +7 -15
- package/templates/be/skeleton/apps/cli/src/app.module.ts +30 -0
- package/templates/be/skeleton/apps/cli/src/cli.options.ts +22 -0
- package/templates/be/skeleton/apps/cli/src/main.ts +24 -0
- package/templates/be/skeleton/src/features/cli/cli.module.ts +7 -0
- package/templates/be/skeleton/src/features/cli/index.ts +1 -0
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +17 -0
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +15 -0
- package/templates/be/skeleton/src/features/cli/migrate/migrate.module.ts +7 -0
- package/templates/be/skeleton/src/features/cli/migrate/subs/run.cli.spec.ts +17 -0
- package/templates/be/skeleton/src/features/cli/migrate/subs/run.cli.ts +18 -0
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +17 -0
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +15 -0
- package/templates/be/skeleton/src/features/cli/seed/seed.module.ts +7 -0
- package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.spec.ts +33 -0
- package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.ts +34 -0
- package/templates/be/skeleton/src/modules/domain/identity/admission.policy.spec.ts +13 -0
- package/templates/be/skeleton/src/modules/domain/identity/auth.guard.spec.ts +40 -0
- package/templates/be/skeleton/src/modules/domain/identity/messages/identity.messages.ts +1 -1
- package/templates/be/skeleton/src/modules/domain/note/errors/note.error.ts +16 -0
- package/templates/be/skeleton/src/modules/domain/note/index.ts +3 -0
- package/templates/be/skeleton/src/modules/domain/note/note.contracts.ts +19 -0
- package/templates/be/skeleton/src/modules/domain/note/note.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/domain/note/note.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/domain/note/note.module.ts +19 -0
- package/templates/be/skeleton/src/modules/domain/note/note.options.ts +2 -0
- package/templates/be/skeleton/src/modules/domain/note/note.service.spec.ts +76 -0
- package/templates/be/skeleton/src/modules/domain/note/note.service.ts +37 -0
- package/templates/be/skeleton/src/modules/domain/note/persistence/connection.ts +8 -0
- package/templates/be/skeleton/src/modules/domain/note/persistence/entities/note.entity.ts +17 -0
- package/templates/be/skeleton/src/modules/domain/note/persistence/migrations/1790000000000-init-note.ts +22 -0
- package/templates/be/skeleton/src/modules/domain/note/persistence/note.rows.ts +17 -0
- package/templates/be/skeleton/src/modules/domain/note/persistence/note.sql.ts +4 -0
- package/templates/be/skeleton/src/modules/platform/config/env-source.config.ts +20 -0
- package/templates/be/skeleton/src/modules/platform/config/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.contracts.ts +18 -0
- package/templates/be/skeleton/src/modules/platform/database/connection-source.client.spec.ts +41 -0
- package/templates/be/skeleton/src/modules/platform/database/connection-source.client.ts +17 -0
- package/templates/be/skeleton/src/modules/platform/database/database.module-definition.ts +13 -0
- package/templates/be/skeleton/src/modules/platform/database/database.module.ts +77 -0
- package/templates/be/skeleton/src/modules/platform/database/database.options.ts +30 -0
- package/templates/be/skeleton/src/modules/platform/database/database.port.ts +65 -0
- package/templates/be/skeleton/src/modules/platform/database/database.sql.ts +47 -0
- package/templates/be/skeleton/src/modules/platform/database/errors/database.error.ts +16 -0
- package/templates/be/skeleton/src/modules/platform/database/index.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +30 -0
- package/templates/be/skeleton/src/modules/platform/database/migration-runner.service.spec.ts +84 -0
- package/templates/be/skeleton/src/modules/platform/database/migration-runner.service.ts +23 -0
- package/templates/be/skeleton/src/modules/platform/database/primary.config.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/database/primary.connection.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/database/primary.decorators.ts +10 -0
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +108 -0
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +75 -0
- package/templates/be/skeleton/src/modules/platform/database/seed-runner.service.spec.ts +88 -0
- package/templates/be/skeleton/src/modules/platform/database/seed-runner.service.ts +29 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +48 -0
- package/templates/be/skeleton/src/modules/platform/errors/messages/errors.messages.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +12 -0
- package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +16 -0
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +8 -1
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.decorators.ts +8 -0
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +14 -1
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +12 -0
- package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +10 -2
- package/templates/be/skeleton/src/modules/platform/http-security/origin.guard.spec.ts +63 -0
- package/templates/be/skeleton/src/modules/platform/http-security/origin.guard.ts +2 -2
- package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +108 -0
- package/templates/be/skeleton/src/modules/platform/http-security/request-validation.service.spec.ts +39 -0
- package/templates/be/skeleton/src/modules/platform/http-security/request-validation.service.ts +25 -0
- package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.spec.ts +172 -0
- package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.ts +66 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +3 -1
- package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.spec.ts +52 -0
- package/templates/be/skeleton-lite/apps/api/Dockerfile +37 -0
- package/templates/be/skeleton-lite/apps/api/src/api.options.ts +16 -0
- package/templates/be/skeleton-lite/apps/api/src/app.module.ts +81 -0
- package/templates/be/skeleton-lite/apps/api/src/main.ts +34 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.ts +14 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +50 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +26 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +19 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +26 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +7 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +6 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +19 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/index.ts +10 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.ts +24 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.config.ts +8 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.decorators.ts +17 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.jwks.ts +32 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.options.ts +24 -0
- package/templates/be/skeleton-lite/src/modules/platform/cqrs/index.ts +4 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +54 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/database.options.ts +23 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +10 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/database.sql.ts +49 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +4 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/migration-runner.service.ts +21 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/primary.config.ts +11 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/seed-runner.service.ts +28 -0
- package/templates/be/skeleton-lite/src/modules/platform/http-security/http-security.module.ts +25 -0
- package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +9 -0
- package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +4 -0
- package/templates/be/skeleton-lite/src/modules/platform/logging/index.ts +5 -0
- package/templates/be/table/error.ts.tpl +16 -0
- package/templates/be/table/index.ts.tpl +2 -0
- package/templates/be/table/migration.sql.tpl +51 -0
- package/templates/be/table/module-definition.ts.tpl +7 -0
- package/templates/be/table/module.ts.tpl +14 -0
- package/templates/be/table/options.ts.tpl +2 -0
- package/templates/be/table/rows.ts.tpl +4 -0
- package/templates/be/table/service.ts.tpl +23 -0
- package/templates/be/table/sql.ts.tpl +4 -0
- package/templates/be/tool-config/jest.config.js +3 -1
- package/templates/be/upgrade-full/apps/cli/src/app.module.ts +14 -0
- package/templates/be/upgrade-full/apps/cli/src/main.ts +26 -0
- package/templates/be/upgrade-full/src/features/cli/cli.module.ts +6 -0
- package/templates/be/upgrade-full/src/features/cli/migrate/subs/run.cli.ts +23 -0
- package/templates/common/gitignore.base +1 -2
- package/templates/fe/app/package.json.tpl +1 -0
- package/templates/fe/app/tsconfig.json +12 -0
- package/templates/fe/common/app-layout.tsx.partial +8 -0
- package/templates/fe/common/site-shell.tsx.partial +9 -0
- package/templates/fe/image/next/Dockerfile +42 -0
- package/templates/fe/skeleton/apps/app/next.config.ts +23 -0
- package/templates/fe/skeleton/apps/app/postcss.config.mjs +7 -0
- package/templates/fe/skeleton/apps/{__app__ → app}/src/app/[locale]/error.tsx +3 -3
- package/templates/fe/skeleton/apps/app/src/app/[locale]/layout.tsx +22 -0
- package/templates/fe/skeleton/apps/{__app__ → app}/src/app/[locale]/loading.tsx +2 -2
- package/templates/fe/skeleton/apps/{__app__ → app}/src/app/[locale]/not-found.tsx +2 -2
- package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +9 -0
- package/templates/fe/skeleton/apps/{__app__ → app}/src/app/global-error.tsx +2 -2
- package/templates/fe/skeleton/apps/app/src/app/globals.css +10 -0
- package/templates/fe/skeleton/apps/app/src/features/layouts/AppLayout/index.tsx +18 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppErrorPage/component.tsx +19 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppErrorPage/index.tsx +11 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppGlobalErrorPage/index.tsx +16 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/component.tsx +10 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +15 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppLoadingPage/index.tsx +8 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppNotFoundPage/index.tsx +9 -0
- package/templates/fe/skeleton/apps/app/src/modules/i18n/index.ts +4 -0
- package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +28 -0
- package/templates/fe/skeleton/apps/app/src/modules/i18n/request.ts +4 -0
- package/templates/fe/skeleton/apps/app/src/modules/routes/index.ts +4 -0
- package/templates/fe/skeleton/apps/app/src/proxy.ts +1 -0
- package/templates/fe/skeleton/apps/landing/next.config.ts +21 -0
- package/templates/fe/skeleton/apps/landing/postcss.config.mjs +7 -0
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/error.tsx +10 -0
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/layout.tsx +22 -0
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/loading.tsx +6 -0
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/not-found.tsx +6 -0
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +9 -0
- package/templates/fe/skeleton/apps/landing/src/app/global-error.tsx +10 -0
- package/templates/fe/skeleton/apps/landing/src/app/globals.css +10 -0
- package/templates/fe/skeleton/apps/landing/src/features/layouts/LandingLayout/index.tsx +24 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingErrorPage/component.tsx +18 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingErrorPage/index.tsx +11 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingGlobalErrorPage/index.tsx +16 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingLoadingPage/index.tsx +8 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingNotFoundPage/index.tsx +9 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingPage/component.tsx +24 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingPage/index.tsx +16 -0
- package/templates/fe/skeleton/apps/landing/src/modules/config/index.ts +12 -0
- package/templates/fe/skeleton/apps/landing/src/modules/i18n/index.ts +7 -0
- package/templates/fe/skeleton/apps/landing/src/modules/i18n/messages/vi.json +31 -0
- package/templates/fe/skeleton/apps/landing/src/modules/i18n/request.ts +4 -0
- package/templates/fe/skeleton/apps/landing/src/modules/routes/index.ts +4 -0
- package/templates/fe/skeleton/apps/landing/src/proxy.ts +1 -0
- package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +49 -0
- package/templates/fe/skeleton/packages/__project__-i18n/src/proxy.ts +24 -0
- package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +20 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/branches/Providers/index.tsx +23 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/ErrorNotice/index.tsx +14 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/GlobalErrorNotice/index.tsx +25 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/LoadingNotice/index.tsx +9 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/LocaleShell/index.tsx +21 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/NotFoundNotice/index.tsx +19 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/SiteShell/classNames.ts +4 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/SiteShell/index.tsx +19 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/index.ts +6 -0
- package/templates/fe/skeleton-lite/apps/web/next.config.ts +22 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/error.tsx +10 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +37 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/loading.tsx +6 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/not-found.tsx +6 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +9 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/providers.tsx +26 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/auth/callback/route.ts +24 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/global-error.tsx +10 -0
- package/templates/fe/{skeleton/apps/__app__ → skeleton-lite/apps/web}/src/app/globals.css +1 -1
- package/templates/fe/skeleton-lite/apps/web/src/app/health/live/route.ts +5 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/component.tsx +29 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +41 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignOutButton/component.tsx +17 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignOutButton/index.tsx +38 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/ErrorNotice/index.tsx +13 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/GlobalErrorNotice/index.tsx +24 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/LoadingNotice/index.tsx +7 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/NotFoundNotice/index.tsx +18 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/SiteShell/classNames.ts +4 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/SiteShell/index.tsx +13 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/layouts/AppLayout/index.tsx +31 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppErrorPage/component.tsx +12 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppErrorPage/index.tsx +10 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppGlobalErrorPage/index.tsx +15 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/component.tsx +14 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +30 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppLoadingPage/index.tsx +8 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppNotFoundPage/index.tsx +9 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/brand/brand.css +20 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/brand/index.ts +2 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/config/index.ts +20 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/read-session.ts +25 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-in.ts +25 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +17 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/browser.ts +14 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/index.ts +3 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/outcome.ts +59 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/principal.ts +29 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/server.ts +70 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +50 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/index.ts +8 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +48 -0
- package/templates/fe/{skeleton/apps/__app__ → skeleton-lite/apps/web}/src/modules/i18n/request.ts +4 -2
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +14 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/routes/index.ts +7 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/routes/safe-next-path.ts +26 -0
- package/templates/fe/skeleton-lite/apps/web/src/proxy.ts +43 -0
- package/templates/fe/table/read.ts.tpl +16 -0
- package/templates/fe/table/write.ts.tpl +16 -0
- package/upgrade/check.mjs +137 -0
- package/upgrade/index.mjs +524 -0
- package/bin/hfs.mjs +0 -233
- package/runtime/engine/admission.mjs +0 -284
- package/runtime/engine/digest.mjs +0 -10
- package/runtime/engine/ledger-db.mjs +0 -1245
- package/runtime/engine/machine-db.mjs +0 -1565
- package/runtime/engine/migrations/machine/0001-init.sql +0 -887
- package/runtime/engine/migrations/machine/0002-worktrees-no-workflow-kind.sql +0 -13
- package/runtime/engine/migrations/machine/0003-worktrees-workflow-orca.sql +0 -21
- package/runtime/engine/migrations/runtime/0001-init.sql +0 -1072
- package/runtime/engine/migrations/runtime/0003-usage-unavailable.sql +0 -13
- package/runtime/engine/migrations/runtime/0004-attempt-why.sql +0 -43
- package/runtime/engine/migrations/runtime/0005-ended-workflow-views.sql +0 -93
- package/runtime/scripts/api/fs/rmdir-link.mjs +0 -12
- package/runtime/scripts/lib/artifact-hold.mjs +0 -89
- package/runtime/scripts/lib/artifact-store.mjs +0 -103
- package/runtime/scripts/lib/redact.mjs +0 -148
- package/runtime/scripts/lib/safe-remove.mjs +0 -275
- package/runtime/scripts/lib/test-secrets.mjs +0 -116
- package/templates/fe/skeleton/apps/__app__/next.config.ts +0 -11
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/layout.tsx +0 -20
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/page.tsx +0 -9
- package/templates/fe/skeleton/apps/__app__/src/components/composites/FailureScreen/index.tsx +0 -28
- package/templates/fe/skeleton/apps/__app__/src/features/layouts/LocaleShell/index.tsx +0 -35
- package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/component.tsx +0 -21
- package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/index.tsx +0 -17
- package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/component.tsx +0 -26
- package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/index.tsx +0 -15
- package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/component.tsx +0 -25
- package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/index.tsx +0 -15
- package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/component.tsx +0 -27
- package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/index.tsx +0 -8
- package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/component.tsx +0 -32
- package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/index.tsx +0 -8
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/config.ts +0 -8
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/index.ts +0 -2
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/messages/vi.json +0 -25
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/routing.ts +0 -9
- package/templates/fe/skeleton/apps/__app__/src/modules/routes/index.ts +0 -4
- package/templates/fe/skeleton/apps/__app__/src/proxy.ts +0 -10
- /package/runtime/scripts/{checks → hfs}/architecture/config-unread.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/constructor-deps.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/cross-app-duplicate.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/default-deny.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/error-codes.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/error-masked.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/hooks-are-hooks.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/injection-token-exported.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/package-shape.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/reachability.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/route-files-thin.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/sql-tokens.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/symbols.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/unit-spec-providers.mjs +0 -0
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/application/check-liveness.contracts.ts +0 -0
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/application/check-liveness.handler.ts +0 -0
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/application/check-liveness.query.ts +0 -0
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/index.ts +0 -0
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/system-health.module.ts +0 -0
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/transport/http/live.controller.ts +0 -0
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/transport/http/system-health-http.module.ts +0 -0
- /package/templates/fe/skeleton/apps/{__app__ → app}/src/app/health/live/route.ts +0 -0
- /package/templates/fe/skeleton/apps/{__app__ → app}/src/modules/config/index.ts +0 -0
- /package/templates/fe/{skeleton/apps/__app__ → skeleton-lite/apps/web}/postcss.config.mjs +0 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// docker.mjs - every app builds its own image: be/apps/<app>/Dockerfile and fe/apps/<app>/Dockerfile (slot repo.app-image, required per declared
|
|
2
|
+
// app; a Dockerfile anywhere else is owned by no slot) built from the APP ROOT, which holds the one package.json and the managed .dockerignore.
|
|
3
|
+
// The Dockerfile is app-owned (starci app scaffold writes it once from templates/<side>/image) and judged here, structurally, from its parsed
|
|
4
|
+
// instructions (scripts/lib/dockerfile.mjs), never by matching the text:
|
|
5
|
+
// HFS_DOCKER_BUILD_CONTEXT the header comment states the app's own build command (`docker build -f <side>/apps/<app>/Dockerfile ... .`, the
|
|
6
|
+
// context the app root) and no COPY or ADD source leaves the context (`..`, an absolute path)
|
|
7
|
+
// HFS_DOCKER_STAGES a `build` stage and a last `runtime` stage; the runtime is unprivileged (`USER node`) and runs no build; every install
|
|
8
|
+
// is `npm ci` (never `npm install`); a back-end runtime installs `--omit=dev --ignore-scripts`, a front-end runtime installs nothing
|
|
9
|
+
// HFS_DOCKER_ENTRY the runtime starts the app's own built entry (be: node be/dist/apps/<app>/src/main.js; fe: node fe/apps/<app>/server.js); an api or
|
|
10
|
+
// next app EXPOSEs the PORT it serves and carries a HEALTHCHECK, a worker carries a process HEALTHCHECK, the cli app
|
|
11
|
+
// says `HEALTHCHECK NONE` and exposes nothing; a Next app builds `output: "standalone"` and its build runs turbo for its own workspace
|
|
12
|
+
// HFS_DOCKER_BASE_PIN every FROM is a prior stage, the one node base image of the canon, or an image pinned by digest; never a moving tag
|
|
13
|
+
// HFS_DOCKER_SECRETS no secret enters an image: no COPY or ADD of an .env file or of .starcistacks, .secrets, a key or a certificate, no ADD of a
|
|
14
|
+
// URL, no ARG or ENV whose name is a credential (NEXT_PUBLIC_* are published by design and are the one exception)
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { loadTypescript } from '../../lib/package-at.mjs';
|
|
18
|
+
import { propertyText } from '../../lib/ts-ast.mjs';
|
|
19
|
+
import { execForm, parseDockerfile, shellCommands, words } from '../../lib/dockerfile.mjs';
|
|
20
|
+
import { found, readText } from './read.mjs';
|
|
21
|
+
|
|
22
|
+
const DOCKER_BUILD_CONTEXT = 'HFS_DOCKER_BUILD_CONTEXT';
|
|
23
|
+
const DOCKER_STAGES = 'HFS_DOCKER_STAGES';
|
|
24
|
+
const DOCKER_ENTRY = 'HFS_DOCKER_ENTRY';
|
|
25
|
+
const DOCKER_BASE_PIN = 'HFS_DOCKER_BASE_PIN';
|
|
26
|
+
const DOCKER_SECRETS = 'HFS_DOCKER_SECRETS';
|
|
27
|
+
|
|
28
|
+
/** The one node base image of every Dockerfile of the canon: an exact node version on an exact alpine release. */
|
|
29
|
+
export const NODE_IMAGE = 'node:22.14.0-alpine3.21';
|
|
30
|
+
/** The two stages every Dockerfile has: the build, then the runtime that ships. */
|
|
31
|
+
const BUILD_STAGE = 'build';
|
|
32
|
+
const RUNTIME_STAGE = 'runtime';
|
|
33
|
+
/** The port a listening app serves unless its environment says otherwise (ENV PORT of the runtime stage). */
|
|
34
|
+
export const DEFAULT_PORT = '3000';
|
|
35
|
+
|
|
36
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
37
|
+
const BE_KINDS_WITH_LISTENER = new Set(['api']);
|
|
38
|
+
const BE_KINDS_ONE_SHOT = new Set(['cli']);
|
|
39
|
+
const CREDENTIAL_PARTS = new Set(['PASSWORD', 'PASSWD', 'SECRET', 'SECRETS', 'TOKEN', 'TOKENS', 'KEY', 'KEYS', 'CREDENTIAL', 'CREDENTIALS', 'PRIVATE']);
|
|
40
|
+
const SECRET_FOLDERS = new Set(['.starcistacks', '.secrets', '.volume', '.git', '.ssh', '.aws']);
|
|
41
|
+
const SECRET_SUFFIXES = ['.key', '.pem', '.p12', '.pfx', '.crt'];
|
|
42
|
+
const BUILD_TOOLS = new Set(['tsc', 'tsc-alias', 'next', 'turbo', 'nest', 'webpack', 'vite', 'esbuild']);
|
|
43
|
+
const DIGEST = /@sha256:[0-9a-f]{64}$/;
|
|
44
|
+
|
|
45
|
+
/** The path of the Dockerfile of a be or fe app, app-relative. */
|
|
46
|
+
export const dockerfilePath = (side, app) => `${side}/apps/${app}/Dockerfile`;
|
|
47
|
+
/** The entry a built be app starts (from the app root). */
|
|
48
|
+
const beEntry = (app) => `be/dist/apps/${app}/src/main.js`;
|
|
49
|
+
/** The entry a Next standalone fe app starts (from the app root: the standalone tree keeps the workspace layout). */
|
|
50
|
+
const feEntry = (app) => `fe/apps/${app}/server.js`;
|
|
51
|
+
|
|
52
|
+
const instructionsOf = (stage, keyword) => stage.instructions.filter((item) => item.keyword === keyword);
|
|
53
|
+
/** The commands a stage's RUN instructions run, each as its words (shell form; an exec-form RUN is one command). */
|
|
54
|
+
function commandsOf(stage) {
|
|
55
|
+
return instructionsOf(stage, 'RUN').flatMap((run) => {
|
|
56
|
+
const exec = execForm(run.text);
|
|
57
|
+
if (exec) return [{ words: exec, line: run.line }];
|
|
58
|
+
return shellCommands(run.text.replace(/^(?:--\S+\s+)+/, '')).map((parts) => ({ words: parts, line: run.line }));
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/** `npm ci`, `npm install`, `npm run <script>`, ... as { tool, verb, args }. */
|
|
62
|
+
const npmCall = (parts) => (parts[0] === 'npm' ? { verb: parts[1] ?? '', args: parts.slice(2) } : null);
|
|
63
|
+
const isBuildCommand = (parts) => {
|
|
64
|
+
const call = npmCall(parts);
|
|
65
|
+
if (call) return call.verb === 'run' && String(call.args[0] ?? '').startsWith('build');
|
|
66
|
+
const tool = parts[0] === 'npx' ? parts[1] : parts[0];
|
|
67
|
+
return BUILD_TOOLS.has(tool);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** The environment `ENV` instructions of a stage set: name -> value (both `ENV A=1 B=2` and `ENV A 1`). */
|
|
71
|
+
function envOf(stage) {
|
|
72
|
+
const env = new Map();
|
|
73
|
+
for (const item of instructionsOf(stage, 'ENV')) {
|
|
74
|
+
const parts = words(item.text);
|
|
75
|
+
if (parts.length >= 2 && !parts[0].includes('=')) { env.set(parts[0], parts.slice(1).join(' ')); continue; }
|
|
76
|
+
for (const part of parts) { const at = part.indexOf('='); if (at > 0) env.set(part.slice(0, at), part.slice(at + 1)); }
|
|
77
|
+
}
|
|
78
|
+
return env;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** R187: the header states this app's build command, and the build never reaches outside its context. */
|
|
82
|
+
function contextFindings(file, parsed, side, app) {
|
|
83
|
+
const findings = [];
|
|
84
|
+
const wanted = dockerfilePath(side, app);
|
|
85
|
+
const named = parsed.comments.some((comment) => {
|
|
86
|
+
const parts = words(comment.text);
|
|
87
|
+
const build = parts.findIndex((part, index) => part === 'docker' && parts[index + 1] === 'build');
|
|
88
|
+
if (build < 0) return false;
|
|
89
|
+
const flag = parts.indexOf('-f', build);
|
|
90
|
+
return flag > 0 && parts[flag + 1] === wanted && parts.at(-1) === '.';
|
|
91
|
+
});
|
|
92
|
+
if (!named) findings.push(found(DOCKER_BUILD_CONTEXT, file, `${file} does not state its build command in a header comment: \`docker build -f ${wanted} -t <image> .\` (the context is the app root, the folder with the one package.json and lockfile).`, { expected: wanted }));
|
|
93
|
+
for (const item of parsed.instructions.filter((entry) => entry.keyword === 'COPY' || entry.keyword === 'ADD')) {
|
|
94
|
+
const parts = words(item.text).filter((part) => !part.startsWith('--'));
|
|
95
|
+
const sources = parts.slice(0, -1);
|
|
96
|
+
const fromStage = words(item.text).some((part) => part.startsWith('--from='));
|
|
97
|
+
if (fromStage) continue;
|
|
98
|
+
for (const source of sources) {
|
|
99
|
+
if (/^[a-z]+:\/\//i.test(source)) continue;
|
|
100
|
+
const segments = source.split('/');
|
|
101
|
+
if (source.startsWith('/') || segments.includes('..')) findings.push(found(DOCKER_BUILD_CONTEXT, file, `${file}:${item.line} ${item.keyword} source ${source} leaves the build context; every source is a path inside the app root.`, { line: item.line, source }));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return findings;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** R188: the stages and the install, the user and the build discipline of each. */
|
|
108
|
+
function stageFindings(file, parsed, side, kind) {
|
|
109
|
+
const findings = [];
|
|
110
|
+
const { stages } = parsed;
|
|
111
|
+
const names = stages.map((stage) => stage.name);
|
|
112
|
+
if (!names.includes(BUILD_STAGE) || stages.at(-1)?.name !== RUNTIME_STAGE || stages.length < 2) {
|
|
113
|
+
findings.push(found(DOCKER_STAGES, file, `${file} has stages ${JSON.stringify(names)}; an image is multi-stage: a stage named ${BUILD_STAGE} (installs and builds) and a last stage named ${RUNTIME_STAGE} (what ships).`, { stages: names }));
|
|
114
|
+
if (!stages.length) return findings;
|
|
115
|
+
}
|
|
116
|
+
for (const stage of stages) {
|
|
117
|
+
for (const command of commandsOf(stage)) {
|
|
118
|
+
const call = npmCall(command.words);
|
|
119
|
+
if (call && (call.verb === 'install' || call.verb === 'i') && !call.args.some((arg) => arg === '-g' || arg === '--global')) findings.push(found(DOCKER_STAGES, file, `${file}:${command.line} runs \`npm ${call.verb}\`; an image installs with \`npm ci\` from the lockfile.`, { line: command.line }));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const runtime = stages.at(-1);
|
|
123
|
+
if (runtime.name !== RUNTIME_STAGE) return findings;
|
|
124
|
+
for (const command of commandsOf(runtime)) {
|
|
125
|
+
if (isBuildCommand(command.words)) findings.push(found(DOCKER_STAGES, file, `${file}:${command.line} the ${RUNTIME_STAGE} stage runs the build (\`${command.words.join(' ')}\`); it only receives what the ${BUILD_STAGE} stage built.`, { line: command.line }));
|
|
126
|
+
}
|
|
127
|
+
const users = instructionsOf(runtime, 'USER').map((item) => words(item.text)[0]);
|
|
128
|
+
if (!users.length || users.at(-1) !== 'node') findings.push(found(DOCKER_STAGES, file, `${file} ${RUNTIME_STAGE} stage does not end as \`USER node\` (found ${JSON.stringify(users.at(-1) ?? 'root')}); the process runs unprivileged.`, { user: users.at(-1) ?? 'root' }));
|
|
129
|
+
const installs = commandsOf(runtime).map((command) => ({ call: npmCall(command.words), line: command.line })).filter((entry) => entry.call?.verb === 'ci');
|
|
130
|
+
if (side === 'fe' && installs.length) findings.push(found(DOCKER_STAGES, file, `${file}:${installs[0].line} the ${RUNTIME_STAGE} stage of a Next app installs nothing: it copies the standalone output, which carries its own dependencies.`, { line: installs[0].line }));
|
|
131
|
+
if (side === 'be' && !installs.some((entry) => entry.call.args.includes('--omit=dev') && entry.call.args.includes('--ignore-scripts'))) findings.push(found(DOCKER_STAGES, file, `${file} ${RUNTIME_STAGE} stage does not run \`npm ci --omit=dev --ignore-scripts\`; a back-end runtime holds production dependencies only and runs no lifecycle script.`, { kind }));
|
|
132
|
+
return findings;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The Next config's `output` property, read with TypeScript's parser: the string literal assigned to `output` in an object literal. */
|
|
136
|
+
function nextOutput(ts, text) {
|
|
137
|
+
return propertyText(ts, text, { file: 'next.config.ts', key: 'output' });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** R189: the entry, the port and the health of the runtime. */
|
|
141
|
+
function entryFindings({ repoRoot, file, parsed, side, app, kind, project, ts }) {
|
|
142
|
+
const findings = [];
|
|
143
|
+
const runtime = parsed.stages.at(-1);
|
|
144
|
+
if (!runtime || runtime.name !== RUNTIME_STAGE) return findings;
|
|
145
|
+
const entry = side === 'be' ? beEntry(app) : feEntry(app);
|
|
146
|
+
const starts = [...instructionsOf(runtime, 'ENTRYPOINT'), ...instructionsOf(runtime, 'CMD')].map((item) => execForm(item.text));
|
|
147
|
+
if (!starts.some((exec) => exec && exec[0] === 'node' && exec[1] === entry)) findings.push(found(DOCKER_ENTRY, file, `${file} ${RUNTIME_STAGE} stage does not start \`["node", "${entry}"]\` in exec form (CMD or ENTRYPOINT); the app runs its own built entry.`, { expected: entry }));
|
|
148
|
+
const exposes = instructionsOf(runtime, 'EXPOSE').flatMap((item) => words(item.text).map((port) => port.replace(/\/(tcp|udp)$/i, '')));
|
|
149
|
+
const health = instructionsOf(runtime, 'HEALTHCHECK').map((item) => words(item.text));
|
|
150
|
+
const none = health.some((parts) => parts[0]?.toUpperCase() === 'NONE');
|
|
151
|
+
const listens = side === 'fe' || BE_KINDS_WITH_LISTENER.has(kind);
|
|
152
|
+
if (listens) {
|
|
153
|
+
const port = envOf(runtime).get('PORT');
|
|
154
|
+
if (!port || !exposes.includes(port)) findings.push(found(DOCKER_ENTRY, file, `${file} ${RUNTIME_STAGE} stage must set \`ENV PORT=<port>\` and \`EXPOSE\` the same port (found PORT ${JSON.stringify(port ?? null)}, EXPOSE ${JSON.stringify(exposes)}); a listening app declares the port it serves.`, { port: port ?? null, exposes }));
|
|
155
|
+
if (!health.length || none) findings.push(found(DOCKER_ENTRY, file, `${file} ${RUNTIME_STAGE} stage has no HEALTHCHECK; an api or Next app answers a health probe the platform can run.`, {}));
|
|
156
|
+
} else if (BE_KINDS_ONE_SHOT.has(kind)) {
|
|
157
|
+
if (!none) findings.push(found(DOCKER_ENTRY, file, `${file} ${RUNTIME_STAGE} stage must say \`HEALTHCHECK NONE\`; a ${kind} app is a one-shot command, not a service.`, { kind }));
|
|
158
|
+
if (exposes.length) findings.push(found(DOCKER_ENTRY, file, `${file} ${RUNTIME_STAGE} stage EXPOSEs ${exposes.join(', ')}; a ${kind} app listens on nothing.`, { exposes }));
|
|
159
|
+
} else {
|
|
160
|
+
if (!health.length || none) findings.push(found(DOCKER_ENTRY, file, `${file} ${RUNTIME_STAGE} stage has no process HEALTHCHECK; a ${kind} app serves nothing, so its healthcheck probes its process.`, { kind }));
|
|
161
|
+
if (exposes.length) findings.push(found(DOCKER_ENTRY, file, `${file} ${RUNTIME_STAGE} stage EXPOSEs ${exposes.join(', ')}; a ${kind} app listens on nothing.`, { exposes }));
|
|
162
|
+
}
|
|
163
|
+
if (side === 'fe') {
|
|
164
|
+
const build = parsed.stages.find((stage) => stage.name === BUILD_STAGE);
|
|
165
|
+
const filter = `--filter=@${project}/${app}`;
|
|
166
|
+
const builds = build && commandsOf(build).some((command) => command.words.includes('turbo') && command.words.includes('build') && command.words.includes(filter));
|
|
167
|
+
if (!builds) findings.push(found(DOCKER_ENTRY, file, `${file} ${BUILD_STAGE} stage does not run \`turbo run build ${filter}\`; a Next image builds its own workspace (and the packages it imports) through turbo.`, { expected: filter }));
|
|
168
|
+
const config = `fe/apps/${app}/next.config.ts`;
|
|
169
|
+
const text = readText(repoRoot, config);
|
|
170
|
+
if (text !== null && ts && nextOutput(ts, text) !== 'standalone') findings.push(found(DOCKER_ENTRY, config, `${config} does not set \`output: "standalone"\`; the runtime stage of ${file} ships the standalone output.`, { expected: 'standalone' }));
|
|
171
|
+
}
|
|
172
|
+
return findings;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** The image reference of a FROM with its build arguments resolved against the ARG defaults before the first FROM; null when one cannot be. */
|
|
176
|
+
function resolvedImage(parsed, image) {
|
|
177
|
+
const defaults = new Map();
|
|
178
|
+
for (const item of parsed.preamble.filter((entry) => entry.keyword === 'ARG')) {
|
|
179
|
+
const [name, ...rest] = words(item.text)[0]?.split('=') ?? [];
|
|
180
|
+
if (name && rest.length) defaults.set(name, rest.join('='));
|
|
181
|
+
}
|
|
182
|
+
let unresolved = false;
|
|
183
|
+
const text = image.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (whole, braced, bare) => {
|
|
184
|
+
const value = defaults.get(braced ?? bare);
|
|
185
|
+
if (value === undefined) unresolved = true;
|
|
186
|
+
return value ?? whole;
|
|
187
|
+
});
|
|
188
|
+
return unresolved ? null : text;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** R190: every FROM is a prior stage, the canon node image, or an image pinned by digest. */
|
|
192
|
+
function pinFindings(file, parsed) {
|
|
193
|
+
const findings = [];
|
|
194
|
+
const earlier = new Set();
|
|
195
|
+
for (const stage of parsed.stages) {
|
|
196
|
+
const image = resolvedImage(parsed, stage.image);
|
|
197
|
+
const ok = image !== null && (earlier.has(image) || image === NODE_IMAGE || DIGEST.test(image));
|
|
198
|
+
if (!ok) findings.push(found(DOCKER_BASE_PIN, file, `${file}:${stage.line} stage ${stage.name ?? stage.index} starts FROM ${stage.image}; the base is ${NODE_IMAGE} (the one node image of the canon), a prior stage, or an image pinned by @sha256 digest. A moving tag changes the image without a change of this file.`, { line: stage.line, image: stage.image, expected: NODE_IMAGE }));
|
|
199
|
+
if (stage.name) earlier.add(stage.name);
|
|
200
|
+
}
|
|
201
|
+
return findings;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const isCredentialName = (name) => !name.startsWith('NEXT_PUBLIC_') && name.split('_').some((part) => CREDENTIAL_PARTS.has(part.toUpperCase()));
|
|
205
|
+
const isSecretPath = (source) => {
|
|
206
|
+
const segments = source.split('/').filter((part) => part && part !== '.');
|
|
207
|
+
const base = segments.at(-1) ?? '';
|
|
208
|
+
const env = base === '.env' || (base.startsWith('.env.') && base !== '.env.example');
|
|
209
|
+
return env || segments.some((segment) => SECRET_FOLDERS.has(segment)) || SECRET_SUFFIXES.some((suffix) => base.endsWith(suffix)) || base.startsWith('kubeconfig');
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
/** R191: no secret enters an image. */
|
|
213
|
+
function secretFindings(file, parsed) {
|
|
214
|
+
const findings = [];
|
|
215
|
+
for (const item of parsed.instructions) {
|
|
216
|
+
if (item.keyword === 'COPY' || item.keyword === 'ADD') {
|
|
217
|
+
const parts = words(item.text);
|
|
218
|
+
if (parts.some((part) => part.startsWith('--from='))) continue;
|
|
219
|
+
const sources = parts.filter((part) => !part.startsWith('--')).slice(0, -1);
|
|
220
|
+
for (const source of sources) {
|
|
221
|
+
if (/^https?:\/\//i.test(source)) findings.push(found(DOCKER_SECRETS, file, `${file}:${item.line} ADD of a URL (${source}); a download is a checksum-verified stage, never an ADD.`, { line: item.line, source }));
|
|
222
|
+
else if (isSecretPath(source)) findings.push(found(DOCKER_SECRETS, file, `${file}:${item.line} ${item.keyword} of ${source}, which holds secret material; credentials are mounted at run time, never copied into an image.`, { line: item.line, source }));
|
|
223
|
+
}
|
|
224
|
+
} else if (item.keyword === 'ARG' || item.keyword === 'ENV') {
|
|
225
|
+
const parts = words(item.text);
|
|
226
|
+
const names = item.keyword === 'ENV' && parts.length >= 2 && !parts[0].includes('=') ? [parts[0]] : parts.map((part) => part.split('=')[0]);
|
|
227
|
+
for (const name of names) if (isCredentialName(name)) findings.push(found(DOCKER_SECRETS, file, `${file}:${item.line} ${item.keyword} ${name} names a credential; an image carries none (an ARG or ENV stays in the image history). Mount it at run time; only NEXT_PUBLIC_* values, which are published by design, are build arguments.`, { line: item.line, name }));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return findings;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** The TypeScript compiler of the app (else of the runtime), or null. */
|
|
234
|
+
const typescriptFor = (repoRoot) => loadTypescript(repoRoot, HERE);
|
|
235
|
+
|
|
236
|
+
/** The findings of the docker rules over the tracked paths `files` (app-relative) of the app at `repoRoot`. */
|
|
237
|
+
export function dockerFindings({ repoRoot, files, repo }) {
|
|
238
|
+
if (!repo?.sides) return [];
|
|
239
|
+
const tracked = new Set(files);
|
|
240
|
+
const apps = [
|
|
241
|
+
...repo.sides.be.apps.map((entry) => ({ side: 'be', app: entry.name, kind: entry.kind })),
|
|
242
|
+
...repo.sides.fe.apps.map((entry) => ({ side: 'fe', app: entry.name, kind: entry.kind })),
|
|
243
|
+
];
|
|
244
|
+
const ts = apps.some((entry) => entry.side === 'fe') ? typescriptFor(repoRoot) : null;
|
|
245
|
+
return apps.flatMap(({ side, app, kind }) => {
|
|
246
|
+
const file = dockerfilePath(side, app);
|
|
247
|
+
if (!tracked.has(file)) return [];
|
|
248
|
+
const text = readText(repoRoot, file);
|
|
249
|
+
if (text === null) return [found(DOCKER_STAGES, file, `${file} is not readable text.`)];
|
|
250
|
+
const parsed = parseDockerfile(text);
|
|
251
|
+
return [
|
|
252
|
+
...contextFindings(file, parsed, side, app),
|
|
253
|
+
...stageFindings(file, parsed, side, kind),
|
|
254
|
+
...entryFindings({ repoRoot, file, parsed, side, app, kind, project: repo.project, ts }),
|
|
255
|
+
...pinFindings(file, parsed),
|
|
256
|
+
...secretFindings(file, parsed),
|
|
257
|
+
];
|
|
258
|
+
});
|
|
259
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// edition.mjs - L01, the edition rule. An hfs.json edition is `full` or `lite` (absent means full); under lite the standard is a
|
|
2
|
+
// filter plus a few additions over the same manifest, never a second engine. The slot layer already answers the paths lite
|
|
3
|
+
// removed (slots lite excludes, or whose litePresence is forbidden, come back HFS_FORBIDDEN_PRESENT with their goesTo); this
|
|
4
|
+
// rule reports what no path can state:
|
|
5
|
+
// - inside a package.json: a test script (`test`, `test:*`, `typecheck:tests`, a jest/vitest/playwright/mocha/cypress or
|
|
6
|
+
// `node --test` command) or a jest/vitest/playwright dependency - the same vocabulary FE_NO_TESTS scans on the fe side;
|
|
7
|
+
// - a test-tool configuration file no more specific slot already forbids;
|
|
8
|
+
// - a declaration of what lite does not have: a worker app, an event pattern (event-bus, queue, fenced-job, projection,
|
|
9
|
+
// saga) or its trigger kinds (saga, reactors, jobs, realtime). The forbidden names are DERIVED - a pattern, trigger or
|
|
10
|
+
// app kind is gone under lite exactly when every slot that carries it is litePresence: forbidden - so adding or
|
|
11
|
+
// restoring a kind is a manifest edit, not an edit here.
|
|
12
|
+
// The edition is never tested here: a full app's view forbids none of these names and keeps its tests, so the rule answers [].
|
|
13
|
+
import { found, readJson } from './read.mjs';
|
|
14
|
+
import { DEPENDENCY_SECTIONS, TEST_DEPENDENCY, TEST_RUNNER_COMMAND, TEST_SCRIPT_NAME, TEST_TOOL_FILE } from './fe-no-tests.mjs';
|
|
15
|
+
|
|
16
|
+
export const HFS_EDITION_FORBIDDEN_PRESENT = 'HFS_EDITION_FORBIDDEN_PRESENT';
|
|
17
|
+
|
|
18
|
+
const PACKAGE_JSON = /(?:^|\/)package\.json$/;
|
|
19
|
+
const TYPECHECK_TESTS = /^(?:pre|post)?typecheck:tests$/;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The names the edition cannot declare: for each of `pattern`, `trigger` and `appKind`, the value is gone when every slot of
|
|
23
|
+
* the resolver's view that carries it is forbidden there. The view is the edition's own (full forbids none of them), so the
|
|
24
|
+
* rule needs no edition test: a full app has nothing gone.
|
|
25
|
+
*/
|
|
26
|
+
function editionGone(resolver) {
|
|
27
|
+
const tally = new Map();
|
|
28
|
+
for (const slot of resolver.slots()) {
|
|
29
|
+
const forbidden = slot.presence === 'forbidden';
|
|
30
|
+
for (const field of ['pattern', 'trigger', 'appKind']) {
|
|
31
|
+
if (slot[field] === undefined) continue;
|
|
32
|
+
const key = `${field}:${slot[field]}`;
|
|
33
|
+
const entry = tally.get(key) ?? { total: 0, forbidden: 0 };
|
|
34
|
+
entry.total += 1;
|
|
35
|
+
entry.forbidden += forbidden ? 1 : 0;
|
|
36
|
+
tally.set(key, entry);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const gone = { pattern: new Set(), trigger: new Set(), appKind: new Set() };
|
|
40
|
+
for (const [key, entry] of tally) {
|
|
41
|
+
if (entry.total !== entry.forbidden) continue;
|
|
42
|
+
const [field, name] = key.split(':');
|
|
43
|
+
gone[field].add(name);
|
|
44
|
+
}
|
|
45
|
+
return gone;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The findings one package.json's scripts and dependencies produce under lite. */
|
|
49
|
+
function packageFindings(repoRoot, rel) {
|
|
50
|
+
const pkg = readJson(repoRoot, rel);
|
|
51
|
+
if (pkg === null || typeof pkg !== 'object' || Array.isArray(pkg)) return [];
|
|
52
|
+
const findings = [];
|
|
53
|
+
const scripts = typeof pkg.scripts === 'object' && pkg.scripts !== null && !Array.isArray(pkg.scripts) ? pkg.scripts : {};
|
|
54
|
+
for (const [name, command] of Object.entries(scripts)) {
|
|
55
|
+
if (TEST_SCRIPT_NAME.test(name) || TYPECHECK_TESTS.test(name) || TEST_RUNNER_COMMAND.test(String(command))) {
|
|
56
|
+
findings.push(found(HFS_EDITION_FORBIDDEN_PRESENT, rel, `${rel} has script ${JSON.stringify(name)}: the lite edition has no tests - a lite app does not typecheck or run a test world`, { name }));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
for (const section of DEPENDENCY_SECTIONS) {
|
|
60
|
+
const deps = pkg[section];
|
|
61
|
+
if (typeof deps !== 'object' || deps === null || Array.isArray(deps)) continue;
|
|
62
|
+
for (const name of Object.keys(deps)) {
|
|
63
|
+
if (TEST_DEPENDENCY.test(name)) {
|
|
64
|
+
findings.push(found(HFS_EDITION_FORBIDDEN_PRESENT, rel, `${rel} ${section} has ${name}: the lite edition has no tests - the dependency belongs to the full edition`, { section, name }));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return findings;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The findings one be-side declaration produces under lite: a worker app, an event pattern or a gone trigger kind. */
|
|
72
|
+
function declarationFindings(repo, gone) {
|
|
73
|
+
const findings = [];
|
|
74
|
+
for (const app of repo.apps ?? []) {
|
|
75
|
+
if (gone.appKind.has(app.kind)) {
|
|
76
|
+
findings.push(found(HFS_EDITION_FORBIDDEN_PRESENT, 'hfs.json', `hfs.json declares the ${app.kind} app ${app.name}: a ${app.kind} belongs to the full edition, a lite app has none`, { app: app.name, kind: app.kind }));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
for (const [field, goneNames] of [['pattern', gone.pattern], ['trigger', gone.trigger]]) {
|
|
80
|
+
const declared = field === 'pattern' ? (repo.patterns ?? []) : (repo.kinds ?? []);
|
|
81
|
+
for (const name of declared) {
|
|
82
|
+
if (goneNames.has(name)) {
|
|
83
|
+
findings.push(found(HFS_EDITION_FORBIDDEN_PRESENT, 'hfs.json', `hfs.json ${field === 'pattern' ? 'declares' : 'names'} the ${name} ${field}: ${name} belongs to the full edition, a lite app has none`, { name }));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return findings;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* L01 for one scope (the app root or one side), over `files` (scope-relative). Both questions are answered by the
|
|
92
|
+
* resolver's view of the edition: the names gone from it, and whether any slot of it still has tests. The declaration
|
|
93
|
+
* findings are judged from the be side (its apps, patterns and kinds): the root scope looks at sides.be, a standalone
|
|
94
|
+
* be-side check at its own view. `withDeclaration: false` (a side scope of a whole-app check) skips them: the root scope
|
|
95
|
+
* already reported them on hfs.json, and the side scope would only repeat them on a path that is not this side's.
|
|
96
|
+
*/
|
|
97
|
+
export function editionFindings({ repoRoot, files, repo, resolver, withDeclaration = true }) {
|
|
98
|
+
const gone = editionGone(resolver);
|
|
99
|
+
const noTestWorld = resolver.slots().every((slot) => slot.tests === 'none');
|
|
100
|
+
const beView = !withDeclaration ? null : (repo.profile === 'app' ? repo.sides?.be : (repo.profile === 'be' ? repo : null));
|
|
101
|
+
const findings = beView ? declarationFindings(beView, gone) : [];
|
|
102
|
+
for (const file of noTestWorld ? files : []) {
|
|
103
|
+
if (file.includes('node_modules/')) continue;
|
|
104
|
+
if (PACKAGE_JSON.test(file)) {
|
|
105
|
+
findings.push(...packageFindings(repoRoot, file));
|
|
106
|
+
} else if (TEST_TOOL_FILE.test(file) && resolver.classifyPath(file).status !== 'forbidden') {
|
|
107
|
+
findings.push(found(HFS_EDITION_FORBIDDEN_PRESENT, file, `${file} is test tooling: the lite edition has no tests - the file belongs to the full edition`));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return findings;
|
|
111
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// event-bus.mjs - the tree checks of the declared patterns (hfs.json sides.be.patterns).
|
|
2
|
+
// BE_EVENT_CLASS_CONTRACT every typed event class `be/src/modules/events/<service>/<event>.event.ts` equals an entry of the vendored
|
|
3
|
+
// contract `be/contracts/<service>/events.json` (the same name and version), every entry of a vendored
|
|
4
|
+
// contract has its class, and a class file is named after its event (`order.placed` -> `order-placed.event.ts`);
|
|
5
|
+
// BE_PATTERN_SPEC_MISSING a declared pattern has its required scenarios: ruleParams.be.patternScenarios.<pattern> lists scenario ids,
|
|
6
|
+
// and an e2e or integration spec of the back end holds a test whose title starts `<pattern>/<scenario>:`.
|
|
7
|
+
// Class members are read with TypeScript's own parser (a `static readonly eventName` and `version` with literal initializers), spec
|
|
8
|
+
// titles are the first argument of `it(` and `test(` calls; no source text is matched by a regular expression.
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { loadSlotManifest, ruleParams } from '../slots.mjs';
|
|
12
|
+
import { loadTypescript } from '../../lib/package-at.mjs';
|
|
13
|
+
import { literalText } from '../../lib/ts-ast.mjs';
|
|
14
|
+
import { found, readJson, readText } from './read.mjs';
|
|
15
|
+
|
|
16
|
+
const EVENT_CLASS_CONTRACT = 'BE_EVENT_CLASS_CONTRACT';
|
|
17
|
+
const PATTERN_SPEC_MISSING = 'BE_PATTERN_SPEC_MISSING';
|
|
18
|
+
|
|
19
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const EVENT_FILE_PREFIX = 'be/src/modules/events/';
|
|
21
|
+
const EVENT_SUFFIX = '.event.ts';
|
|
22
|
+
const CONTRACT_PREFIX = 'be/contracts/';
|
|
23
|
+
const CONTRACT_NAME = 'events.json';
|
|
24
|
+
const TEST_FOLDERS = ['be/src/tests/e2e/', 'be/src/tests/integration/'];
|
|
25
|
+
const SPEC_SUFFIXES = ['.e2e-spec.ts', '.integration-spec.ts'];
|
|
26
|
+
const TEST_CALLEES = new Set(['it', 'test']);
|
|
27
|
+
|
|
28
|
+
/** The TypeScript compiler of the app (else of the runtime), or null. */
|
|
29
|
+
const typescriptFor = (repoRoot) => loadTypescript(repoRoot, HERE);
|
|
30
|
+
|
|
31
|
+
/** The event-bus pattern file name of an event name: dots become dashes (`order.placed` -> `order-placed`). */
|
|
32
|
+
export const stemOfEvent = (name) => name.split('.').join('-');
|
|
33
|
+
|
|
34
|
+
/** The `eventName` and `version` literals a class declares as `static readonly` members, with the line of the class. */
|
|
35
|
+
function readEventClass(ts, text, file) {
|
|
36
|
+
const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
|
|
37
|
+
const classes = [];
|
|
38
|
+
const visit = (node) => {
|
|
39
|
+
if (ts.isClassDeclaration(node)) {
|
|
40
|
+
let name = null;
|
|
41
|
+
let version = null;
|
|
42
|
+
for (const member of node.members) {
|
|
43
|
+
if (!ts.isPropertyDeclaration(member) || !member.initializer || !member.name || !ts.isIdentifier(member.name)) continue;
|
|
44
|
+
const isStatic = member.modifiers?.some((m) => m.kind === ts.SyntaxKind.StaticKeyword);
|
|
45
|
+
if (!isStatic) continue;
|
|
46
|
+
let value = member.initializer;
|
|
47
|
+
while (ts.isAsExpression(value) || ts.isParenthesizedExpression(value)) value = value.expression;
|
|
48
|
+
if (member.name.text === 'eventName') name = literalText(ts, value);
|
|
49
|
+
if (member.name.text === 'version' && ts.isNumericLiteral(value)) version = Number(value.text);
|
|
50
|
+
}
|
|
51
|
+
if (name !== null || version !== null || node.name) classes.push({ className: node.name?.text ?? '<anonymous>', name, version });
|
|
52
|
+
}
|
|
53
|
+
ts.forEachChild(node, visit);
|
|
54
|
+
};
|
|
55
|
+
visit(source);
|
|
56
|
+
return classes;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Findings of BE_EVENT_CLASS_CONTRACT. */
|
|
60
|
+
export function eventClassContractFindings({ repoRoot, files, repo }) {
|
|
61
|
+
const patterns = repo.sides?.be?.patterns ?? [];
|
|
62
|
+
if (!patterns.includes('event-bus')) return [];
|
|
63
|
+
const ts = typescriptFor(repoRoot);
|
|
64
|
+
if (!ts) return [];
|
|
65
|
+
const findings = [];
|
|
66
|
+
const classesByService = new Map();
|
|
67
|
+
for (const file of files) {
|
|
68
|
+
if (!file.startsWith(EVENT_FILE_PREFIX) || !file.endsWith(EVENT_SUFFIX)) continue;
|
|
69
|
+
const rest = file.slice(EVENT_FILE_PREFIX.length).split('/');
|
|
70
|
+
if (rest.length !== 2) continue;
|
|
71
|
+
const [service, base] = rest;
|
|
72
|
+
const stem = base.slice(0, -EVENT_SUFFIX.length);
|
|
73
|
+
const text = readText(repoRoot, file);
|
|
74
|
+
if (text === null) continue;
|
|
75
|
+
const classes = readEventClass(ts, text, file).filter((c) => c.name !== null || c.version !== null);
|
|
76
|
+
if (classes.length !== 1 || classes[0].name === null || classes[0].version === null) {
|
|
77
|
+
findings.push(found(EVENT_CLASS_CONTRACT, file, `${file} must declare exactly one event class with literal \`static readonly eventName\` and \`static readonly version\` members (the typed event of be/contracts/${service}/${CONTRACT_NAME}); found ${classes.length === 1 ? 'one without both literals' : `${classes.length} candidates`}.`, { service }));
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const [event] = classes;
|
|
81
|
+
if (stemOfEvent(event.name) !== stem) {
|
|
82
|
+
findings.push(found(EVENT_CLASS_CONTRACT, file, `${file} declares the event \`${event.name}\`, so the file is \`${stemOfEvent(event.name)}${EVENT_SUFFIX}\`: a class file is named after its event.`, { service, event: event.name }));
|
|
83
|
+
}
|
|
84
|
+
if (!classesByService.has(service)) classesByService.set(service, new Map());
|
|
85
|
+
classesByService.get(service).set(event.name, { file, version: event.version });
|
|
86
|
+
}
|
|
87
|
+
const contracts = new Map();
|
|
88
|
+
for (const file of files) {
|
|
89
|
+
if (!file.startsWith(CONTRACT_PREFIX) || !file.endsWith(`/${CONTRACT_NAME}`)) continue;
|
|
90
|
+
const rest = file.slice(CONTRACT_PREFIX.length).split('/');
|
|
91
|
+
if (rest.length !== 2) continue;
|
|
92
|
+
contracts.set(rest[0], { file, doc: readJson(repoRoot, file) });
|
|
93
|
+
}
|
|
94
|
+
for (const [service, byName] of classesByService) {
|
|
95
|
+
const contract = contracts.get(service);
|
|
96
|
+
for (const [name, { file, version }] of byName) {
|
|
97
|
+
const entry = contract?.doc?.events?.[name];
|
|
98
|
+
if (!contract) findings.push(found(EVENT_CLASS_CONTRACT, file, `${file} declares the event \`${name}\` but ${CONTRACT_PREFIX}${service}/${CONTRACT_NAME} is not tracked; every event class is an entry of the vendored contract of its service.`, { service, event: name }));
|
|
99
|
+
else if (entry === undefined) findings.push(found(EVENT_CLASS_CONTRACT, file, `${file} declares the event \`${name}\` which ${contract.file} does not list; emit the contract (\`starci app emit\`) so the class and the contract agree.`, { service, event: name }));
|
|
100
|
+
else if (entry.version !== version) findings.push(found(EVENT_CLASS_CONTRACT, file, `${file} declares \`${name}\` at version ${version} but ${contract.file} lists version ${entry.version}; a version changes in the class and the contract together.`, { service, event: name }));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
for (const [service, contract] of contracts) {
|
|
104
|
+
const events = contract.doc?.events;
|
|
105
|
+
if (events === null || typeof events !== 'object') continue;
|
|
106
|
+
for (const name of Object.keys(events)) {
|
|
107
|
+
if (!classesByService.get(service)?.has(name)) findings.push(found(EVENT_CLASS_CONTRACT, contract.file, `${contract.file} lists the event \`${name}\` but ${EVENT_FILE_PREFIX}${service}/${stemOfEvent(name)}${EVENT_SUFFIX} declares no such class; every contract entry has its typed event class.`, { service, event: name }));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return findings;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The scenario ids a back end proves: every `<pattern>/<scenario>:` test title of an e2e or integration spec. */
|
|
114
|
+
function provenScenarios(ts, repoRoot, files) {
|
|
115
|
+
const proven = new Set();
|
|
116
|
+
for (const file of files) {
|
|
117
|
+
if (!TEST_FOLDERS.some((folder) => file.startsWith(folder)) || !SPEC_SUFFIXES.some((suffix) => file.endsWith(suffix))) continue;
|
|
118
|
+
const text = readText(repoRoot, file);
|
|
119
|
+
if (text === null) continue;
|
|
120
|
+
const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
|
|
121
|
+
const visit = (node) => {
|
|
122
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && TEST_CALLEES.has(node.expression.text)) {
|
|
123
|
+
const title = literalText(ts, node.arguments[0]);
|
|
124
|
+
const colon = title === null ? -1 : title.indexOf(':');
|
|
125
|
+
if (colon > 0) proven.add(title.slice(0, colon));
|
|
126
|
+
}
|
|
127
|
+
ts.forEachChild(node, visit);
|
|
128
|
+
};
|
|
129
|
+
visit(source);
|
|
130
|
+
}
|
|
131
|
+
return proven;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let memoScenarios;
|
|
135
|
+
/** ruleParams.be.patternScenarios of the slot manifest this runtime ships. */
|
|
136
|
+
const scenariosTable = () => (memoScenarios ??= ruleParams(loadSlotManifest(), 'be').patternScenarios ?? {});
|
|
137
|
+
|
|
138
|
+
/** Findings of BE_PATTERN_SPEC_MISSING: each declared pattern's required scenarios have a test. */
|
|
139
|
+
export function patternSpecFindings({ repoRoot, files, repo, scenarios = scenariosTable() }) {
|
|
140
|
+
const patterns = (repo.sides?.be?.patterns ?? []).filter((name) => (scenarios[name] ?? []).length > 0);
|
|
141
|
+
if (patterns.length === 0) return [];
|
|
142
|
+
const ts = typescriptFor(repoRoot);
|
|
143
|
+
if (!ts) return [];
|
|
144
|
+
const proven = provenScenarios(ts, repoRoot, files);
|
|
145
|
+
const findings = [];
|
|
146
|
+
for (const pattern of patterns) {
|
|
147
|
+
for (const scenario of scenarios[pattern]) {
|
|
148
|
+
const id = `${pattern}/${scenario}`;
|
|
149
|
+
if (!proven.has(id)) findings.push(found(PATTERN_SPEC_MISSING, 'hfs.json', `The pattern \`${pattern}\` is declared but no e2e or integration spec (be/src/tests/e2e, be/src/tests/integration) holds a test titled \`${id}: ...\`; each scenario of a declared pattern is proven on the test world.`, { pattern, scenario: id }));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return findings;
|
|
153
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// fe-contract-documents.mjs - FE_GRAPHQL_CONTRACT (R113): every GraphQL document a front end sends is one its back end
|
|
2
|
+
// serves. An app keeps the contract of each GraphQL service it runs as a snapshot, `be/contracts/<service>/schema.graphql`
|
|
3
|
+
// (`starci app emit`, HFS_CONTRACT_SNAPSHOT_DRIFT keeps it current), and its front end keeps every operation it sends
|
|
4
|
+
// in a `.graphql` file under `fe/`. Each operation is judged against the contract of the service that serves its first
|
|
5
|
+
// root field (the one snapshot whose root type declares it): a root field no contract declares, a field, argument or input
|
|
6
|
+
// field the contract does not declare, a required argument left out, a variable of another type, a leaf with a selection or
|
|
7
|
+
// an object without one is a finding on the document. The finding names the service and every problem.
|
|
8
|
+
import { operationProblems, parseDocument, parseSchema, rootFieldsOf } from '../../lib/graphql-contract.mjs';
|
|
9
|
+
import { found, readText } from './read.mjs';
|
|
10
|
+
|
|
11
|
+
export const FE_GRAPHQL_CONTRACT = 'FE_GRAPHQL_CONTRACT';
|
|
12
|
+
|
|
13
|
+
const CONTRACT = /^be\/contracts\/([^/]+)\/schema\.graphql$/;
|
|
14
|
+
const isDocument = (file) => file.startsWith('fe/') && file.endsWith('.graphql') && !file.includes('/__generated__/') && !file.includes('/node_modules/');
|
|
15
|
+
|
|
16
|
+
/** The parsed contracts of the app: [{ service, path, schema }], or a finding for a snapshot that does not parse. */
|
|
17
|
+
function contractsOf(repoRoot, files, findings) {
|
|
18
|
+
const contracts = [];
|
|
19
|
+
for (const file of files) {
|
|
20
|
+
const match = CONTRACT.exec(file);
|
|
21
|
+
if (!match) continue;
|
|
22
|
+
const text = readText(repoRoot, file);
|
|
23
|
+
if (text === null) continue;
|
|
24
|
+
try {
|
|
25
|
+
contracts.push({ service: match[1], path: file, schema: parseSchema(text) });
|
|
26
|
+
} catch (error) {
|
|
27
|
+
findings.push(found(FE_GRAPHQL_CONTRACT, file, `${file} is not a GraphQL schema the front-end documents can be judged against: ${error.message}. Run \`npm run contract:emit\`.`));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return contracts;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The findings of R113 over the tracked paths `files` of the app at `repoRoot`. */
|
|
34
|
+
export function feContractFindings({ repoRoot, files }) {
|
|
35
|
+
const documents = files.filter(isDocument).sort();
|
|
36
|
+
if (documents.length === 0) return [];
|
|
37
|
+
const findings = [];
|
|
38
|
+
const contracts = contractsOf(repoRoot, files, findings);
|
|
39
|
+
for (const file of documents) {
|
|
40
|
+
const text = readText(repoRoot, file);
|
|
41
|
+
if (text === null) continue;
|
|
42
|
+
let document;
|
|
43
|
+
try {
|
|
44
|
+
document = parseDocument(text);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
findings.push(found(FE_GRAPHQL_CONTRACT, file, `${file} is not a GraphQL document: ${error.message}.`));
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
for (const operation of document.operations) {
|
|
50
|
+
const label = operation.name ?? `anonymous ${operation.operation}`;
|
|
51
|
+
const [first] = rootFieldsOf(operation);
|
|
52
|
+
const serving = contracts.filter(({ schema }) => schema.types.get(schema.roots[operation.operation])?.fields?.has(first));
|
|
53
|
+
if (serving.length === 0) {
|
|
54
|
+
const where = contracts.length === 0 ? 'the app keeps no be/contracts/<service>/schema.graphql' : `no contract of ${contracts.map((c) => c.path).join(', ')} declares ${operation.operation} ${first}`;
|
|
55
|
+
findings.push(found(FE_GRAPHQL_CONTRACT, file, `${file}: ${label} asks ${operation.operation} ${first}, but ${where}; the front end sends only what a back end serves.`, { operation: label, service: null }));
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const [contract] = serving;
|
|
59
|
+
const problems = operationProblems(contract.schema, document, operation);
|
|
60
|
+
if (problems.length > 0) {
|
|
61
|
+
findings.push(found(FE_GRAPHQL_CONTRACT, file, `${file}: ${label} does not match the ${contract.service} contract (${contract.path}): ${problems.join('; ')}.`, { operation: label, service: contract.service, problems }));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return findings;
|
|
66
|
+
}
|
|
@@ -12,11 +12,12 @@ import { found, readJson } from './read.mjs';
|
|
|
12
12
|
export const FE_NO_TESTS = 'FE_NO_TESTS';
|
|
13
13
|
const SPEC_FILE = /(?:\.(?:spec|test)|-spec)\.[cm]?[jt]sx?$/;
|
|
14
14
|
const TEST_DIRECTORY = /(?:^|\/)(?:e2e|__tests__|__mocks__|test-support)\//;
|
|
15
|
-
const TEST_TOOL_FILE = /(?:^|\/)(?:(?:vitest|jest|playwright)\.[^/]+|cypress\.config\.[^/]+|tsconfig\.e2e\.json|e2e\.ya?ml)$/;
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
15
|
+
export const TEST_TOOL_FILE = /(?:^|\/)(?:(?:vitest|jest|playwright)\.[^/]+|cypress\.config\.[^/]+|tsconfig\.e2e\.json|e2e\.ya?ml)$/;
|
|
16
|
+
// The edition rule (rules/edition.mjs, L01) applies the same lists to the whole app tree in lite: they are the test vocabulary.
|
|
17
|
+
export const TEST_SCRIPT_NAME = /^(?:pre|post)?test(?::|$)/;
|
|
18
|
+
export const TEST_RUNNER_COMMAND = /(?:^|[\s&|;(])(?:npx\s+)?(?:vitest|jest|playwright|cypress|mocha)(?=$|[\s&|;)])|\bnode\s+(?:--\S+\s+)*--test\b/;
|
|
19
|
+
export const TEST_DEPENDENCY = /^(?:vitest|@vitest\/.+|playwright|playwright-core|@playwright\/.+|jest|@jest\/.+|ts-jest|babel-jest|@types\/jest|jest-[\w-]+|mocha|@types\/mocha|cypress|@testing-library\/.+|jsdom|@types\/jsdom|happy-dom|axe-core|@axe-core\/.+|vitest-axe|@starci\/(?:vitest|jest|playwright)-preset)$/;
|
|
20
|
+
export const DEPENDENCY_SECTIONS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
|
|
20
21
|
|
|
21
22
|
/** True for a path FE_NO_TESTS owns: a spec file, a test directory or a test-tool file. */
|
|
22
23
|
export const isFeTestPath = (file) => SPEC_FILE.test(file) || TEST_DIRECTORY.test(file) || TEST_TOOL_FILE.test(file);
|