@starci/hfs 5.0.7 → 5.1.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.
Files changed (204) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +7 -8
  3. package/cache/fake-session.mjs +1 -1
  4. package/cache/hfs-family.mjs +22 -9
  5. package/cache/import-graph.d.mts +32 -4
  6. package/cache/import-graph.mjs +88 -11
  7. package/cache/lint-family.d.mts +10 -3
  8. package/cache/lint-family.mjs +65 -37
  9. package/cache/records.d.mts +7 -0
  10. package/cache/records.mjs +17 -1
  11. package/cache/run.d.mts +1 -1
  12. package/cache/session.mjs +6 -2
  13. package/cache/side-key.d.mts +2 -0
  14. package/cache/side-key.mjs +5 -1
  15. package/cache/structure-memo.d.mts +34 -0
  16. package/cache/structure-memo.mjs +150 -0
  17. package/cache/types.d.mts +6 -2
  18. package/cache/wiring-family.mjs +1 -1
  19. package/codemod/directive-first.d.mts +48 -0
  20. package/codemod/directive-first.mjs +136 -0
  21. package/codemod/explicit-surface-edit.d.mts +14 -0
  22. package/codemod/explicit-surface-edit.mjs +57 -0
  23. package/codemod/explicit-surface-names.d.mts +30 -0
  24. package/codemod/explicit-surface-names.mjs +135 -0
  25. package/codemod/explicit-surface-render.d.mts +29 -0
  26. package/codemod/explicit-surface-render.mjs +197 -0
  27. package/codemod/explicit-surface.d.mts +50 -0
  28. package/codemod/explicit-surface.mjs +374 -0
  29. package/codemod/main.d.mts +7 -5
  30. package/codemod/main.mjs +37 -16
  31. package/codemod/small-renames.mjs +22 -2
  32. package/codemod/source-tree.d.mts +8 -0
  33. package/codemod/source-tree.mjs +23 -0
  34. package/lint/eslint-list.cjs +55 -0
  35. package/lint/run.d.mts +35 -61
  36. package/lint/run.mjs +91 -288
  37. package/package.json +1 -1
  38. package/release/lint-report.d.mts +1 -1
  39. package/release/lint-report.mjs +3 -3
  40. package/report/sonar.d.mts +1 -1
  41. package/report/sonar.mjs +3 -3
  42. package/report/stylelint-findings.d.mts +23 -0
  43. package/report/stylelint-findings.mjs +9 -0
  44. package/runtime/knowledge/hfs/canon-pins.yaml +8 -8
  45. package/runtime/knowledge/hfs/ci-standard.yaml +2 -2
  46. package/runtime/knowledge/hfs/rules.yaml +56 -5
  47. package/runtime/knowledge/hfs/slots.yaml +1 -1
  48. package/runtime/knowledge/sonar-gate.yaml +2 -2
  49. package/runtime/modules/kernel/failure-codes.yaml +4 -4
  50. package/runtime/scripts/api/node/run-node.d.mts +5 -0
  51. package/runtime/scripts/hfs/architecture/test-world-files.d.mts +4 -3
  52. package/runtime/scripts/hfs/architecture/test-world-files.mjs +26 -7
  53. package/runtime/scripts/hfs/check.d.mts +16 -1
  54. package/runtime/scripts/hfs/check.mjs +18 -7
  55. package/runtime/scripts/hfs/explicit-surface-rule.d.mts +58 -0
  56. package/runtime/scripts/hfs/explicit-surface-rule.mjs +59 -0
  57. package/runtime/scripts/hfs/gate-threshold.mjs +1 -1
  58. package/runtime/scripts/hfs/rule-memo.d.mts +30 -0
  59. package/runtime/scripts/hfs/rule-memo.mjs +24 -0
  60. package/runtime/scripts/hfs/rules/a15-findings.d.mts +6 -0
  61. package/runtime/scripts/hfs/rules/a15-findings.mjs +11 -2
  62. package/runtime/scripts/hfs/rules/ci-standard.mjs +2 -2
  63. package/runtime/scripts/hfs/rules/monorepo.d.mts +1 -3
  64. package/runtime/scripts/hfs/rules/monorepo.mjs +9 -11
  65. package/runtime/scripts/hfs/rules/pipeline.mjs +13 -9
  66. package/runtime/scripts/hfs/rules/read.d.mts +5 -0
  67. package/runtime/scripts/hfs/rules/read.mjs +13 -0
  68. package/runtime/scripts/hfs/rules/secrets.d.mts +2 -0
  69. package/runtime/scripts/hfs/rules/secrets.mjs +4 -1
  70. package/runtime/scripts/hfs/rules/stacks-release.mjs +35 -21
  71. package/runtime/scripts/lib/declaration-surface.d.mts +35 -0
  72. package/runtime/scripts/lib/declaration-surface.mjs +112 -0
  73. package/runtime/scripts/lib/worktree-exclude.mjs +1 -1
  74. package/scaffold/app.mjs +3 -3
  75. package/src/check-cache-structure-support.d.mts +52 -0
  76. package/src/main.d.mts +3 -2
  77. package/src/main.mjs +123 -109
  78. package/sync/ci-commands.d.mts +2 -1
  79. package/sync/ci-commands.mjs +19 -20
  80. package/sync/index.mjs +2 -2
  81. package/templates/app/package-scripts/core.json +2 -2
  82. package/templates/app/task-graph/turbo.json +0 -3
  83. package/templates/be/inbox/delivery-recorded.event.ts.tpl +2 -2
  84. package/templates/be/inbox/entities.index.ts.tpl +1 -1
  85. package/templates/be/inbox/module-definition.ts.tpl +8 -2
  86. package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +10 -2
  87. package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +8 -2
  88. package/templates/be/patterns/api/feature.module-definition.ts.tpl +9 -3
  89. package/templates/be/patterns/api/graphql.module-definition.ts.tpl +9 -3
  90. package/templates/be/patterns/cli/group.module-definition.ts.tpl +9 -3
  91. package/templates/be/patterns/event-bus/platform/event-bus.module-definition.ts.tpl +8 -3
  92. package/templates/be/patterns/event-bus/platform/persistence/connection.ts.tpl +3 -2
  93. package/templates/be/patterns/event-bus/platform/persistence/event-bus.sql.ts.tpl +4 -3
  94. package/templates/be/patterns/idempotency/platform/inbox/inbox.module-definition.ts.tpl +8 -3
  95. package/templates/be/patterns/idempotency/platform/inbox/persistence/connection.ts.tpl +3 -2
  96. package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.sql.ts.tpl +7 -6
  97. package/templates/be/patterns/jobs/job.module-definition.ts.tpl +9 -3
  98. package/templates/be/patterns/jobs/job.processor.ts.tpl +1 -1
  99. package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +9 -3
  100. package/templates/be/patterns/jobs/platform/jobs.module-definition.ts.tpl +8 -3
  101. package/templates/be/patterns/jobs/platform/persistence.connection.ts.tpl +3 -2
  102. package/templates/be/patterns/jobs/platform/persistence.jobs.sql.ts.tpl +5 -4
  103. package/templates/be/patterns/projections/persistence.connection.ts.tpl +3 -2
  104. package/templates/be/patterns/projections/projection.module-definition.ts.tpl +8 -3
  105. package/templates/be/patterns/queues/platform/persistence.connection.ts.tpl +3 -2
  106. package/templates/be/patterns/queues/platform/persistence.queue.sql.ts.tpl +4 -3
  107. package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +8 -3
  108. package/templates/be/patterns/queues/queue.module-definition.ts.tpl +9 -3
  109. package/templates/be/patterns/reactors/consumer.ts.tpl +1 -1
  110. package/templates/be/patterns/reactors/message.module-definition.ts.tpl +9 -3
  111. package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +9 -3
  112. package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +9 -3
  113. package/templates/be/patterns/realtime/channel.sse.module-definition.ts.tpl +9 -3
  114. package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +9 -3
  115. package/templates/be/patterns/saga/saga.done.consumer.ts.tpl +1 -1
  116. package/templates/be/patterns/saga/saga.failed.consumer.ts.tpl +1 -1
  117. package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +9 -3
  118. package/templates/be/patterns/saga/saga.module-definition.ts.tpl +9 -3
  119. package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +9 -3
  120. package/templates/be/platform/graphql/graphql.module-definition.ts +8 -3
  121. package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +9 -3
  122. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +9 -3
  123. package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +9 -3
  124. package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +9 -3
  125. package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +9 -3
  126. package/templates/be/skeleton/src/modules/domain/identity/identity.module-definition.ts +8 -3
  127. package/templates/be/skeleton/src/modules/domain/liveness/liveness.module-definition.ts +8 -3
  128. package/templates/be/skeleton/src/modules/domain/note/note.module-definition.ts +8 -3
  129. package/templates/be/skeleton/src/modules/domain/note/persistence/connection.ts +3 -2
  130. package/templates/be/skeleton/src/modules/domain/note/persistence/note.sql.ts +2 -1
  131. package/templates/be/skeleton/src/modules/platform/clock/clock.module-definition.ts +8 -3
  132. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module-definition.ts +8 -3
  133. package/templates/be/skeleton/src/modules/platform/database/database.module-definition.ts +8 -3
  134. package/templates/be/skeleton/src/modules/platform/database/index.ts +1 -0
  135. package/templates/be/skeleton/src/modules/platform/errors/errors.module-definition.ts +10 -4
  136. package/templates/be/skeleton/src/modules/platform/http/http.module-definition.ts +8 -4
  137. package/templates/be/skeleton/src/modules/platform/http-security/http-security.module-definition.ts +10 -4
  138. package/templates/be/skeleton/src/modules/platform/http-security/persistence/connection.ts +3 -2
  139. package/templates/be/skeleton/src/modules/platform/http-security/persistence/rate-limit.sql.ts +3 -2
  140. package/templates/be/skeleton/src/modules/platform/i18n/i18n.module-definition.ts +10 -4
  141. package/templates/be/skeleton/src/modules/platform/ids/ids.module-definition.ts +8 -3
  142. package/templates/be/skeleton/src/modules/platform/logging/logging.module-definition.ts +8 -3
  143. package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +8 -3
  144. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +1 -1
  145. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/google.strategy.ts +5 -1
  146. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/local.strategy.ts +5 -1
  147. package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +1 -1
  148. package/templates/be/skeleton-lite/src/modules/platform/http-security/persistence/entities/index.ts +1 -1
  149. package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +7 -1
  150. package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +7 -1
  151. package/templates/be/table/api/feature.module-definition.ts.tpl +9 -3
  152. package/templates/be/table/api/graphql.module-definition.ts.tpl +9 -3
  153. package/templates/be/table/entities.index.ts.tpl +1 -1
  154. package/templates/be/table/module-definition.ts.tpl +8 -3
  155. package/templates/be/test-world/fixtures/migrations.sql.ts.tpl +1 -0
  156. package/templates/be/test-world/world/test-world.config.ts.tpl +8 -1
  157. package/templates/fe/common/app-layout.tsx.partial +1 -1
  158. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +2 -1
  159. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignIn.ts +11 -3
  160. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +8 -2
  161. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/store.ts +2 -1
  162. package/templates/fe/skeleton/apps/__app__/src/modules/toast/hooks.ts +1 -1
  163. package/templates/fe/skeleton/apps/__app__/src/modules/toast/toast.tsx +4 -1
  164. package/templates/fe/skeleton/apps/__app__/src/redux/hooks.ts +3 -2
  165. package/templates/fe/skeleton/apps/__app__/src/redux/index.ts +1 -0
  166. package/templates/fe/skeleton/apps/__app__/src/redux/slices/session.ts +12 -4
  167. package/templates/fe/skeleton/apps/__app__/src/redux/store.ts +8 -3
  168. package/templates/fe/skeleton/apps/app/src/app/[locale]/layout.tsx +4 -3
  169. package/templates/fe/skeleton/apps/app/src/features/layouts/AppLayout/index.tsx +1 -1
  170. package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +2 -1
  171. package/templates/fe/skeleton/apps/app/src/features/pages/AppLoadingPage/index.tsx +2 -1
  172. package/templates/fe/skeleton/apps/app/src/features/pages/AppNotFoundPage/index.tsx +2 -1
  173. package/templates/fe/skeleton/apps/app/src/modules/i18n/index.ts +2 -1
  174. package/templates/fe/skeleton/apps/app/src/modules/i18n/request.ts +4 -1
  175. package/templates/fe/skeleton/apps/landing/src/app/[locale]/layout.tsx +4 -3
  176. package/templates/fe/skeleton/apps/landing/src/features/layouts/LandingLayout/index.tsx +2 -2
  177. package/templates/fe/skeleton/apps/landing/src/features/pages/LandingLoadingPage/index.tsx +2 -1
  178. package/templates/fe/skeleton/apps/landing/src/features/pages/LandingNotFoundPage/index.tsx +2 -1
  179. package/templates/fe/skeleton/apps/landing/src/features/pages/LandingPage/index.tsx +2 -1
  180. package/templates/fe/skeleton/apps/landing/src/modules/i18n/index.ts +2 -1
  181. package/templates/fe/skeleton/apps/landing/src/modules/i18n/request.ts +4 -1
  182. package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +19 -1
  183. package/templates/fe/skeleton/packages/__project__-i18n/src/proxy.ts +1 -1
  184. package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +5 -1
  185. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +4 -3
  186. package/templates/fe/skeleton-lite/apps/web/src/features/layouts/AppLayout/index.tsx +2 -2
  187. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +2 -1
  188. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppLoadingPage/index.tsx +2 -1
  189. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppNotFoundPage/index.tsx +2 -1
  190. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +3 -1
  191. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +5 -1
  192. package/templates/fe/table/graphql/useMutateSwr.ts.tpl +2 -1
  193. package/templates/fe/table/graphql/useQuerySwr.ts.tpl +2 -1
  194. package/templates/fe/transport/graphql/clients/clients/auth.ts +1 -1
  195. package/templates/fe/transport/graphql/clients/clients/no-auth.ts +1 -1
  196. package/templates/fe/transport/graphql/clients/links/attach-access-token.ts +1 -1
  197. package/templates/fe/transport/graphql/clients/links/error.ts +1 -1
  198. package/templates/fe/transport/graphql/clients/links/http.ts +1 -1
  199. package/templates/fe/transport/graphql/clients/links/locale.ts +1 -1
  200. package/templates/fe/transport/graphql/clients/links/refresh-token.ts +1 -1
  201. package/templates/fe/transport/graphql/clients/links/retry.ts +1 -1
  202. package/templates/fe/transport/graphql/clients/links/timeout.ts +1 -1
  203. package/templates/fe/units/page/index.tsx.tpl +3 -2
  204. package/templates/fe/units/slot/SlotView.tsx.tpl +1 -1
