@starci/hfs 1.0.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/README.md +110 -12
- package/bin/hfs.mjs +119 -15
- package/emit/compiler.mjs +35 -0
- package/emit/contracts.mjs +97 -0
- package/emit/operations-worker.mjs +24 -0
- package/emit/operations.mjs +126 -0
- package/emit/schema-worker.mjs +117 -0
- package/emit/static-graph.mjs +670 -0
- package/emit/type-schema.mjs +145 -0
- package/package.json +4 -1
- package/report/sonar.mjs +180 -0
- package/runtime/engine/admission.mjs +284 -0
- package/runtime/engine/digest.mjs +10 -0
- package/runtime/engine/ledger-db.mjs +1245 -0
- package/runtime/engine/machine-db.mjs +1484 -0
- package/runtime/engine/migrations/machine/0001-init.sql +887 -0
- package/runtime/engine/migrations/runtime/0001-init.sql +1072 -0
- package/runtime/engine/migrations/runtime/0003-usage-unavailable.sql +13 -0
- package/runtime/engine/migrations/runtime/0004-attempt-why.sql +43 -0
- package/runtime/engine/plain-object.mjs +5 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +16 -58
- package/runtime/knowledge/hfs/slots.yaml +405 -137
- package/runtime/knowledge/patterns/fe/folder.yaml +309 -0
- package/runtime/knowledge/sonar-gate.yaml +85 -0
- package/runtime/modules/kernel/failure-codes.yaml +1480 -16
- package/runtime/scripts/checks/architecture/backend.mjs +350 -0
- package/runtime/scripts/checks/architecture/background-unowned.mjs +107 -0
- package/runtime/scripts/checks/architecture/client-reaches-server.mjs +94 -0
- package/runtime/scripts/checks/architecture/clones.mjs +200 -0
- package/runtime/scripts/checks/architecture/config-unread.mjs +35 -0
- package/runtime/scripts/checks/architecture/config.mjs +310 -0
- package/runtime/scripts/checks/architecture/connection-map.mjs +208 -0
- package/runtime/scripts/checks/architecture/constructor-deps.mjs +100 -0
- package/runtime/scripts/checks/architecture/contract-fixture-guard.mjs +128 -0
- package/runtime/scripts/checks/architecture/contracts.mjs +792 -0
- package/runtime/scripts/checks/architecture/cross-app-duplicate.mjs +73 -0
- package/runtime/scripts/checks/architecture/dead-exports.mjs +265 -0
- package/runtime/scripts/checks/architecture/default-deny.mjs +129 -0
- package/runtime/scripts/checks/architecture/doc-language.mjs +39 -0
- package/runtime/scripts/checks/architecture/entrypoint.mjs +57 -0
- package/runtime/scripts/checks/architecture/error-codes.mjs +45 -0
- package/runtime/scripts/checks/architecture/error-masked.mjs +60 -0
- package/runtime/scripts/checks/architecture/fe-slot-allows.mjs +38 -0
- package/runtime/scripts/checks/architecture/feature-shape.mjs +49 -0
- package/runtime/scripts/checks/architecture/framework-pinned.mjs +83 -0
- package/runtime/scripts/checks/architecture/frontend.mjs +995 -0
- package/runtime/scripts/checks/architecture/hfs-graph.mjs +61 -0
- package/runtime/scripts/checks/architecture/hfs.mjs +521 -0
- package/runtime/scripts/checks/architecture/hooks-are-hooks.mjs +88 -0
- package/runtime/scripts/checks/architecture/i18n-keys.mjs +146 -0
- package/runtime/scripts/checks/architecture/index.mjs +316 -0
- package/runtime/scripts/checks/architecture/injection-token-exported.mjs +62 -0
- package/runtime/scripts/checks/architecture/machine-ast.mjs +138 -0
- package/runtime/scripts/checks/architecture/module-per-transport.mjs +130 -0
- package/runtime/scripts/checks/architecture/next-data.mjs +775 -0
- package/runtime/scripts/checks/architecture/owners.mjs +89 -0
- package/runtime/scripts/checks/architecture/package-shape.mjs +63 -0
- package/runtime/scripts/checks/architecture/reachability.mjs +233 -0
- package/runtime/scripts/checks/architecture/register-once.mjs +141 -0
- package/runtime/scripts/checks/architecture/registration.mjs +319 -0
- package/runtime/scripts/checks/architecture/required-files.mjs +172 -0
- package/runtime/scripts/checks/architecture/route-files-thin.mjs +97 -0
- package/runtime/scripts/checks/architecture/schema-owner.mjs +261 -0
- package/runtime/scripts/checks/architecture/size-growth.mjs +73 -0
- package/runtime/scripts/checks/architecture/source-names.mjs +607 -0
- package/runtime/scripts/checks/architecture/sql-owner.mjs +142 -0
- package/runtime/scripts/checks/architecture/sql-tokens.mjs +327 -0
- package/runtime/scripts/checks/architecture/symbols.mjs +193 -0
- package/runtime/scripts/checks/architecture/test-world-files.mjs +163 -0
- package/runtime/scripts/checks/architecture/tiers.mjs +130 -0
- package/runtime/scripts/checks/architecture/transport-owner.mjs +112 -0
- package/runtime/scripts/checks/architecture/typescript.mjs +500 -0
- package/runtime/scripts/checks/architecture/unit-spec-providers.mjs +122 -0
- package/runtime/scripts/checks/architecture.mjs +41 -0
- package/runtime/scripts/checks/common.mjs +37 -0
- package/runtime/scripts/checks/typescript-programs.mjs +82 -0
- package/runtime/scripts/lib/artifact-hold.mjs +89 -0
- package/runtime/scripts/lib/artifact-store.mjs +103 -0
- package/runtime/scripts/lib/fs-kind.mjs +10 -0
- package/runtime/scripts/lib/git.mjs +53 -0
- package/runtime/scripts/lib/hfs-allows.mjs +57 -0
- package/runtime/scripts/lib/hfs-check.mjs +254 -28
- package/runtime/scripts/lib/hfs-rules/contract.mjs +126 -0
- package/runtime/scripts/lib/hfs-rules/deps.mjs +63 -0
- package/runtime/scripts/lib/hfs-rules/fe-no-tests.mjs +47 -0
- package/runtime/scripts/lib/hfs-rules/frontend.mjs +124 -0
- package/runtime/scripts/lib/hfs-rules/lint-suppression.mjs +34 -0
- package/runtime/scripts/lib/hfs-rules/pipeline.mjs +51 -0
- package/runtime/scripts/lib/hfs-rules/proof-commands.mjs +64 -0
- package/runtime/scripts/lib/hfs-rules/read.mjs +28 -0
- package/runtime/scripts/lib/hfs-rules/repo-local-checks.mjs +32 -0
- package/runtime/scripts/lib/hfs-rules/secrets.mjs +54 -0
- package/runtime/scripts/lib/hfs-rules/spec-placement.mjs +31 -0
- package/runtime/scripts/lib/hfs-rules/stacks.mjs +54 -0
- package/runtime/scripts/lib/hfs-rules/test-topology.mjs +31 -0
- package/runtime/scripts/lib/hfs-slots.mjs +88 -40
- package/runtime/scripts/lib/hfs-tree.mjs +80 -0
- package/runtime/scripts/lib/hfs-view.mjs +68 -0
- package/runtime/scripts/lib/json.mjs +22 -0
- package/runtime/scripts/lib/language.mjs +107 -0
- package/runtime/scripts/lib/path-key.mjs +2 -0
- package/runtime/scripts/lib/redact.mjs +148 -0
- package/runtime/scripts/lib/repo-identity.mjs +50 -0
- package/runtime/scripts/lib/safe-remove.mjs +179 -0
- package/runtime/scripts/lib/secret-patterns.mjs +44 -0
- package/runtime/scripts/lib/sleep-sync.mjs +17 -0
- package/runtime/scripts/lib/stack-declaration.mjs +52 -0
- package/runtime/scripts/lib/stack-services.mjs +217 -0
- package/runtime/scripts/lib/test-secrets.mjs +120 -0
- package/scaffold/service.mjs +333 -0
- package/sync/format.mjs +46 -0
- package/sync/hygiene.mjs +56 -24
- package/sync/index.mjs +126 -41
- package/sync/managed.mjs +170 -0
- package/sync/skeleton.mjs +32 -10
- package/sync/sonar-key.mjs +13 -0
- package/sync/ts-strict.mjs +48 -0
- package/templates/{common → be/ci-workflows/github/workflows}/ci.yml +13 -13
- package/templates/be/{e2e.yml → ci-workflows/github/workflows/e2e.yml} +1 -0
- package/templates/be/hooks/husky/pre-commit +13 -0
- package/templates/be/hooks/husky/pre-push +7 -0
- package/templates/be/package-scripts/package.json +21 -0
- package/templates/be/{sonar-project.properties → quality-config/sonar-project.properties} +2 -3
- package/templates/be/skeleton/apps/__app__/src/app.module.ts +14 -4
- package/templates/be/skeleton/apps/__app__/src/main.ts +9 -6
- package/templates/be/skeleton/scripts/.gitkeep +0 -0
- package/templates/be/skeleton/src/features/system-health/application/check-liveness.contracts.ts +2 -0
- package/templates/be/skeleton/src/features/system-health/application/check-liveness.handler.ts +22 -0
- package/templates/be/skeleton/src/features/system-health/application/check-liveness.query.ts +11 -0
- package/templates/be/skeleton/src/features/system-health/index.ts +1 -1
- package/templates/be/skeleton/src/features/system-health/system-health.module.ts +3 -3
- package/templates/be/skeleton/src/features/system-health/transport/http/live.controller.ts +8 -9
- package/templates/be/skeleton/src/features/system-health/transport/http/system-health-http.module.ts +3 -6
- package/templates/be/skeleton/src/modules/domain/liveness/index.ts +3 -0
- package/templates/be/skeleton/src/modules/domain/liveness/liveness.contracts.ts +9 -0
- package/templates/be/skeleton/src/modules/domain/liveness/liveness.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/domain/liveness/liveness.module.ts +14 -0
- package/templates/be/skeleton/src/modules/domain/liveness/liveness.options.ts +2 -0
- package/templates/be/skeleton/src/modules/domain/liveness/liveness.service.spec.ts +40 -0
- package/templates/be/skeleton/src/modules/domain/liveness/liveness.service.ts +26 -0
- package/templates/be/skeleton/src/modules/platform/clock/clock.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/clock/clock.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/clock/clock.module.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/clock/clock.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/clock/clock.port.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/clock/index.ts +4 -0
- package/templates/be/skeleton/src/modules/platform/clock/system-clock.service.spec.ts +20 -0
- package/templates/be/skeleton/src/modules/platform/clock/system-clock.service.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/composition/composition.decorators.ts +8 -0
- package/templates/be/skeleton/src/modules/platform/composition/index.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/config/config.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/config/index.ts +2 -3
- package/templates/be/skeleton/src/modules/platform/config/server.config.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/config/server.options.ts +0 -3
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.contracts.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.handler.ts +27 -0
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.log-events.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module.ts +18 -0
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/cqrs/index.ts +4 -0
- package/templates/be/skeleton/src/modules/platform/errors/error.filter.ts +8 -12
- package/templates/be/skeleton/src/modules/platform/errors/errors.log-events.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/errors/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/logging/index.ts +4 -4
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +99 -0
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +38 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.module.ts +23 -10
- package/templates/be/skeleton/src/modules/platform/logging/logging.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.port.ts +15 -0
- package/templates/be/tool-config/eslint.config.mjs +3 -0
- package/templates/be/tool-config/jest.config.js +1 -0
- package/templates/be/tool-config/prettierignore +8 -0
- package/templates/be/tool-config/prettierrc +1 -0
- package/templates/be/tool-config/src/tests/tsconfig.json +5 -0
- package/templates/be/tool-config/tsconfig.build.json +5 -0
- package/templates/be/tool-config/tsconfig.json +11 -0
- package/templates/common/gitignore.base +1 -1
- package/templates/fe/ci-workflows/github/workflows/ci.yml +54 -0
- package/templates/fe/hooks/husky/pre-commit +16 -0
- package/templates/fe/hooks/husky/pre-push +6 -0
- package/templates/fe/package-scripts/package.json +17 -0
- package/templates/fe/parts/api-client.ts +44 -0
- package/templates/fe/parts/api-outcome.ts +7 -0
- package/templates/fe/quality-config/sonar-project.properties +8 -0
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/layout.tsx +1 -1
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/not-found.tsx +1 -1
- package/templates/fe/skeleton/apps/__app__/src/app/global-error.tsx +1 -1
- package/templates/fe/skeleton/scripts/.gitkeep +0 -0
- package/templates/fe/skeleton-app/apps/__app__/src/modules/api/client.ts +1 -0
- package/templates/fe/skeleton-app/apps/__app__/src/modules/api/index.ts +3 -0
- package/templates/fe/skeleton-app/apps/__app__/src/modules/api/outcome.ts +1 -0
- package/templates/fe/skeleton-app/apps/__app__/src/modules/i18n/index.ts +4 -0
- package/templates/fe/skeleton-shared/apps/__app__/next.config.ts +12 -0
- package/templates/fe/skeleton-shared/apps/__app__/src/modules/api/index.ts +2 -0
- package/templates/fe/skeleton-shared/apps/__app__/src/modules/i18n/index.ts +9 -0
- package/templates/fe/skeleton-shared/apps/__app__/src/modules/i18n/request.ts +5 -0
- package/templates/fe/skeleton-shared/apps/__app__/src/proxy.ts +5 -0
- package/templates/fe/skeleton-shared/packages/__family__-api/package.json +12 -0
- package/templates/fe/skeleton-shared/packages/__family__-api/src/client.ts +1 -0
- package/templates/fe/skeleton-shared/packages/__family__-api/src/index.ts +3 -0
- package/templates/fe/skeleton-shared/packages/__family__-api/src/outcome.ts +1 -0
- package/templates/fe/skeleton-shared/packages/__family__-api/tsconfig.json +5 -0
- package/templates/fe/skeleton-shared/packages/__family__-i18n/package.json +18 -0
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/app.ts +19 -0
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/index.ts +2 -0
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/proxy.ts +12 -0
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/request.ts +15 -0
- package/templates/fe/skeleton-shared/packages/__family__-i18n/tsconfig.json +5 -0
- package/templates/fe/tool-config/eslint.config.mjs +3 -0
- package/templates/fe/tool-config/prettierignore +10 -0
- package/templates/fe/tool-config/prettierrc +1 -0
- package/templates/fe/tool-config/stylelint.config.mjs +3 -0
- package/templates/fe/tool-config/tsconfig.json +4 -0
- package/templates/be/pre-commit +0 -8
- package/templates/be/skeleton/apps/__app__/src/__app__.composition.spec.ts +0 -31
- package/templates/be/skeleton/src/features/system-health/transport/http/live.controller.spec.ts +0 -13
- package/templates/be/skeleton/src/modules/platform/config/env-source.spec.ts +0 -36
- package/templates/be/skeleton/src/modules/platform/config/server.config.spec.ts +0 -19
- package/templates/be/skeleton/src/modules/platform/errors/domain-error.spec.ts +0 -15
- package/templates/be/skeleton/src/modules/platform/errors/error.filter.spec.ts +0 -38
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.spec.ts +0 -33
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.ts +0 -35
- package/templates/be/skeleton/src/modules/platform/logging/log-id.ts +0 -9
- package/templates/be/skeleton/src/modules/platform/logging/logger.port.ts +0 -19
- package/templates/common/codecov.yml +0 -13
- package/templates/common/pre-push +0 -5
- package/templates/fe/e2e.yml +0 -22
- package/templates/fe/pre-commit +0 -7
- package/templates/fe/skeleton/apps/__app__/src/app/health/live/route.spec.ts +0 -10
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/messages.spec.ts +0 -27
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/routing.spec.ts +0 -10
- package/templates/fe/sonar-project.properties +0 -11
- /package/templates/be/skeleton/src/modules/platform/config/{env-source.ts → env-source.config.ts} +0 -0
- /package/templates/be/skeleton/src/modules/platform/errors/{domain-error.ts → domain.error.ts} +0 -0
- /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/next.config.ts +0 -0
- /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/config.ts +0 -0
- /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/navigation.ts +0 -0
- /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/request.ts +0 -0
- /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/routing.ts +0 -0
- /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/proxy.ts +0 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { hasLocale } from "next-intl"
|
|
2
|
+
import type { defineRouting } from "next-intl/routing"
|
|
3
|
+
import { getRequestConfig } from "next-intl/server"
|
|
4
|
+
|
|
5
|
+
type Routing = ReturnType<typeof defineRouting>
|
|
6
|
+
|
|
7
|
+
/** The requested locale when the app serves it, else the default one. */
|
|
8
|
+
export const resolveLocale = (routing: Routing, requested: string | undefined): string => (hasLocale(routing.locales, requested) ? requested : routing.defaultLocale)
|
|
9
|
+
|
|
10
|
+
/** The request config of an app: the resolved locale and the catalog the app loads for it. */
|
|
11
|
+
export const createRequestConfig = (routing: Routing, loadMessages: (locale: string) => Promise<Record<string, unknown>>) =>
|
|
12
|
+
getRequestConfig(async ({ requestLocale }) => {
|
|
13
|
+
const locale = resolveLocale(routing, await requestLocale)
|
|
14
|
+
return { locale, messages: await loadMessages(locale) }
|
|
15
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"@starci/prettier-config"
|
package/templates/be/pre-commit
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
# Commit gate: staged lint, types, the unit specs the staged files touch, work hygiene. Never e2e.
|
|
2
|
-
npx lint-staged
|
|
3
|
-
npm run typecheck
|
|
4
|
-
staged=$(git diff --cached --name-only --diff-filter=ACMR -- '*.ts' '*.tsx' | grep -v -E '(^|/)e2e/|\.e2e-spec\.ts$' || true)
|
|
5
|
-
if [ -n "$staged" ]; then
|
|
6
|
-
npx jest --selectProjects unit --passWithNoTests --findRelatedTests $staged
|
|
7
|
-
fi
|
|
8
|
-
npx hfs work-hygiene
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { INestApplication } from "@nestjs/common"
|
|
2
|
-
import { Test } from "@nestjs/testing"
|
|
3
|
-
import request from "supertest"
|
|
4
|
-
import { SERVER_OPTIONS } from "@modules/platform/config"
|
|
5
|
-
import { Logger } from "@modules/platform/logging"
|
|
6
|
-
import { AppModule } from "./app.module"
|
|
7
|
-
|
|
8
|
-
describe("{{app}} composition", () => {
|
|
9
|
-
let app: INestApplication
|
|
10
|
-
|
|
11
|
-
beforeAll(async () => {
|
|
12
|
-
const moduleRef = await Test.createTestingModule({ imports: [AppModule.register({ server: { port: 3000 } })] }).compile()
|
|
13
|
-
app = moduleRef.createNestApplication()
|
|
14
|
-
await app.init()
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
afterAll(async () => {
|
|
18
|
-
await app.close()
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
it("boots the real AppModule and resolves its providers", () => {
|
|
22
|
-
expect(app.get(Logger)).toBeInstanceOf(Logger)
|
|
23
|
-
expect(app.get(SERVER_OPTIONS)).toEqual({ port: 3000 })
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it("answers GET /health/live with the { status, info, error, details } shape", async () => {
|
|
27
|
-
const response = await request(app.getHttpServer()).get("/health/live")
|
|
28
|
-
expect(response.status).toBe(200)
|
|
29
|
-
expect(response.body).toEqual({ status: "ok", info: {}, error: {}, details: {} })
|
|
30
|
-
})
|
|
31
|
-
})
|
package/templates/be/skeleton/src/features/system-health/transport/http/live.controller.spec.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { HealthCheckService } from "@nestjs/terminus"
|
|
2
|
-
import { mock } from "@starci/jest-preset/mock"
|
|
3
|
-
import { LiveController } from "./live.controller"
|
|
4
|
-
|
|
5
|
-
describe("LiveController", () => {
|
|
6
|
-
it("runs Terminus with no indicator, so liveness never depends on a dependency", async () => {
|
|
7
|
-
const health = mock<HealthCheckService>()
|
|
8
|
-
const result = { status: "ok" as const, info: {}, error: {}, details: {} }
|
|
9
|
-
health.check.mockResolvedValue(result)
|
|
10
|
-
await expect(new LiveController(health).live()).resolves.toEqual(result)
|
|
11
|
-
expect(health.check).toHaveBeenCalledWith([])
|
|
12
|
-
})
|
|
13
|
-
})
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs"
|
|
2
|
-
import { tmpdir } from "node:os"
|
|
3
|
-
import { join } from "node:path"
|
|
4
|
-
import { EnvSource } from "./env-source"
|
|
5
|
-
import { ConfigError } from "./errors/config.error"
|
|
6
|
-
|
|
7
|
-
describe("EnvSource", () => {
|
|
8
|
-
it("reads a key and treats an empty value as unset", () => {
|
|
9
|
-
const env = EnvSource.of({ PORT: "8080", EMPTY: "" })
|
|
10
|
-
expect(env.optional("PORT")).toBe("8080")
|
|
11
|
-
expect(env.optional("EMPTY")).toBeUndefined()
|
|
12
|
-
expect(env.optional("ABSENT")).toBeUndefined()
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
it("refuses a missing required key by naming it", () => {
|
|
16
|
-
const failure = () => EnvSource.of({}).required("DATABASE_URL")
|
|
17
|
-
expect(failure).toThrow(ConfigError)
|
|
18
|
-
expect(failure).toThrow("Configuration key DATABASE_URL is required and has no default.")
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
it("resolves <KEY>_FILE to the trimmed content of the file", () => {
|
|
22
|
-
const dir = mkdtempSync(join(tmpdir(), "env-source-"))
|
|
23
|
-
try {
|
|
24
|
-
const path = join(dir, "token")
|
|
25
|
-
writeFileSync(path, "value-from-file\n")
|
|
26
|
-
expect(EnvSource.of({ API_TOKEN_FILE: path }).required("API_TOKEN")).toBe("value-from-file")
|
|
27
|
-
} finally {
|
|
28
|
-
rmSync(dir, { recursive: true, force: true })
|
|
29
|
-
}
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
it("names the _FILE key, not a path or a value, when the file cannot be read", () => {
|
|
33
|
-
const failure = () => EnvSource.of({ API_TOKEN_FILE: join(tmpdir(), "no-such-dir", "token") }).optional("API_TOKEN")
|
|
34
|
-
expect(failure).toThrow("Configuration key API_TOKEN_FILE points at a file that cannot be read.")
|
|
35
|
-
})
|
|
36
|
-
})
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { EnvSource } from "./env-source"
|
|
2
|
-
import { ConfigError } from "./errors/config.error"
|
|
3
|
-
import { parseServerConfig } from "./server.config"
|
|
4
|
-
|
|
5
|
-
describe("parseServerConfig", () => {
|
|
6
|
-
it("falls back to port 3000 when PORT is unset", () => {
|
|
7
|
-
expect(parseServerConfig(EnvSource.of({}))).toEqual({ port: 3000 })
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
it("accepts an integer PORT inside the TCP range", () => {
|
|
11
|
-
expect(parseServerConfig(EnvSource.of({ PORT: "8080" }))).toEqual({ port: 8080 })
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
it.each(["80a", "0", "70000", "1.5"])("refuses PORT=%s by naming the key only", (port) => {
|
|
15
|
-
const failure = () => parseServerConfig(EnvSource.of({ PORT: port }))
|
|
16
|
-
expect(failure).toThrow(ConfigError)
|
|
17
|
-
expect(failure).toThrow("Configuration key PORT does not hold a valid value.")
|
|
18
|
-
})
|
|
19
|
-
})
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { DomainError } from "./domain-error"
|
|
2
|
-
|
|
3
|
-
class SampleError extends DomainError {}
|
|
4
|
-
|
|
5
|
-
describe("DomainError", () => {
|
|
6
|
-
it("carries the code, the message and the cause, and is named after its subclass", () => {
|
|
7
|
-
const cause = new SyntaxError("bad input")
|
|
8
|
-
const error = new SampleError("SAMPLE_FAILED", "The sample failed.", { cause })
|
|
9
|
-
expect(error.code).toBe("SAMPLE_FAILED")
|
|
10
|
-
expect(error.message).toBe("The sample failed.")
|
|
11
|
-
expect(error.cause).toBe(cause)
|
|
12
|
-
expect(error.name).toBe("SampleError")
|
|
13
|
-
expect(error).toBeInstanceOf(Error)
|
|
14
|
-
})
|
|
15
|
-
})
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { ArgumentsHost, HttpException, HttpStatus } from "@nestjs/common"
|
|
2
|
-
import { HttpArgumentsHost } from "@nestjs/common/interfaces"
|
|
3
|
-
import { mock } from "@starci/jest-preset/mock"
|
|
4
|
-
import type { Response } from "express"
|
|
5
|
-
import { Logger, LogId } from "@modules/platform/logging"
|
|
6
|
-
import { ErrorFilter } from "./error.filter"
|
|
7
|
-
|
|
8
|
-
const arrange = () => {
|
|
9
|
-
const response = mock<Response>()
|
|
10
|
-
response.status.mockReturnValue(response)
|
|
11
|
-
const http = mock<HttpArgumentsHost>()
|
|
12
|
-
http.getResponse.mockReturnValue(response)
|
|
13
|
-
const host = mock<ArgumentsHost>()
|
|
14
|
-
host.switchToHttp.mockReturnValue(http)
|
|
15
|
-
const logger = mock<Logger>()
|
|
16
|
-
return { response, host, logger, filter: new ErrorFilter(logger) }
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
describe("ErrorFilter", () => {
|
|
20
|
-
it("answers an unknown failure with a 500 that names no detail, and logs it once", () => {
|
|
21
|
-
const { response, host, logger, filter } = arrange()
|
|
22
|
-
filter.catch(new TypeError("secret detail"), host)
|
|
23
|
-
expect(response.status).toHaveBeenCalledWith(HttpStatus.INTERNAL_SERVER_ERROR)
|
|
24
|
-
expect(response.json).toHaveBeenCalledWith({ code: "internal_error" })
|
|
25
|
-
expect(logger.error).toHaveBeenCalledTimes(1)
|
|
26
|
-
expect(logger.error).toHaveBeenCalledWith(LogId.RequestFailed, {
|
|
27
|
-
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
|
28
|
-
failure: { name: "TypeError" },
|
|
29
|
-
})
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
it("keeps the status of a framework exception and answers with its status code only", () => {
|
|
33
|
-
const { response, host, filter } = arrange()
|
|
34
|
-
filter.catch(new HttpException("Not Found", HttpStatus.NOT_FOUND), host)
|
|
35
|
-
expect(response.status).toHaveBeenCalledWith(HttpStatus.NOT_FOUND)
|
|
36
|
-
expect(response.json).toHaveBeenCalledWith({ code: "http_404" })
|
|
37
|
-
})
|
|
38
|
-
})
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { Writable } from "node:stream"
|
|
2
|
-
import { createJsonLogger } from "./json-logger"
|
|
3
|
-
import { LogId } from "./log-id"
|
|
4
|
-
|
|
5
|
-
const capture = () => {
|
|
6
|
-
const lines: string[] = []
|
|
7
|
-
const sink = new Writable({
|
|
8
|
-
write(chunk: Buffer, _encoding, done) {
|
|
9
|
-
lines.push(chunk.toString())
|
|
10
|
-
done()
|
|
11
|
-
},
|
|
12
|
-
})
|
|
13
|
-
return { lines, logger: createJsonLogger(sink) }
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
describe("createJsonLogger", () => {
|
|
17
|
-
it("writes one JSON line carrying the level, the enum identity, a timestamp and the payload", () => {
|
|
18
|
-
const { lines, logger } = capture()
|
|
19
|
-
logger.error(LogId.StartupFailed, { code: "CONFIG_KEY_MISSING" })
|
|
20
|
-
expect(lines).toHaveLength(1)
|
|
21
|
-
expect(lines[0].endsWith("\n")).toBe(true)
|
|
22
|
-
expect(JSON.parse(lines[0])).toMatchObject({ level: "error", id: "server.startup_failed", code: "CONFIG_KEY_MISSING" })
|
|
23
|
-
expect(JSON.parse(lines[0]).time).toEqual(expect.any(String))
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it("maps every level to its own name", () => {
|
|
27
|
-
const { lines, logger } = capture()
|
|
28
|
-
logger.debug(LogId.ServerStarted)
|
|
29
|
-
logger.info(LogId.ServerStarted)
|
|
30
|
-
logger.warn(LogId.ServerStarted)
|
|
31
|
-
expect(lines.map((line) => JSON.parse(line).level)).toEqual(["debug", "info", "warn"])
|
|
32
|
-
})
|
|
33
|
-
})
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import type { Writable } from "node:stream"
|
|
2
|
-
import type { LogId } from "./log-id"
|
|
3
|
-
import { Logger, LogPayload } from "./logger.port"
|
|
4
|
-
|
|
5
|
-
type Level = "debug" | "info" | "warn" | "error"
|
|
6
|
-
|
|
7
|
-
/** The default adapter: one JSON object per line on a stream, so a collector reads it without parsing prose. */
|
|
8
|
-
class JsonLogger extends Logger {
|
|
9
|
-
constructor(private readonly sink: Writable) {
|
|
10
|
-
super()
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
debug(id: LogId, payload?: LogPayload): void {
|
|
14
|
-
this.write("debug", id, payload)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
info(id: LogId, payload?: LogPayload): void {
|
|
18
|
-
this.write("info", id, payload)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
warn(id: LogId, payload?: LogPayload): void {
|
|
22
|
-
this.write("warn", id, payload)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
error(id: LogId, payload?: LogPayload): void {
|
|
26
|
-
this.write("error", id, payload)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
private write(level: Level, id: LogId, payload?: LogPayload): void {
|
|
30
|
-
this.sink.write(`${JSON.stringify({ level, id, time: new Date().toISOString(), ...payload })}\n`)
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Builds the JSON logger; `sink` is stdout unless a caller (a spec, a process manager) supplies its own stream. */
|
|
35
|
-
export const createJsonLogger = (sink: Writable = process.stdout): Logger => new JsonLogger(sink)
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/** Every log line is named by a member of this enum, so a line can be found from the code that wrote it. */
|
|
2
|
-
export enum LogId {
|
|
3
|
-
/** The HTTP listener is bound and the app serves requests. */
|
|
4
|
-
ServerStarted = "server.started",
|
|
5
|
-
/** The app failed before it could serve; the process exits non-zero. */
|
|
6
|
-
StartupFailed = "server.startup_failed",
|
|
7
|
-
/** A request ended in a failure the app's filter answered. */
|
|
8
|
-
RequestFailed = "http.request_failed",
|
|
9
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { LogId } from "./log-id"
|
|
2
|
-
|
|
3
|
-
/** The structured data of one log line; values are plain data, never a request, a token or a secret. */
|
|
4
|
-
export type LogPayload = Readonly<Record<string, unknown>>
|
|
5
|
-
|
|
6
|
-
/** The logging port: capabilities log through it with an enum identity and a structured payload, never `console`. */
|
|
7
|
-
export abstract class Logger {
|
|
8
|
-
/** Detail for people debugging one environment. */
|
|
9
|
-
abstract debug(id: LogId, payload?: LogPayload): void
|
|
10
|
-
|
|
11
|
-
/** A normal event worth keeping. */
|
|
12
|
-
abstract info(id: LogId, payload?: LogPayload): void
|
|
13
|
-
|
|
14
|
-
/** Something unexpected the app recovered from. */
|
|
15
|
-
abstract warn(id: LogId, payload?: LogPayload): void
|
|
16
|
-
|
|
17
|
-
/** A failure the app could not recover from in place. */
|
|
18
|
-
abstract error(id: LogId, payload?: LogPayload): void
|
|
19
|
-
}
|
package/templates/fe/e2e.yml
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
# End-to-end runs are manual: nothing triggers this workflow but a person dispatching it.
|
|
2
|
-
name: e2e
|
|
3
|
-
|
|
4
|
-
on:
|
|
5
|
-
workflow_dispatch:
|
|
6
|
-
|
|
7
|
-
permissions:
|
|
8
|
-
contents: read
|
|
9
|
-
|
|
10
|
-
jobs:
|
|
11
|
-
e2e:
|
|
12
|
-
runs-on: ubuntu-latest
|
|
13
|
-
steps:
|
|
14
|
-
- uses: actions/checkout@v4
|
|
15
|
-
- uses: actions/setup-node@v4
|
|
16
|
-
with:
|
|
17
|
-
node-version: {{nodeMajor}}
|
|
18
|
-
cache: npm
|
|
19
|
-
- run: npm ci
|
|
20
|
-
- run: npx playwright install --with-deps chromium
|
|
21
|
-
- name: e2e
|
|
22
|
-
run: npm run test:e2e
|
package/templates/fe/pre-commit
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
# Commit gate: staged lint, types, the unit specs the staged files touch. Never e2e.
|
|
2
|
-
npx lint-staged
|
|
3
|
-
npm run typecheck
|
|
4
|
-
staged=$(git diff --cached --name-only --diff-filter=ACMR -- '*.ts' '*.tsx' | grep -v -E '(^|/)e2e/|\.e2e-spec\.ts$' || true)
|
|
5
|
-
if [ -n "$staged" ]; then
|
|
6
|
-
npx vitest related --run --passWithNoTests $staged
|
|
7
|
-
fi
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest"
|
|
2
|
-
import { GET } from "./route"
|
|
3
|
-
|
|
4
|
-
describe("GET /health/live", () => {
|
|
5
|
-
it("answers the { status, info, error, details } shape from process-local state", async () => {
|
|
6
|
-
const response = GET()
|
|
7
|
-
expect(response.status).toBe(200)
|
|
8
|
-
await expect(response.json()).resolves.toEqual({ status: "ok", info: {}, error: {}, details: {} })
|
|
9
|
-
})
|
|
10
|
-
})
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest"
|
|
2
|
-
import vi from "./messages/vi.json"
|
|
3
|
-
|
|
4
|
-
const leaves = (value: unknown, path = ""): string[] =>
|
|
5
|
-
typeof value === "object" && value !== null
|
|
6
|
-
? Object.entries(value).flatMap(([key, child]) => leaves(child, path ? `${path}.${key}` : key))
|
|
7
|
-
: [path]
|
|
8
|
-
|
|
9
|
-
const valueAt = (path: string): unknown => path.split(".").reduce<unknown>((node, key) => (node as Record<string, unknown>)[key], vi)
|
|
10
|
-
|
|
11
|
-
describe("messages/vi.json", () => {
|
|
12
|
-
it("carries the strings the error, not-found and home routes render", () => {
|
|
13
|
-
expect(leaves(vi).sort()).toEqual([
|
|
14
|
-
"errors.global.retry",
|
|
15
|
-
"errors.global.title",
|
|
16
|
-
"errors.page.retry",
|
|
17
|
-
"errors.page.title",
|
|
18
|
-
"home.title",
|
|
19
|
-
"notFound.home",
|
|
20
|
-
"notFound.title",
|
|
21
|
-
])
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
it("has no empty string", () => {
|
|
25
|
-
for (const path of leaves(vi)) expect(valueAt(path)).not.toBe("")
|
|
26
|
-
})
|
|
27
|
-
})
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest"
|
|
2
|
-
import { routing } from "./routing"
|
|
3
|
-
|
|
4
|
-
describe("routing", () => {
|
|
5
|
-
it("serves vi by default, without a prefix", () => {
|
|
6
|
-
expect(routing.defaultLocale).toBe("vi")
|
|
7
|
-
expect(routing.localePrefix).toBe("as-needed")
|
|
8
|
-
expect(routing.locales).toContain("vi")
|
|
9
|
-
})
|
|
10
|
-
})
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
sonar.projectKey={{sonarKey}}
|
|
2
|
-
sonar.host.url=https://sonar.starci.org
|
|
3
|
-
sonar.sourceEncoding=UTF-8
|
|
4
|
-
sonar.sources=apps
|
|
5
|
-
sonar.tests=apps
|
|
6
|
-
sonar.exclusions={{sonarExclusions}}
|
|
7
|
-
sonar.test.inclusions=**/*.spec.ts,**/*.spec.tsx
|
|
8
|
-
sonar.coverage.exclusions={{sonarCoverageExclusions}}
|
|
9
|
-
sonar.javascript.lcov.reportPaths=coverage/lcov.info
|
|
10
|
-
sonar.typescript.tsconfigPaths={{tsconfigPaths}}
|
|
11
|
-
sonar.nodejs.maxspace=8192
|
/package/templates/be/skeleton/src/modules/platform/config/{env-source.ts → env-source.config.ts}
RENAMED
|
File without changes
|
/package/templates/be/skeleton/src/modules/platform/errors/{domain-error.ts → domain.error.ts}
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|