@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
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { createJsonLogger } from "./json-logger"
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export type { LogPayload } from "./logger.port"
|
|
1
|
+
export { createJsonLogger } from "./json-logger.service"
|
|
2
|
+
export { InjectLogger } from "./logging.decorators"
|
|
3
|
+
export { LoggingLogEvent } from "./logging.log-events"
|
|
5
4
|
export { LoggingModule } from "./logging.module"
|
|
5
|
+
export type { Logger } from "./logging.port"
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Test } from "@nestjs/testing"
|
|
2
|
+
import { FakeClock } from "@starci/jest-preset"
|
|
3
|
+
import { CLOCK } from "@modules/platform/clock"
|
|
4
|
+
import { JsonLoggerService, createJsonLogger } from "./json-logger.service"
|
|
5
|
+
|
|
6
|
+
const TIME = "2026-01-01T00:00:00.000Z"
|
|
7
|
+
|
|
8
|
+
const build = async () => {
|
|
9
|
+
const clock = new FakeClock(TIME)
|
|
10
|
+
const moduleRef = await Test.createTestingModule({
|
|
11
|
+
providers: [JsonLoggerService, { provide: CLOCK, useValue: clock }],
|
|
12
|
+
}).compile()
|
|
13
|
+
return { service: moduleRef.get(JsonLoggerService), clock }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** One JSON line, the way the logger writes it. */
|
|
17
|
+
const line = (fields: Record<string, unknown>): string => `${JSON.stringify(fields)}\n`
|
|
18
|
+
|
|
19
|
+
describe("JsonLoggerService", () => {
|
|
20
|
+
const stdout: Array<string> = []
|
|
21
|
+
const stderr: Array<string> = []
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
stdout.length = 0
|
|
25
|
+
stderr.length = 0
|
|
26
|
+
jest.spyOn(process.stdout, "write").mockImplementation((chunk) => {
|
|
27
|
+
stdout.push(String(chunk))
|
|
28
|
+
return true
|
|
29
|
+
})
|
|
30
|
+
jest.spyOn(process.stderr, "write").mockImplementation((chunk) => {
|
|
31
|
+
stderr.push(String(chunk))
|
|
32
|
+
return true
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
jest.restoreAllMocks()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe("info", () => {
|
|
41
|
+
it("stamps the line with the clock and writes it to stdout", async () => {
|
|
42
|
+
const { service } = await build()
|
|
43
|
+
|
|
44
|
+
service.info("thing.happened", { id: 1 })
|
|
45
|
+
|
|
46
|
+
expect(stdout).toEqual([line({ level: "info", event: "thing.happened", time: TIME, id: 1 })])
|
|
47
|
+
expect(stderr).toEqual([])
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
describe("warn", () => {
|
|
52
|
+
it("writes the line to stderr, stamped with the moment of the call", async () => {
|
|
53
|
+
const { service, clock } = await build()
|
|
54
|
+
clock.advance(5_000)
|
|
55
|
+
|
|
56
|
+
service.warn("thing.slow")
|
|
57
|
+
|
|
58
|
+
expect(stderr).toEqual([line({ level: "warn", event: "thing.slow", time: "2026-01-01T00:00:05.000Z" })])
|
|
59
|
+
expect(stdout).toEqual([])
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe("error", () => {
|
|
64
|
+
it("serializes an Error cause by name and message", async () => {
|
|
65
|
+
const { service } = await build()
|
|
66
|
+
|
|
67
|
+
service.error("thing.failed", new TypeError("boom"), { id: 2 })
|
|
68
|
+
|
|
69
|
+
expect(stderr).toEqual([
|
|
70
|
+
line({
|
|
71
|
+
level: "error",
|
|
72
|
+
event: "thing.failed",
|
|
73
|
+
time: TIME,
|
|
74
|
+
errorName: "TypeError",
|
|
75
|
+
errorMessage: "boom",
|
|
76
|
+
id: 2,
|
|
77
|
+
}),
|
|
78
|
+
])
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it("serializes a cause that is not an Error to its text", async () => {
|
|
82
|
+
const { service } = await build()
|
|
83
|
+
|
|
84
|
+
service.error("thing.failed", "plain text")
|
|
85
|
+
|
|
86
|
+
expect(stderr).toEqual([
|
|
87
|
+
line({ level: "error", event: "thing.failed", time: TIME, errorMessage: "plain text" }),
|
|
88
|
+
])
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
describe("createJsonLogger", () => {
|
|
93
|
+
it("builds a logger stamped by the given clock", () => {
|
|
94
|
+
createJsonLogger(new FakeClock(TIME)).info("thing.started")
|
|
95
|
+
|
|
96
|
+
expect(stdout).toEqual([line({ level: "info", event: "thing.started", time: TIME })])
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Injectable } from "@nestjs/common"
|
|
2
|
+
import { InjectClock } from "@modules/platform/clock"
|
|
3
|
+
import type { Clock } from "@modules/platform/clock"
|
|
4
|
+
import type { LogFields, Logger } from "./logging.port"
|
|
5
|
+
|
|
6
|
+
type Level = "info" | "warn" | "error"
|
|
7
|
+
|
|
8
|
+
@Injectable()
|
|
9
|
+
/** The default adapter: one JSON object per line, stamped by the Clock; info goes to stdout, warn and error to stderr. */
|
|
10
|
+
export class JsonLoggerService implements Logger {
|
|
11
|
+
constructor(@InjectClock() private readonly clock: Clock) {}
|
|
12
|
+
|
|
13
|
+
/** Writes an info line. */
|
|
14
|
+
info(event: string, fields?: LogFields): void {
|
|
15
|
+
this.write("info", process.stdout, event, fields)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Writes a warn line. */
|
|
19
|
+
warn(event: string, fields?: LogFields): void {
|
|
20
|
+
this.write("warn", process.stderr, event, fields)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Writes an error line with the cause serialized by name and message. */
|
|
24
|
+
error(event: string, cause: unknown, fields?: LogFields): void {
|
|
25
|
+
const detail =
|
|
26
|
+
cause instanceof Error
|
|
27
|
+
? { errorName: cause.name, errorMessage: cause.message }
|
|
28
|
+
: { errorMessage: String(cause) }
|
|
29
|
+
this.write("error", process.stderr, event, { ...detail, ...fields })
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private write(level: Level, sink: NodeJS.WriteStream, event: string, fields?: LogFields): void {
|
|
33
|
+
sink.write(`${JSON.stringify({ level, event, time: this.clock.now().toISOString(), ...fields })}\n`)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Builds the JSON logger stamping lines with `clock`; main.ts uses it before the DI container exists. */
|
|
38
|
+
export const createJsonLogger = (clock: Clock): Logger => new JsonLoggerService(clock)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { injector } from "@modules/platform/composition"
|
|
2
|
+
import type { TypedParameterDecorator } from "@modules/platform/composition"
|
|
3
|
+
import type { Logger } from "./logging.port"
|
|
4
|
+
|
|
5
|
+
/** Token of the Logger port. */
|
|
6
|
+
export const LOGGER: unique symbol = Symbol("platform.logging.logger")
|
|
7
|
+
|
|
8
|
+
/** Injects the Logger port. Parameter type: Logger. */
|
|
9
|
+
export const InjectLogger = (): TypedParameterDecorator<Logger> => injector<Logger>(LOGGER)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Log events of the process lifecycle: the entry points (main.ts of each app) log these. */
|
|
2
|
+
export enum LoggingLogEvent {
|
|
3
|
+
/** The HTTP listener is bound and the service answers requests. */
|
|
4
|
+
ServerStarted = "server.started",
|
|
5
|
+
/** The service failed before it could serve; the process exits non-zero. */
|
|
6
|
+
StartupFailed = "server.startup_failed",
|
|
7
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ConfigurableModuleBuilder } from "@nestjs/common"
|
|
2
|
+
import type { LoggingOptions } from "./logging.options"
|
|
3
|
+
|
|
4
|
+
/** The configurable-module base of the logging capability; `isGlobal` is decided by the app root. */
|
|
5
|
+
export const { ConfigurableModuleClass, OPTIONS_TYPE } = new ConfigurableModuleBuilder<LoggingOptions>()
|
|
6
|
+
.setExtras({ isGlobal: false }, (definition, extras) => ({ ...definition, global: extras.isGlobal }))
|
|
7
|
+
.build()
|
|
@@ -1,11 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { Module } from "@nestjs/common"
|
|
2
|
+
import type { DynamicModule } from "@nestjs/common"
|
|
3
|
+
import { CLOCK } from "@modules/platform/clock"
|
|
4
|
+
import type { Clock } from "@modules/platform/clock"
|
|
5
|
+
import { createJsonLogger } from "./json-logger.service"
|
|
6
|
+
import { LOGGER } from "./logging.decorators"
|
|
7
|
+
import { ConfigurableModuleClass, OPTIONS_TYPE } from "./logging.module-definition"
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
@Module({})
|
|
10
|
+
/** Provides the Logger port as the JSON-lines adapter stamped by the Clock the app registers. */
|
|
11
|
+
export class LoggingModule extends ConfigurableModuleClass {
|
|
12
|
+
/** Registers the capability once per app. */
|
|
13
|
+
static register(options: typeof OPTIONS_TYPE): DynamicModule {
|
|
14
|
+
const base = super.register(options)
|
|
15
|
+
return {
|
|
16
|
+
...base,
|
|
17
|
+
providers: [
|
|
18
|
+
...(base.providers ?? []),
|
|
19
|
+
{ provide: LOGGER, inject: [CLOCK], useFactory: (clock: Clock) => createJsonLogger(clock) },
|
|
20
|
+
],
|
|
21
|
+
exports: [LOGGER],
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** The structured data of one log line; values are plain data, never a request, a token or a secret. */
|
|
2
|
+
export interface LogFields {
|
|
3
|
+
/** The value logged under `name`. */
|
|
4
|
+
readonly [name: string]: unknown
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** The logging port: owners log through it with an enum member of their own `<owner>.log-events.ts`, never `console`. */
|
|
8
|
+
export interface Logger {
|
|
9
|
+
/** A normal event worth keeping. */
|
|
10
|
+
info(event: string, fields?: LogFields): void
|
|
11
|
+
/** Something unexpected the service recovered from. */
|
|
12
|
+
warn(event: string, fields?: LogFields): void
|
|
13
|
+
/** A failure: the cause is serialized by name and message, never dumped whole. */
|
|
14
|
+
error(event: string, cause: unknown, fields?: LogFields): void
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require("@starci/jest-preset").starciJestConfig()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"@starci/prettier-config"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@starci/tsconfig/be.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"paths": {
|
|
5
|
+
"@features/*": ["./src/features/*"],
|
|
6
|
+
"@modules/*": ["./src/modules/*"],
|
|
7
|
+
"@tests/*": ["./src/tests/*"]
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
"exclude": ["node_modules", "dist", "src/tests/world", "src/tests/integration", "src/tests/e2e", "src/tests/contract"]
|
|
11
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{{header}}
|
|
2
|
+
name: ci
|
|
3
|
+
|
|
4
|
+
on:
|
|
5
|
+
push:
|
|
6
|
+
branches: [main]
|
|
7
|
+
pull_request:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
ci:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
env:
|
|
16
|
+
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
- uses: actions/setup-node@v4
|
|
20
|
+
with:
|
|
21
|
+
node-version: {{nodeMajor}}
|
|
22
|
+
cache: npm
|
|
23
|
+
- run: npm ci
|
|
24
|
+
- name: hfs check
|
|
25
|
+
run: npm run hfs:report
|
|
26
|
+
- name: lint
|
|
27
|
+
run: npm run lint:check
|
|
28
|
+
- name: eslint report
|
|
29
|
+
if: ${{ !cancelled() }}
|
|
30
|
+
run: npm run lint:report
|
|
31
|
+
- name: eslint sonar import
|
|
32
|
+
if: ${{ !cancelled() }}
|
|
33
|
+
run: npx hfs report eslint reports/eslint.json reports/eslint.sonar.json
|
|
34
|
+
- name: stylelint report
|
|
35
|
+
if: ${{ !cancelled() }}
|
|
36
|
+
run: npm run lint:report:css
|
|
37
|
+
- name: stylelint sonar import
|
|
38
|
+
if: ${{ !cancelled() }}
|
|
39
|
+
run: npx hfs report stylelint reports/stylelint.json reports/stylelint.sonar.json
|
|
40
|
+
- name: format
|
|
41
|
+
run: npm run format:check
|
|
42
|
+
- name: typecheck
|
|
43
|
+
run: npm run typecheck
|
|
44
|
+
- name: build
|
|
45
|
+
run: npm run build
|
|
46
|
+
- uses: SonarSource/sonarqube-scan-action@v7
|
|
47
|
+
if: ${{ !cancelled() && env.SONAR_TOKEN != '' }}
|
|
48
|
+
env:
|
|
49
|
+
SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }}
|
|
50
|
+
- uses: SonarSource/sonarqube-quality-gate-action@v1
|
|
51
|
+
if: ${{ !cancelled() && env.SONAR_TOKEN != '' }}
|
|
52
|
+
timeout-minutes: 10
|
|
53
|
+
env:
|
|
54
|
+
SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{{header}}
|
|
2
|
+
# Commit gate: the secrets guard, then types, lint and format of the staged files.
|
|
3
|
+
npx hfs work-hygiene
|
|
4
|
+
npm run typecheck
|
|
5
|
+
sources=$(git diff --cached --name-only --diff-filter=ACMR -- '*.ts' '*.tsx' '*.mts' '*.cts' '*.js' '*.jsx' '*.mjs' '*.cjs' || true)
|
|
6
|
+
if [ -n "$sources" ]; then
|
|
7
|
+
npx eslint --max-warnings=0 --no-warn-ignored $sources
|
|
8
|
+
fi
|
|
9
|
+
styles=$(git diff --cached --name-only --diff-filter=ACMR -- '*.css' || true)
|
|
10
|
+
if [ -n "$styles" ]; then
|
|
11
|
+
npx stylelint $styles
|
|
12
|
+
fi
|
|
13
|
+
formatted=$(git diff --cached --name-only --diff-filter=ACMR || true)
|
|
14
|
+
if [ -n "$formatted" ]; then
|
|
15
|
+
npx prettier --check --ignore-unknown $formatted
|
|
16
|
+
fi
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"scripts": {
|
|
3
|
+
"prepare": "husky",
|
|
4
|
+
{{appScripts}}
|
|
5
|
+
"codegen": "npm run codegen --workspaces --if-present",
|
|
6
|
+
"build": "npm run build --workspaces --if-present",
|
|
7
|
+
"typecheck": "npm run typecheck --workspaces --if-present",
|
|
8
|
+
"lint": "npm run codegen --silent && eslint . --fix --max-warnings=0 && stylelint \"{{styleGlob}}\" --fix",
|
|
9
|
+
"lint:check": "npm run codegen --silent && eslint . --max-warnings=0 && stylelint \"{{styleGlob}}\"",
|
|
10
|
+
"lint:report": "eslint . --format json --output-file reports/eslint.json",
|
|
11
|
+
"lint:report:css": "stylelint \"{{styleGlob}}\" --formatter json --output-file reports/stylelint.json",
|
|
12
|
+
"format": "prettier --write .",
|
|
13
|
+
"format:check": "prettier --check .",
|
|
14
|
+
"hfs:check": "hfs check",
|
|
15
|
+
"hfs:report": "hfs check --sonar reports/hfs.sonar.json"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Outcome } from "./outcome"
|
|
2
|
+
|
|
3
|
+
/** How long a request waits before it is abandoned and answered `unavailable`. */
|
|
4
|
+
const REQUEST_TIMEOUT_MS = 8000
|
|
5
|
+
|
|
6
|
+
/** One request of the client: the caller's own signal is joined with the timeout. */
|
|
7
|
+
export interface ClientRequest {
|
|
8
|
+
readonly url: string
|
|
9
|
+
readonly method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"
|
|
10
|
+
readonly body?: unknown
|
|
11
|
+
readonly signal?: AbortSignal
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The status of a response as an Outcome kind; 401 and 403 are `refused`, nothing collapses into `null`. */
|
|
15
|
+
const toOutcome = async (response: Response): Promise<Outcome<unknown>> => {
|
|
16
|
+
if (response.status === 401 || response.status === 403) return { kind: "refused" }
|
|
17
|
+
if (response.status === 404) return { kind: "not-found" }
|
|
18
|
+
if (response.status === 400 || response.status === 422) return { kind: "invalid" }
|
|
19
|
+
if (!response.ok) return { kind: "unavailable" }
|
|
20
|
+
try {
|
|
21
|
+
return { kind: "ok", data: (await response.json()) as unknown }
|
|
22
|
+
} catch {
|
|
23
|
+
return { kind: "unavailable" }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The repository's one fetch. It always carries a timeout signal, never throws, and answers an Outcome; the body is
|
|
29
|
+
* `unknown` until a generated wire type narrows it.
|
|
30
|
+
*/
|
|
31
|
+
export const request = async ({ url, method = "GET", body, signal }: ClientRequest): Promise<Outcome<unknown>> => {
|
|
32
|
+
const timeout = AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
33
|
+
try {
|
|
34
|
+
const response = await fetch(url, {
|
|
35
|
+
method,
|
|
36
|
+
headers: { accept: "application/json", ...(body === undefined ? {} : { "content-type": "application/json" }) },
|
|
37
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
38
|
+
signal: signal === undefined ? timeout : AbortSignal.any([signal, timeout]),
|
|
39
|
+
})
|
|
40
|
+
return await toOutcome(response)
|
|
41
|
+
} catch {
|
|
42
|
+
return { kind: "unavailable" }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** The one result every read of the backend returns: a status is a kind, never a thrown error and never `null`. */
|
|
2
|
+
export type Outcome<T> =
|
|
3
|
+
| { readonly kind: "ok"; readonly data: T }
|
|
4
|
+
| { readonly kind: "refused" }
|
|
5
|
+
| { readonly kind: "invalid" }
|
|
6
|
+
| { readonly kind: "not-found" }
|
|
7
|
+
| { readonly kind: "unavailable" }
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{{header}}
|
|
2
|
+
sonar.projectKey={{sonarKey}}
|
|
3
|
+
sonar.sourceEncoding=UTF-8
|
|
4
|
+
sonar.sources={{sonarRoots}}
|
|
5
|
+
sonar.exclusions=**/.next/**,**/node_modules/**,**/src/messages/**
|
|
6
|
+
sonar.typescript.tsconfigPaths={{tsconfigPaths}}
|
|
7
|
+
sonar.externalIssuesReportPaths=reports/hfs.sonar.json,reports/eslint.sonar.json,reports/stylelint.sonar.json
|
|
8
|
+
sonar.nodejs.maxspace=8192
|
|
@@ -2,7 +2,7 @@ import { hasLocale, NextIntlClientProvider } from "next-intl"
|
|
|
2
2
|
import { getMessages, setRequestLocale } from "next-intl/server"
|
|
3
3
|
import { notFound } from "next/navigation"
|
|
4
4
|
import type { ReactNode } from "react"
|
|
5
|
-
import { routing } from "../../modules/i18n
|
|
5
|
+
import { routing } from "../../modules/i18n"
|
|
6
6
|
import "../globals.css"
|
|
7
7
|
|
|
8
8
|
interface LocaleLayoutProps {
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{{> fe/parts/api-client.ts}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{{> fe/parts/api-outcome.ts}}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { NextConfig } from "next"
|
|
2
|
+
import createNextIntlPlugin from "next-intl/plugin"
|
|
3
|
+
|
|
4
|
+
const withNextIntl = createNextIntlPlugin("./src/modules/i18n/request.ts")
|
|
5
|
+
|
|
6
|
+
/** Next config of the {{app}} app: next-intl wired to the request config, Turbopack rooted at the repository, the shared packages compiled from source. */
|
|
7
|
+
const nextConfig: NextConfig = {
|
|
8
|
+
turbopack: { root: process.cwd() },
|
|
9
|
+
transpilePackages: ["@{{family}}/i18n", "@{{family}}/api"],
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default withNextIntl(nextConfig)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createAppI18n } from "@{{family}}/i18n"
|
|
2
|
+
|
|
3
|
+
/** This app's locales and the next-intl stack (routing, navigation) built from them by the shared i18n package. */
|
|
4
|
+
const i18n = createAppI18n({ locales: ["vi"], defaultLocale: "vi" })
|
|
5
|
+
|
|
6
|
+
export const { DEFAULT_LOCALE, LOCALES, getPathname, Link, redirect, routing, usePathname, useRouter } = i18n
|
|
7
|
+
|
|
8
|
+
/** A served locale. */
|
|
9
|
+
export type Locale = (typeof LOCALES)[number]
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { createRequestConfig } from "@{{family}}/i18n/request"
|
|
2
|
+
import { routing } from "./index"
|
|
3
|
+
|
|
4
|
+
/** Loads the catalog of the requested locale from this app's `messages/`; the shared package resolves the locale. */
|
|
5
|
+
export default createRequestConfig(routing, async (locale) => (await import(`./messages/${locale}.json`)).default)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{{> fe/parts/api-client.ts}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{{> fe/parts/api-outcome.ts}}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@{{family}}/i18n",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.ts",
|
|
8
|
+
"./proxy": "./src/proxy.ts",
|
|
9
|
+
"./request": "./src/request.ts"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"typecheck": "tsc --noEmit --pretty false"
|
|
13
|
+
},
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"next": ">=16",
|
|
16
|
+
"next-intl": ">=4"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createNavigation } from "next-intl/navigation"
|
|
2
|
+
import { defineRouting } from "next-intl/routing"
|
|
3
|
+
|
|
4
|
+
/** The locales an app serves and the one it serves at the unprefixed path. */
|
|
5
|
+
export interface AppI18nOptions<Locale extends string> {
|
|
6
|
+
readonly locales: readonly [Locale, ...Locale[]]
|
|
7
|
+
readonly defaultLocale: Locale
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The next-intl stack of one app, written once for the repository: one routing table (default locale unprefixed) and the
|
|
12
|
+
* locale-aware navigation built on it. Components import `Link` and `redirect` from their app's i18n module, never from
|
|
13
|
+
* `next/link` or `next/navigation`. The proxy and the request config are the `./proxy` and `./request` entries, so a client
|
|
14
|
+
* bundle that imports this one carries no server code.
|
|
15
|
+
*/
|
|
16
|
+
export const createAppI18n = <const Locale extends string>({ locales, defaultLocale }: AppI18nOptions<Locale>) => {
|
|
17
|
+
const routing = defineRouting({ locales, defaultLocale, localePrefix: "as-needed" })
|
|
18
|
+
return { LOCALES: locales, DEFAULT_LOCALE: defaultLocale, routing, ...createNavigation(routing) }
|
|
19
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import createMiddleware from "next-intl/middleware"
|
|
2
|
+
import type { defineRouting } from "next-intl/routing"
|
|
3
|
+
import { NextResponse, type NextRequest } from "next/server"
|
|
4
|
+
|
|
5
|
+
/** The API, the health probe, framework files and files with an extension are never locale-negotiated. */
|
|
6
|
+
const UNNEGOTIATED = /^\/(?:api|health|_next|_vercel)(?:\/|$)|\.[^/]+$/
|
|
7
|
+
|
|
8
|
+
/** The proxy of an app: negotiates the locale and redirects, and lets everything that is not a page through untouched. */
|
|
9
|
+
export const createProxy = (routing: ReturnType<typeof defineRouting>) => {
|
|
10
|
+
const negotiate = createMiddleware(routing)
|
|
11
|
+
return (request: NextRequest) => (UNNEGOTIATED.test(request.nextUrl.pathname) ? NextResponse.next() : negotiate(request))
|
|
12
|
+
}
|