@@ -1,4 +1,5 @@
1
1
  import { Injectable } from "@nestjs/common"
2
+ import type { Type } from "@nestjs/common"
2
3
  import { PassportStrategy } from "@nestjs/passport"
3
4
  import { Strategy } from "passport-local"
4
5
  import { PassportError, PassportErrorCode } from "../errors/passport.error"
@@ -18,9 +19,12 @@ export interface PasswordAccount {
18
19
  /** The lookup the identity domain provides: the account of one email, or undefined. */
19
20
  export type FindPasswordAccount = (email: string) => Promise<PasswordAccount | undefined>
20
21
 
22
+ /** The Passport strategy base of the local scheme, typed as the class the strategy extends. */
23
+ const LocalStrategyBase: Type<object> = PassportStrategy(Strategy, "local")
24
+
21
25
  @Injectable()
22
26
  /** Email and password sign-in: bcrypt against the stored hash; a miss and a wrong password fail alike. */
23
- export class LocalStrategy extends PassportStrategy(Strategy, "local") {
27
+ export class LocalStrategy extends LocalStrategyBase {
24
28
  constructor(@InjectPasswordAccounts() private readonly findAccount: FindPasswordAccount) {
25
29
  super({ usernameField: "email", passwordField: "password" })
26
30
  }
@@ -5,7 +5,7 @@ import type { DatabaseOptions } from "./database.options"
5
5
  import { PRIMARY_CONNECTION } from "./primary.connection"
6
6
 
7
7
  /** The primary pool's override token for infrastructure fixtures; TypeORM tokens stay owned by this capability. */
8
- export const PRIMARY_DATA_SOURCE = getDataSourceToken(PRIMARY_CONNECTION)
8
+ export const PRIMARY_DATA_SOURCE: ReturnType<typeof getDataSourceToken> = getDataSourceToken(PRIMARY_CONNECTION)
9
9
 
10
10
  /** Token of the options of the database capability. */
11
11
  export const DATABASE_OPTIONS: unique symbol = Symbol("platform.database.options")
@@ -1,4 +1,4 @@
1
1
  import { RateWindowEntity } from "./rate-window.entity"
2
2
 
3
3
  /** The entities of the http-security capability, named in PRIMARY_ENTITIES of platform/database/entities.ts. */
4
- export const httpSecurityEntities = [RateWindowEntity]
4
+ export const httpSecurityEntities: ReadonlyArray<typeof RateWindowEntity> = [RateWindowEntity]
@@ -4,8 +4,14 @@ import { ACCESS_TOKEN_VERIFIER } from "@modules/domain/identity"
4
4
  import { OWN_TOKEN_VERIFIER } from "@modules/integrations/passport"
5
5
  import { OBJECT_STORAGE } from "@modules/integrations/object-storage"
6
6
 
7
+ /** What a wiring proof hands the compiled root: public options and the infrastructure doubles. */
8
+ export interface WiringProof {
9
+ readonly options: Readonly<Record<string, unknown>>
10
+ readonly overrides: ReadonlyArray<{ readonly token: unknown; readonly value: unknown }>
11
+ }
12
+
7
13
  /** Compiles the real API root with public configuration and explicit infrastructure doubles. */
8
- export const buildWiringProof = () => ({
14
+ export const buildWiringProof = (): WiringProof => ({
9
15
  options: {
10
16
  server: { port: 3000 },
11
17
  httpSecurity: {
@@ -2,8 +2,14 @@ import { getDataSourceToken } from "@nestjs/typeorm"
2
2
  import { Secret } from "@modules/platform/config"
3
3
  import { PRIMARY_CONNECTION } from "@modules/platform/database"
4
4
 
5
+ /** What a wiring proof hands the compiled root: public options and the infrastructure doubles. */
6
+ export interface WiringProof {
7
+ readonly options: Readonly<Record<string, unknown>>
8
+ readonly overrides: ReadonlyArray<{ readonly token: unknown; readonly value: unknown }>
9
+ }
10
+
5
11
  /** Public fixture options and the database double for compiling the real CLI AppModule without network access. */
6
- export const buildWiringProof = () => ({
12
+ export const buildWiringProof = (): WiringProof => ({
7
13
  options: {
8
14
  connections: [{
9
15
  name: PRIMARY_CONNECTION,
@@ -1,6 +1,12 @@
1
- import { ConfigurableModuleBuilder } from "@nestjs/common"
1
+ import { ConfigurableModuleBuilder, type ConfigurableModuleHost } from "@nestjs/common"
2
2
  import type { {{Name}}ApiOptions } from "./{{name}}.options"
3
3
 
4
+ const built = new ConfigurableModuleBuilder<{{Name}}ApiOptions>().setClassMethodName("register").build()
5
+ type Host = ConfigurableModuleHost<{{Name}}ApiOptions, "register", "create", {}>
6
+
4
7
  /** The configurable-module base of the {{name}} module. */
5
- export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE } =
6
- new ConfigurableModuleBuilder<{{Name}}ApiOptions>().setClassMethodName("register").build()
8
+ export const ConfigurableModuleClass: Host["ConfigurableModuleClass"] = built.ConfigurableModuleClass
9
+ /** The injection token of the registered options. */
10
+ export const MODULE_OPTIONS_TOKEN: Host["MODULE_OPTIONS_TOKEN"] = built.MODULE_OPTIONS_TOKEN
11
+ /** The options of a synchronous registration, for the register signature. */
12
+ export const OPTIONS_TYPE: Host["OPTIONS_TYPE"] = built.OPTIONS_TYPE
@@ -1,6 +1,12 @@
1
- import { ConfigurableModuleBuilder } from "@nestjs/common"
1
+ import { ConfigurableModuleBuilder, type ConfigurableModuleHost } from "@nestjs/common"
2
2
  import type { {{Name}}GraphqlOptions } from "./{{name}}-graphql.options"
3
3
 
4
+ const built = new ConfigurableModuleBuilder<{{Name}}GraphqlOptions>().setClassMethodName("register").build()
5
+ type Host = ConfigurableModuleHost<{{Name}}GraphqlOptions, "register", "create", {}>
6
+
4
7
  /** The configurable-module base of the {{name}}-graphql module. */
5
- export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE } =
6
- new ConfigurableModuleBuilder<{{Name}}GraphqlOptions>().setClassMethodName("register").build()
8
+ export const ConfigurableModuleClass: Host["ConfigurableModuleClass"] = built.ConfigurableModuleClass
9
+ /** The injection token of the registered options. */
10
+ export const MODULE_OPTIONS_TOKEN: Host["MODULE_OPTIONS_TOKEN"] = built.MODULE_OPTIONS_TOKEN
11
+ /** The options of a synchronous registration, for the register signature. */
12
+ export const OPTIONS_TYPE: Host["OPTIONS_TYPE"] = built.OPTIONS_TYPE
@@ -1,4 +1,4 @@
1
1
  import { {{Singular}}Entity } from "./{{name}}.entity"
2
2
 
3
3
  /** The entities of the {{name}} capability, named in PRIMARY_ENTITIES of platform/database/entities.ts. */
4
- export const {{nameCamel}}Entities = [{{Singular}}Entity]
4
+ export const {{nameCamel}}Entities: ReadonlyArray<typeof {{Singular}}Entity> = [{{Singular}}Entity]
@@ -1,7 +1,12 @@
1
- import { ConfigurableModuleBuilder } from "@nestjs/common"
1
+ import { ConfigurableModuleBuilder, type ConfigurableModuleHost } from "@nestjs/common"
2
2
  import type { {{Name}}Options } from "./{{name}}.options"
3
3
 
4
- /** The configurable-module base of the {{name}} capability. */
5
- export const { ConfigurableModuleClass, OPTIONS_TYPE } = new ConfigurableModuleBuilder<{{Name}}Options>()
4
+ const built = new ConfigurableModuleBuilder<{{Name}}Options>()
6
5
  .setExtras({ isGlobal: false }, (definition, extras) => ({ ...definition, global: extras.isGlobal }))
7
6
  .build()
7
+ type Host = ConfigurableModuleHost<{{Name}}Options, "register", "create", { isGlobal: boolean }>
8
+
9
+ /** The configurable-module base of the {{name}} capability. */
10
+ export const ConfigurableModuleClass: Host["ConfigurableModuleClass"] = built.ConfigurableModuleClass
11
+ /** The options of a synchronous registration, for the register signature. */
12
+ export const OPTIONS_TYPE: Host["OPTIONS_TYPE"] = built.OPTIONS_TYPE
@@ -1,5 +1,6 @@
1
1
  import type { EntityManager } from "typeorm"
2
2
  import { sql } from "@modules/platform/database"
3
+ import type { SqlText } from "@modules/platform/database"
3
4
 
4
5
  /** One row of a connection's migration ledger: the name of a migration the cli migrate step applied. */
5
6
  export interface AppliedMigrationRow {
@@ -5,10 +5,11 @@
5
5
  */
6
6
  import { applyCorrelation } from "@modules/platform/logging"
7
7
  import { defineTestWorld } from "@starci/test-world"
8
+ import type { TestWorldApi } from "@starci/test-world"
8
9
  {{appModuleImports}}
9
10
  import { {{optionsImports}} } from "./test-apps.options"
10
11
 
11
- export const { useTestWorld, useSandbox } = defineTestWorld({
12
+ const world: TestWorldApi = defineTestWorld({
12
13
  stack: ".starcistacks/dev",
13
14
  stacks: {
14
15
  postgresql: { connections: [{{connectionDeclarations}}] },
@@ -19,3 +20,9 @@ export const { useTestWorld, useSandbox } = defineTestWorld({
19
20
  migrate: { module: migrateCli, options: cliOptionsOf },
20
21
  modules: { base: platformBase },
21
22
  })
23
+
24
+ /** Registers the hooks that boot and close the world around a spec and answers its handle; call it inside `describe`. */
25
+ export const useTestWorld: TestWorldApi["useTestWorld"] = world.useTestWorld
26
+
27
+ /** The contract layer: the real integration client against a provider sandbox, in a `contract-spec`. */
28
+ export const useSandbox: TestWorldApi["useSandbox"] = world.useSandbox
@@ -1,4 +1,4 @@
1
- export const AppLayout = async (props: AppLayoutProps) => {
1
+ export const AppLayout = async (props: AppLayoutProps): Promise<ReactElement> => {
2
2
  const t = await getTranslations("app.shell")
3
3
  return (
4
4
  <SiteShell brand={t("brand")} homeHref={APP_ROUTES.home}>
@@ -2,9 +2,10 @@
2
2
 
3
3
  import { useDebugValue } from "react"
4
4
  import { useAppSelector } from "@/redux"
5
+ import type { SessionState } from "@/redux"
5
6
 
6
7
  /** The reader's session as the app shares it (redux, kept by the auth session effect): unknown, signed in or signed out. */
7
- export const useSession = () => {
8
+ export const useSession = (): SessionState => {
8
9
  const session = useAppSelector((state) => state.session)
9
10
  useDebugValue(session.status)
10
11
  return session
@@ -12,18 +12,26 @@ const FAILURES: readonly SignInFailure[] = ["refused", "invalid", "unavailable"]
12
12
  /** The failure of a sign-in's failure kind. */
13
13
  const failureOf = (kind: string): SignInFailure => FAILURES.find((each) => each === kind) ?? "unavailable"
14
14
 
15
+ /** What the sign-in hook answers: whether a request runs, why the last one failed, the sign-in itself and `reject`. */
16
+ export interface SignIn {
17
+ readonly failure: SignInFailure | undefined
18
+ readonly pending: boolean
19
+ readonly signIn: (email: string, password: string) => void
20
+ readonly reject: () => void
21
+ }
22
+
15
23
  /**
16
24
  * Signs the reader in with an email and a password through modules/auth: whether a request runs, why the last one failed,
17
25
  * the sign-in itself and `reject` for input the form could not read. A success needs no state here: the session effect
18
26
  * moves the reader to signed in.
19
27
  */
20
- export const useSignIn = () => {
28
+ export const useSignIn = (): SignIn => {
21
29
  const [failure, setFailure] = useState<SignInFailure>()
22
30
  const [pending, startTransition] = useTransition()
23
- const signIn = (email: string, password: string) =>
31
+ const signIn = (email: string, password: string): void =>
24
32
  startTransition(async () => {
25
33
  const failed = await authClient.signInWithPassword(email, password)
26
34
  setFailure(failed === undefined ? undefined : failureOf(failed.kind))
27
35
  })
28
- return { failure, pending, signIn, reject: () => setFailure("invalid") }
36
+ return { failure, pending, signIn, reject: (): void => setFailure("invalid") }
29
37
  }
@@ -11,14 +11,20 @@ const responseOf = (failure: AuthFailure | undefined) =>
11
11
  ? { success: true as const, data: null }
12
12
  : { success: false as const, error: failure.code, kind: failure.kind, message: failure.message }
13
13
 
14
+ /** What the sign-out hook answers: whether the request runs and the sign-out itself. */
15
+ export interface SignOut {
16
+ readonly pending: boolean
17
+ readonly signOut: () => void
18
+ }
19
+
14
20
  /**
15
21
  * Ends the reader's session through modules/auth: whether the request runs and the sign-out itself. A failure is reported by
16
22
  * modules/toast; a success needs no toast, the session effect moves the reader to signed out.
17
23
  */
18
- export const useSignOut = () => {
24
+ export const useSignOut = (): SignOut => {
19
25
  const withToast = useGraphQLWithToast()
20
26
  const [pending, startTransition] = useTransition()
21
- const signOut = () =>
27
+ const signOut = (): void =>
22
28
  startTransition(async () => {
23
29
  await withToast(async () => responseOf(await authClient.signOut()), { showSuccessToast: false })
24
30
  })
@@ -1,6 +1,7 @@
1
1
  "use client"
2
2
 
3
3
  import { create } from "zustand"
4
+ import type { StoreApi, UseBoundStore } from "zustand"
4
5
 
5
6
  /** The home guide's local UI state; it carries no session or server data. */
6
7
  type AppGuideState = {
@@ -9,7 +10,7 @@ type AppGuideState = {
9
10
  }
10
11
 
11
12
  /** One feature-local store, shared by the guide trigger and its controlled dialog. */
12
- export const useAppGuideStore = create<AppGuideState>()((set) => ({
13
+ export const useAppGuideStore: UseBoundStore<StoreApi<AppGuideState>> = create<AppGuideState>()((set) => ({
13
14
  isOpen: false,
14
15
  setOpen: (isOpen) => set({ isOpen }),
15
16
  }))
@@ -23,7 +23,7 @@ const useToastMessages = (): ToastMessages => {
23
23
  }
24
24
 
25
25
  /** A localized runGraphQLWithToast: every GraphQL write reports its outcome through it. */
26
- export const useGraphQLWithToast = () => {
26
+ export const useGraphQLWithToast = (): (<T>(action: () => Promise<GraphQLResponse<T>>, options?: RunWithToastOptions) => Promise<GraphQLResponse<T>>) => {
27
27
  const messages = useToastMessages()
28
28
  return useCallback(
29
29
  <T>(action: () => Promise<GraphQLResponse<T>>, options?: RunWithToastOptions) =>
@@ -18,8 +18,11 @@ const INDICATOR: Record<"success" | "danger" | "warning" | "info", ReactNode> =
18
18
  info: <InformationCircleIcon className="size-6" />,
19
19
  }
20
20
 
21
+ /** The app's toast: HeroUI's, with the four status methods wrapped. */
22
+ type AppToast = typeof heroToast
23
+
21
24
  /** The app's toast: HeroUI's imperative toast with the status indicator injected; the region is mounted by the providers. */
22
- export const toast = {
25
+ export const toast: AppToast = {
23
26
  ...heroToast,
24
27
  success: (message: ReactNode, options?: StatusToastOptions) =>
25
28
  heroToast.success(message, { indicator: INDICATOR.success, ...options }),
@@ -1,8 +1,9 @@
1
1
  import { useDispatch, useSelector } from "react-redux"
2
+ import type { UseDispatch, UseSelector } from "react-redux"
2
3
  import type { AppDispatch, RootState } from "./store"
3
4
 
4
5
  /** The typed dispatch of the app's store. */
5
- export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
6
+ export const useAppDispatch: UseDispatch<AppDispatch> = useDispatch.withTypes<AppDispatch>()
6
7
 
7
8
  /** The typed selector of the app's store. */
8
- export const useAppSelector = useSelector.withTypes<RootState>()
9
+ export const useAppSelector: UseSelector<RootState> = useSelector.withTypes<RootState>()
@@ -1,3 +1,4 @@
1
1
  export { useAppDispatch, useAppSelector } from "./hooks"
2
2
  export { ReduxProvider } from "./ReduxProvider"
3
3
  export { sessionChanged } from "./slices/session"
4
+ export type { SessionState } from "./slices/session"
@@ -1,5 +1,5 @@
1
1
  import { createSlice } from "@reduxjs/toolkit"
2
- import type { PayloadAction } from "@reduxjs/toolkit"
2
+ import type { CaseReducer, PayloadAction, Reducer, Slice } from "@reduxjs/toolkit"
3
3
  import type { AuthSession } from "@/modules/auth"
4
4
 
5
5
  /** The reader's session as the app shares it: unknown until modules/auth answers, then signed in or signed out. */
@@ -8,10 +8,18 @@ export interface SessionState {
8
8
  readonly session?: AuthSession
9
9
  }
10
10
 
11
+ /** The reducers of the session slice. */
12
+ interface SessionReducers {
13
+ sessionChanged: CaseReducer<SessionState, PayloadAction<AuthSession | undefined>>
14
+ }
15
+
16
+ /** The session slice as the store and the effects see it. */
17
+ type SessionSlice = Slice<SessionState, SessionReducers, "session">
18
+
11
19
  const initialState: SessionState = { status: "unknown" }
12
20
 
13
21
  /** The session slice: written only by the auth session effect (hooks/effects), read by any component through useAppSelector. */
14
- export const sessionSlice = createSlice({
22
+ export const sessionSlice: SessionSlice = createSlice({
15
23
  name: "session",
16
24
  initialState,
17
25
  reducers: {
@@ -21,6 +29,6 @@ export const sessionSlice = createSlice({
21
29
  })
22
30
 
23
31
  /** The one action of the session slice: modules/auth reported a session (or none). */
24
- export const { sessionChanged } = sessionSlice.actions
32
+ export const sessionChanged: SessionSlice["actions"]["sessionChanged"] = sessionSlice.actions.sessionChanged
25
33
  /** The reducer the store mounts under `session`. */
26
- export const sessionReducer = sessionSlice.reducer
34
+ export const sessionReducer: Reducer<SessionState> = sessionSlice.reducer
@@ -1,14 +1,19 @@
1
1
  import { configureStore } from "@reduxjs/toolkit"
2
+ import type { EnhancedStore } from "@reduxjs/toolkit"
2
3
  import { sessionReducer } from "./slices/session"
4
+ import type { SessionState } from "./slices/session"
5
+
6
+ /** The state of the store, for useAppSelector: one slice per key of the root reducer. */
7
+ export interface RootState {
8
+ readonly session: SessionState
9
+ }
3
10
 
4
11
  /** The one store of the app: app-wide shared state only (feature-local state lives in hooks/zustand). */
5
- export const store = configureStore({
12
+ export const store: EnhancedStore<RootState> = configureStore({
6
13
  reducer: {
7
14
  session: sessionReducer,
8
15
  },
9
16
  })
10
17
 
11
- /** The state of the store, for useAppSelector. */
12
- export type RootState = ReturnType<typeof store.getState>
13
18
  /** The dispatch of the store, for useAppDispatch. */
14
19
  export type AppDispatch = typeof store.dispatch
@@ -1,4 +1,5 @@
1
- import type { ReactNode } from "react"
1
+ import type { Metadata } from "next"
2
+ import type { ReactElement, ReactNode } from "react"
2
3
  import { LocaleShell } from "@{{project}}/ui"
3
4
  import { AppProviders } from "@/components/providers/AppProviders"
4
5
  import { AppLayout, appLayoutMetadata } from "@/features/layouts/AppLayout"
@@ -11,10 +12,10 @@ type LayoutProps = {
11
12
  }
12
13
 
13
14
  /** The localized document title of this route segment. */
14
- export const generateMetadata = async (props: LayoutProps) => appLayoutMetadata((await props.params).locale)
15
+ export const generateMetadata = async (props: LayoutProps): Promise<Metadata> => appLayoutMetadata((await props.params).locale)
15
16
 
16
17
  /** The locale segment's layout slot: the document of the address's language, the app's client providers and its chrome. */
17
- const Layout = async (props: LayoutProps) => (
18
+ const Layout = async (props: LayoutProps): Promise<ReactElement> => (
18
19
  <LocaleShell
19
20
  {...await appI18n.readLocaleSegment(props.params)}
20
21
  content={
@@ -1,6 +1,6 @@
1
1
  import type { Metadata } from "next"
2
2
  import { getTranslations } from "next-intl/server"
3
- import type { ReactNode } from "react"
3
+ import type { ReactElement, ReactNode } from "react"
4
4
  import { SiteShell } from "@{{project}}/ui"
5
5
  import { siteUrl } from "@/modules/config"
6
6
  import { APP_ROUTES } from "@/modules/routes"
@@ -1,3 +1,4 @@
1
+ import type { ReactElement } from "react"
1
2
  import type { Metadata } from "next"
2
3
  import { getTranslations } from "next-intl/server"
3
4
  import { AppGuideOverlay } from "@/features/overlays/AppGuideOverlay"
@@ -10,7 +11,7 @@ export const appHomeMetadata = async (): Promise<Metadata> => {
10
11
  }
11
12
 
12
13
  /** The first page of the product app: a server component; the session surface it draws runs on the client. */
13
- export const AppHomePage = async () => {
14
+ export const AppHomePage = async (): Promise<ReactElement> => {
14
15
  const t = await getTranslations("app.home")
15
16
  return (
16
17
  <>
@@ -1,8 +1,9 @@
1
+ import type { ReactElement } from "react"
1
2
  import { getTranslations } from "next-intl/server"
2
3
  import { LoadingNotice } from "@{{project}}/ui"
3
4
 
4
5
  /** Shown while a route of the product app resolves. */
5
- export const AppLoadingPage = async () => {
6
+ export const AppLoadingPage = async (): Promise<ReactElement> => {
6
7
  const t = await getTranslations("app.loading")
7
8
  return <LoadingNotice label={t("message")} />
8
9
  }
@@ -1,9 +1,10 @@
1
+ import type { ReactElement } from "react"
1
2
  import { getTranslations } from "next-intl/server"
2
3
  import { NotFoundNotice } from "@{{project}}/ui"
3
4
  import { APP_ROUTES } from "@/modules/routes"
4
5
 
5
6
  /** Shown when a route of the product app calls `notFound()`. */
6
- export const AppNotFoundPage = async () => {
7
+ export const AppNotFoundPage = async (): Promise<ReactElement> => {
7
8
  const t = await getTranslations("app.notFound")
8
9
  return <NotFoundNotice title={t("title")} homeLabel={t("home")} homeHref={APP_ROUTES.home} />
9
10
  }
@@ -1,4 +1,5 @@
1
1
  import { createAppI18n } from "@{{project}}/i18n"
2
+ import type { AppI18n } from "@{{project}}/i18n"
2
3
 
3
4
  /** The product app's i18n: the product's one next-intl stack (the i18n package), with this app's own catalogs under `messages/`. */
4
- export const appI18n = createAppI18n("app", (locale) => import(`./messages/${locale}.json`))
5
+ export const appI18n: AppI18n = createAppI18n("app", (locale) => import(`./messages/${locale}.json`))
@@ -1,4 +1,7 @@
1
1
  import { appI18n } from "./index"
2
+ import type { AppI18n } from "@{{project}}/i18n"
2
3
 
3
4
  /** The request config next-intl's plugin loads (next.config.ts): the product stack over this app's catalogs. */
4
- export default appI18n.requestConfig
5
+ const requestConfig: AppI18n["requestConfig"] = appI18n.requestConfig
6
+
7
+ export default requestConfig
@@ -1,4 +1,5 @@
1
- import type { ReactNode } from "react"
1
+ import type { Metadata } from "next"
2
+ import type { ReactElement, ReactNode } from "react"
2
3
  import { LocaleShell } from "@{{project}}/ui"
3
4
  import { LandingLayout, landingLayoutMetadata } from "@/features/layouts/LandingLayout"
4
5
  import { landingI18n } from "@/modules/i18n"
@@ -10,10 +11,10 @@ type LayoutProps = {
10
11
  }
11
12
 
12
13
  /** The localized document title and description of the landing. */
13
- export const generateMetadata = async (props: LayoutProps) => landingLayoutMetadata((await props.params).locale)
14
+ export const generateMetadata = async (props: LayoutProps): Promise<Metadata> => landingLayoutMetadata((await props.params).locale)
14
15
 
15
16
  /** The locale segment's layout slot: the document of the address's language around the landing's chrome. */
16
- const Layout = async (props: LayoutProps) => (
17
+ const Layout = async (props: LayoutProps): Promise<ReactElement> => (
17
18
  <LocaleShell {...await landingI18n.readLocaleSegment(props.params)} content={<LandingLayout content={props.children} />} />
18
19
  )
19
20
 
@@ -1,6 +1,6 @@
1
1
  import type { Metadata } from "next"
2
2
  import { getTranslations } from "next-intl/server"
3
- import type { ReactNode } from "react"
3
+ import type { ReactElement, ReactNode } from "react"
4
4
  import { SiteShell } from "@{{project}}/ui"
5
5
  import { LANDING_ROUTES } from "@/modules/routes"
6
6
 
@@ -14,7 +14,7 @@ export const landingLayoutMetadata = async (locale: string): Promise<Metadata> =
14
14
  }
15
15
 
16
16
  /** The landing's chrome, resolved on the server: the shared brand shell with the wordmark linking home, around the routed body. */
17
- export const LandingLayout = async (props: LandingLayoutProps) => {
17
+ export const LandingLayout = async (props: LandingLayoutProps): Promise<ReactElement> => {
18
18
  const t = await getTranslations("landing.shell")
19
19
  return (
20
20
  <SiteShell brand={t("brand")} homeHref={LANDING_ROUTES.home}>
@@ -1,8 +1,9 @@
1
+ import type { ReactElement } from "react"
1
2
  import { getTranslations } from "next-intl/server"
2
3
  import { LoadingNotice } from "@{{project}}/ui"
3
4
 
4
5
  /** Shown while a route of the landing resolves. */
5
- export const LandingLoadingPage = async () => {
6
+ export const LandingLoadingPage = async (): Promise<ReactElement> => {
6
7
  const t = await getTranslations("landing.loading")
7
8
  return <LoadingNotice label={t("message")} />
8
9
  }
@@ -1,9 +1,10 @@
1
+ import type { ReactElement } from "react"
1
2
  import { getTranslations } from "next-intl/server"
2
3
  import { NotFoundNotice } from "@{{project}}/ui"
3
4
  import { LANDING_ROUTES } from "@/modules/routes"
4
5
 
5
6
  /** Shown when a route of the landing calls `notFound()`. */
6
- export const LandingNotFoundPage = async () => {
7
+ export const LandingNotFoundPage = async (): Promise<ReactElement> => {
7
8
  const t = await getTranslations("landing.notFound")
8
9
  return <NotFoundNotice title={t("title")} homeLabel={t("home")} homeHref={LANDING_ROUTES.home} />
9
10
  }
@@ -1,3 +1,4 @@
1
+ import type { ReactElement } from "react"
1
2
  import type { Metadata } from "next"
2
3
  import { getTranslations } from "next-intl/server"
3
4
  import { appUrl } from "@/modules/config"
@@ -10,7 +11,7 @@ export const landingMetadata = async (): Promise<Metadata> => {
10
11
  }
11
12
 
12
13
  /** The landing page, resolved on the server: its copy from the catalog and the product app's origin from the config module. */
13
- export const LandingPage = async () => {
14
+ export const LandingPage = async (): Promise<ReactElement> => {
14
15
  const t = await getTranslations("landing.home")
15
16
  return <LandingPageBase props={{ title: t("title"), lede: t("lede"), openApp: t("openApp"), appHref: appUrl() }} />
16
17
  }
@@ -1,4 +1,5 @@
1
1
  import { createAppI18n } from "@{{project}}/i18n"
2
+ import type { AppI18n } from "@{{project}}/i18n"
2
3
 
3
4
  /** The landing's i18n: the product's one next-intl stack (the i18n package), with this app's own catalogs under `messages/`. */
4
- export const landingI18n = createAppI18n("landing", (locale) => import(`./messages/${locale}.json`))
5
+ export const landingI18n: AppI18n = createAppI18n("landing", (locale) => import(`./messages/${locale}.json`))
@@ -1,4 +1,7 @@
1
1
  import { landingI18n } from "./index"
2
+ import type { AppI18n } from "@{{project}}/i18n"
2
3
 
3
4
  /** The request config next-intl's plugin loads (next.config.ts): the product stack over the landing's catalogs. */
4
- export default landingI18n.requestConfig
5
+ const requestConfig: AppI18n["requestConfig"] = landingI18n.requestConfig
6
+
7
+ export default requestConfig
@@ -6,6 +6,7 @@ import { getMessages, getRequestConfig } from "next-intl/server"
6
6
  import { notFound } from "next/navigation"
7
7
  import { locale as routeLocale } from "next/root-params"
8
8
  import { PRODUCT_TIME_ZONE, routing } from "./routing"
9
+ import type { ProductRouting } from "./routing"
9
10
 
10
11
  /** Loads one app's own catalog for a served locale. */
11
12
  type LoadMessages = (locale: string) => Promise<unknown>
@@ -27,6 +28,23 @@ const isLocaleReader = (value: unknown): value is () => Promise<unknown> => type
27
28
 
28
29
  const readRouteLocale = async (): Promise<unknown> => (isLocaleReader(routeLocale) ? routeLocale() : undefined)
29
30
 
31
+ /** The language the address states, its catalog and the product time zone. */
32
+ interface LocaleSegment {
33
+ readonly locale: string
34
+ readonly messages: AbstractIntlMessages
35
+ readonly timeZone: string
36
+ }
37
+
38
+ /** The navigation helpers of the product's routing (links, redirects, the locale-aware router). */
39
+ type ProductNavigation = ReturnType<typeof createNavigation<ProductRouting["locales"], "as-needed">>
40
+
41
+ /** What one app of the product reads of its next-intl stack. */
42
+ export interface AppI18n {
43
+ readonly navigation: ProductNavigation
44
+ readonly requestConfig: ReturnType<typeof getRequestConfig>
45
+ readonly readLocaleSegment: (params: Promise<{ readonly locale: string }>) => Promise<LocaleSegment>
46
+ }
47
+
30
48
  /**
31
49
  * The next-intl stack of one app of the product, written once here and called once by each app's `modules/i18n` (R59): the
32
50
  * product's one routing with its locale-aware navigation, the request config next-intl's plugin loads (the app's
@@ -34,7 +52,7 @@ const readRouteLocale = async (): Promise<unknown> => (isLocaleReader(routeLocal
34
52
  * the app in the text of a message its catalog lacks (`<app>:<namespace>.<key>`); `loadMessages` loads its own catalogs. A server
35
53
  * module: a client component reads the default locale from `./routing`.
36
54
  */
37
- export const createAppI18n = (app: string, loadMessages: LoadMessages) => ({
55
+ export const createAppI18n = (app: string, loadMessages: LoadMessages): AppI18n => ({
38
56
  navigation: createNavigation(routing),
39
57
  /**
40
58
  * The locale is the `[locale]` route param read through `next/root-params` (`experimental.rootParams` in the app's next.config.ts,
@@ -20,5 +20,5 @@ const negotiate = createMiddleware(routing)
20
20
  * the default and serves the default locale (vi) without a prefix. One routing table for every app; each app's `src/proxy.ts`
21
21
  * re-exports this.
22
22
  */
23
- export const proxy = (request: NextRequest) =>
23
+ export const proxy = (request: NextRequest): NextResponse =>
24
24
  isUnnegotiated(request.nextUrl.pathname) ? NextResponse.next() : negotiate(request)
@@ -1,4 +1,5 @@
1
1
  import { defineRouting } from "next-intl/routing"
2
+ import type { RoutingConfig } from "next-intl/routing"
2
3
 
3
4
  /** The locales every app of the product serves; every catalog of an app has the same keys in each. */
4
5
  const LOCALES = ["vi", "en"] as const
@@ -9,11 +10,14 @@ export const DEFAULT_LOCALE = "vi"
9
10
  /** The product's time zone: fixed, not inferred, so the server and the hydrated client format the same instant the same way. */
10
11
  export const PRODUCT_TIME_ZONE = "Asia/Ho_Chi_Minh"
11
12
 
13
+ /** The type of the product's routing table: the served locales, no locale prefix for the default one, no localized pathnames or domains. */
14
+ export type ProductRouting = RoutingConfig<typeof LOCALES, "as-needed", never, never>
15
+
12
16
  /**
13
17
  * The one routing table of the product, for the proxy, the navigation and the request config of every app: vi default, a prefix
14
18
  * only when the locale is not the default, so `/vi` means the same thing on every app.
15
19
  */
16
- export const routing = defineRouting({
20
+ export const routing: ProductRouting = defineRouting({
17
21
  locales: LOCALES,
18
22
  defaultLocale: DEFAULT_LOCALE,
19
23
  localePrefix: "as-needed",