@starci/hfs 5.0.8 → 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 (152) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/cache/import-graph.mjs +6 -61
  3. package/cache/lint-family.d.mts +10 -1
  4. package/cache/lint-family.mjs +12 -5
  5. package/codemod/directive-first.d.mts +48 -0
  6. package/codemod/directive-first.mjs +136 -0
  7. package/codemod/explicit-surface-edit.d.mts +14 -0
  8. package/codemod/explicit-surface-edit.mjs +57 -0
  9. package/codemod/explicit-surface-names.d.mts +30 -0
  10. package/codemod/explicit-surface-names.mjs +135 -0
  11. package/codemod/explicit-surface-render.d.mts +29 -0
  12. package/codemod/explicit-surface-render.mjs +197 -0
  13. package/codemod/explicit-surface.d.mts +50 -0
  14. package/codemod/explicit-surface.mjs +374 -0
  15. package/codemod/main.d.mts +7 -5
  16. package/codemod/main.mjs +37 -16
  17. package/codemod/source-tree.d.mts +8 -0
  18. package/codemod/source-tree.mjs +23 -0
  19. package/lint/eslint-list.cjs +29 -0
  20. package/lint/run.d.mts +19 -5
  21. package/lint/run.mjs +47 -14
  22. package/package.json +1 -1
  23. package/runtime/knowledge/hfs/canon-pins.yaml +7 -7
  24. package/runtime/knowledge/hfs/rules.yaml +52 -1
  25. package/runtime/scripts/hfs/architecture/test-world-files.d.mts +4 -3
  26. package/runtime/scripts/hfs/architecture/test-world-files.mjs +26 -7
  27. package/runtime/scripts/hfs/explicit-surface-rule.d.mts +58 -0
  28. package/runtime/scripts/hfs/explicit-surface-rule.mjs +59 -0
  29. package/runtime/scripts/lib/declaration-surface.d.mts +35 -0
  30. package/runtime/scripts/lib/declaration-surface.mjs +112 -0
  31. package/templates/be/inbox/delivery-recorded.event.ts.tpl +2 -2
  32. package/templates/be/inbox/entities.index.ts.tpl +1 -1
  33. package/templates/be/inbox/module-definition.ts.tpl +8 -2
  34. package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +10 -2
  35. package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +8 -2
  36. package/templates/be/patterns/api/feature.module-definition.ts.tpl +9 -3
  37. package/templates/be/patterns/api/graphql.module-definition.ts.tpl +9 -3
  38. package/templates/be/patterns/cli/group.module-definition.ts.tpl +9 -3
  39. package/templates/be/patterns/event-bus/platform/event-bus.module-definition.ts.tpl +8 -3
  40. package/templates/be/patterns/event-bus/platform/persistence/connection.ts.tpl +3 -2
  41. package/templates/be/patterns/event-bus/platform/persistence/event-bus.sql.ts.tpl +4 -3
  42. package/templates/be/patterns/idempotency/platform/inbox/inbox.module-definition.ts.tpl +8 -3
  43. package/templates/be/patterns/idempotency/platform/inbox/persistence/connection.ts.tpl +3 -2
  44. package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.sql.ts.tpl +7 -6
  45. package/templates/be/patterns/jobs/job.module-definition.ts.tpl +9 -3
  46. package/templates/be/patterns/jobs/job.processor.ts.tpl +1 -1
  47. package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +9 -3
  48. package/templates/be/patterns/jobs/platform/jobs.module-definition.ts.tpl +8 -3
  49. package/templates/be/patterns/jobs/platform/persistence.connection.ts.tpl +3 -2
  50. package/templates/be/patterns/jobs/platform/persistence.jobs.sql.ts.tpl +5 -4
  51. package/templates/be/patterns/projections/persistence.connection.ts.tpl +3 -2
  52. package/templates/be/patterns/projections/projection.module-definition.ts.tpl +8 -3
  53. package/templates/be/patterns/queues/platform/persistence.connection.ts.tpl +3 -2
  54. package/templates/be/patterns/queues/platform/persistence.queue.sql.ts.tpl +4 -3
  55. package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +8 -3
  56. package/templates/be/patterns/queues/queue.module-definition.ts.tpl +9 -3
  57. package/templates/be/patterns/reactors/consumer.ts.tpl +1 -1
  58. package/templates/be/patterns/reactors/message.module-definition.ts.tpl +9 -3
  59. package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +9 -3
  60. package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +9 -3
  61. package/templates/be/patterns/realtime/channel.sse.module-definition.ts.tpl +9 -3
  62. package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +9 -3
  63. package/templates/be/patterns/saga/saga.done.consumer.ts.tpl +1 -1
  64. package/templates/be/patterns/saga/saga.failed.consumer.ts.tpl +1 -1
  65. package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +9 -3
  66. package/templates/be/patterns/saga/saga.module-definition.ts.tpl +9 -3
  67. package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +9 -3
  68. package/templates/be/platform/graphql/graphql.module-definition.ts +8 -3
  69. package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +9 -3
  70. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +9 -3
  71. package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +9 -3
  72. package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +9 -3
  73. package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +9 -3
  74. package/templates/be/skeleton/src/modules/domain/identity/identity.module-definition.ts +8 -3
  75. package/templates/be/skeleton/src/modules/domain/liveness/liveness.module-definition.ts +8 -3
  76. package/templates/be/skeleton/src/modules/domain/note/note.module-definition.ts +8 -3
  77. package/templates/be/skeleton/src/modules/domain/note/persistence/connection.ts +3 -2
  78. package/templates/be/skeleton/src/modules/domain/note/persistence/note.sql.ts +2 -1
  79. package/templates/be/skeleton/src/modules/platform/clock/clock.module-definition.ts +8 -3
  80. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module-definition.ts +8 -3
  81. package/templates/be/skeleton/src/modules/platform/database/database.module-definition.ts +8 -3
  82. package/templates/be/skeleton/src/modules/platform/database/index.ts +1 -0
  83. package/templates/be/skeleton/src/modules/platform/errors/errors.module-definition.ts +10 -4
  84. package/templates/be/skeleton/src/modules/platform/http/http.module-definition.ts +8 -4
  85. package/templates/be/skeleton/src/modules/platform/http-security/http-security.module-definition.ts +10 -4
  86. package/templates/be/skeleton/src/modules/platform/http-security/persistence/connection.ts +3 -2
  87. package/templates/be/skeleton/src/modules/platform/http-security/persistence/rate-limit.sql.ts +3 -2
  88. package/templates/be/skeleton/src/modules/platform/i18n/i18n.module-definition.ts +10 -4
  89. package/templates/be/skeleton/src/modules/platform/ids/ids.module-definition.ts +8 -3
  90. package/templates/be/skeleton/src/modules/platform/logging/logging.module-definition.ts +8 -3
  91. package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +8 -3
  92. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +1 -1
  93. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/google.strategy.ts +5 -1
  94. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/local.strategy.ts +5 -1
  95. package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +1 -1
  96. package/templates/be/skeleton-lite/src/modules/platform/http-security/persistence/entities/index.ts +1 -1
  97. package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +7 -1
  98. package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +7 -1
  99. package/templates/be/table/api/feature.module-definition.ts.tpl +9 -3
  100. package/templates/be/table/api/graphql.module-definition.ts.tpl +9 -3
  101. package/templates/be/table/entities.index.ts.tpl +1 -1
  102. package/templates/be/table/module-definition.ts.tpl +8 -3
  103. package/templates/be/test-world/fixtures/migrations.sql.ts.tpl +1 -0
  104. package/templates/be/test-world/world/test-world.config.ts.tpl +8 -1
  105. package/templates/fe/common/app-layout.tsx.partial +1 -1
  106. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +2 -1
  107. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignIn.ts +11 -3
  108. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +8 -2
  109. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/store.ts +2 -1
  110. package/templates/fe/skeleton/apps/__app__/src/modules/toast/hooks.ts +1 -1
  111. package/templates/fe/skeleton/apps/__app__/src/modules/toast/toast.tsx +4 -1
  112. package/templates/fe/skeleton/apps/__app__/src/redux/hooks.ts +3 -2
  113. package/templates/fe/skeleton/apps/__app__/src/redux/index.ts +1 -0
  114. package/templates/fe/skeleton/apps/__app__/src/redux/slices/session.ts +12 -4
  115. package/templates/fe/skeleton/apps/__app__/src/redux/store.ts +8 -3
  116. package/templates/fe/skeleton/apps/app/src/app/[locale]/layout.tsx +4 -3
  117. package/templates/fe/skeleton/apps/app/src/features/layouts/AppLayout/index.tsx +1 -1
  118. package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +2 -1
  119. package/templates/fe/skeleton/apps/app/src/features/pages/AppLoadingPage/index.tsx +2 -1
  120. package/templates/fe/skeleton/apps/app/src/features/pages/AppNotFoundPage/index.tsx +2 -1
  121. package/templates/fe/skeleton/apps/app/src/modules/i18n/index.ts +2 -1
  122. package/templates/fe/skeleton/apps/app/src/modules/i18n/request.ts +4 -1
  123. package/templates/fe/skeleton/apps/landing/src/app/[locale]/layout.tsx +4 -3
  124. package/templates/fe/skeleton/apps/landing/src/features/layouts/LandingLayout/index.tsx +2 -2
  125. package/templates/fe/skeleton/apps/landing/src/features/pages/LandingLoadingPage/index.tsx +2 -1
  126. package/templates/fe/skeleton/apps/landing/src/features/pages/LandingNotFoundPage/index.tsx +2 -1
  127. package/templates/fe/skeleton/apps/landing/src/features/pages/LandingPage/index.tsx +2 -1
  128. package/templates/fe/skeleton/apps/landing/src/modules/i18n/index.ts +2 -1
  129. package/templates/fe/skeleton/apps/landing/src/modules/i18n/request.ts +4 -1
  130. package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +19 -1
  131. package/templates/fe/skeleton/packages/__project__-i18n/src/proxy.ts +1 -1
  132. package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +5 -1
  133. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +4 -3
  134. package/templates/fe/skeleton-lite/apps/web/src/features/layouts/AppLayout/index.tsx +2 -2
  135. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +2 -1
  136. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppLoadingPage/index.tsx +2 -1
  137. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppNotFoundPage/index.tsx +2 -1
  138. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +3 -1
  139. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +5 -1
  140. package/templates/fe/table/graphql/useMutateSwr.ts.tpl +2 -1
  141. package/templates/fe/table/graphql/useQuerySwr.ts.tpl +2 -1
  142. package/templates/fe/transport/graphql/clients/clients/auth.ts +1 -1
  143. package/templates/fe/transport/graphql/clients/clients/no-auth.ts +1 -1
  144. package/templates/fe/transport/graphql/clients/links/attach-access-token.ts +1 -1
  145. package/templates/fe/transport/graphql/clients/links/error.ts +1 -1
  146. package/templates/fe/transport/graphql/clients/links/http.ts +1 -1
  147. package/templates/fe/transport/graphql/clients/links/locale.ts +1 -1
  148. package/templates/fe/transport/graphql/clients/links/refresh-token.ts +1 -1
  149. package/templates/fe/transport/graphql/clients/links/retry.ts +1 -1
  150. package/templates/fe/transport/graphql/clients/links/timeout.ts +1 -1
  151. package/templates/fe/units/page/index.tsx.tpl +3 -2
  152. package/templates/fe/units/slot/SlotView.tsx.tpl +1 -1
@@ -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",
@@ -1,7 +1,8 @@
1
1
  import { hasLocale } from "next-intl"
2
2
  import { getMessages } from "next-intl/server"
3
3
  import { notFound } from "next/navigation"
4
- import type { ReactNode } from "react"
4
+ import type { Metadata } from "next"
5
+ import type { ReactElement, ReactNode } from "react"
5
6
  import { AppLayout, appLayoutMetadata } from "@/features/layouts/AppLayout"
6
7
  import { PRODUCT_TIME_ZONE, routing } from "@/modules/i18n"
7
8
  import { Providers } from "./providers"
@@ -14,10 +15,10 @@ type LayoutProps = {
14
15
  }
15
16
 
16
17
  /** The localized document title of this route segment. */
17
- export const generateMetadata = async (props: LayoutProps) => appLayoutMetadata((await props.params).locale)
18
+ export const generateMetadata = async (props: LayoutProps): Promise<Metadata> => appLayoutMetadata((await props.params).locale)
18
19
 
19
20
  /** The locale segment's document, providers and app chrome. */
20
- const Layout = async (props: LayoutProps) => {
21
+ const Layout = async (props: LayoutProps): Promise<ReactElement> => {
21
22
  const { locale } = await props.params
22
23
  if (!hasLocale(routing.locales, locale)) notFound()
23
24
  const messages = await getMessages()
@@ -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 "@/components/frames/SiteShell"
5
5
  import { siteUrl } from "@/modules/config"
6
6
  import { APP_ROUTES } from "@/modules/routes"
@@ -17,7 +17,7 @@ export const appLayoutMetadata = async (locale: string): Promise<Metadata> => {
17
17
  }
18
18
 
19
19
  /** The server-resolved app chrome around the routed body. */
20
- export const AppLayout = async (props: AppLayoutProps) => {
20
+ export const AppLayout = async (props: AppLayoutProps): Promise<ReactElement> => {
21
21
  const t = await getTranslations("app.shell")
22
22
  return (
23
23
  <html lang={props.locale}>
@@ -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 front door (the session is client-side, drawn by the blocks the page composes). */
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 "@/components/composites/LoadingNotice"
3
4
 
4
5
  /** Shown while a route 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 "@/components/composites/NotFoundNotice"
3
4
  import { APP_ROUTES } from "@/modules/routes"
4
5
 
5
6
  /** Shown when a route does not exist. */
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
  }
@@ -21,7 +21,7 @@ const LOCALE_HEADER = "x-next-intl-locale"
21
21
  * The request config next-intl's plugin loads. The locale is the one the middleware negotiated: this module is reachable from the proxy
22
22
  * through the navigation, and `next/root-params` can only be imported inside the app directory.
23
23
  */
24
- export default getRequestConfig(async () => {
24
+ const requestConfig: ReturnType<typeof getRequestConfig> = getRequestConfig(async () => {
25
25
  const requested = (await headers()).get(LOCALE_HEADER)
26
26
  const locale = hasLocale(routing.locales, requested) ? requested : routing.defaultLocale
27
27
  return {
@@ -31,3 +31,5 @@ export default getRequestConfig(async () => {
31
31
  getMessageFallback: ({ namespace, key }) => `web:${namespace ?? ""}.${key}`,
32
32
  }
33
33
  })
34
+
35
+ export default requestConfig
@@ -7,7 +7,11 @@ export const DEFAULT_LOCALE = "vi" as const
7
7
  export const PRODUCT_TIME_ZONE = "Asia/Ho_Chi_Minh"
8
8
 
9
9
  /** The one locale-routing table of the web app. */
10
- export const routing = {
10
+ export const routing: {
11
+ readonly locales: typeof LOCALES
12
+ readonly defaultLocale: typeof DEFAULT_LOCALE
13
+ readonly localePrefix: "as-needed"
14
+ } = {
11
15
  locales: LOCALES,
12
16
  defaultLocale: DEFAULT_LOCALE,
13
17
  localePrefix: "as-needed" as const,
@@ -1,5 +1,6 @@
1
1
  import { useSWRConfig } from "swr"
2
2
  import useSWRMutation from "swr/mutation"
3
+ import type { SWRMutationResponse } from "swr/mutation"
3
4
  import type { GraphQLResponse } from "@/modules/api"
4
5
  import { mutateCreate{{Singular}} } from "@/modules/api/graphql/mutations/mutation-create-{{singular}}"
5
6
  import type { MutateCreate{{Singular}}Data } from "@/modules/api/graphql/mutations/types/create-{{singular}}"
@@ -12,7 +13,7 @@ export const MUTATE_CREATE_{{singularUpper}}_SWR_KEY = "MUTATE_CREATE_{{singular
12
13
  * Creates one {{singular}} row; answers the GraphQLResponse so the caller reports it through modules/toast
13
14
  * (useGraphQLWithToast). A success primes the {{singular}} query of the new row with that response.
14
15
  */
15
- export const useMutateCreate{{Singular}}Swr = () => {
16
+ export const useMutateCreate{{Singular}}Swr = (): SWRMutationResponse<GraphQLResponse<MutateCreate{{Singular}}Data>, Error, string, never> => {
16
17
  const { mutate } = useSWRConfig()
17
18
  return useSWRMutation(MUTATE_CREATE_{{singularUpper}}_SWR_KEY, async () => {
18
19
  const response: GraphQLResponse<MutateCreate{{Singular}}Data> = await mutateCreate{{Singular}}({ request: {} })
@@ -1,4 +1,5 @@
1
1
  import useSWR from "swr"
2
+ import type { SWRResponse } from "swr"
2
3
  import type { GraphQLResponse } from "@/modules/api"
3
4
  import { query{{Singular}} } from "@/modules/api/graphql/queries/query-{{singular}}"
4
5
  import type { Query{{Singular}}Data } from "@/modules/api/graphql/queries/types/{{singular}}"
@@ -7,7 +8,7 @@ import type { Query{{Singular}}Data } from "@/modules/api/graphql/queries/types/
7
8
  export const QUERY_{{singularUpper}}_SWR_KEY = "QUERY_{{singularUpper}}_SWR"
8
9
 
9
10
  /** One {{singular}} row of the signed-in reader by id, as its GraphQLResponse; no id, no request. */
10
- export const useQuery{{Singular}}Swr = (id: string | undefined) =>
11
+ export const useQuery{{Singular}}Swr = (id: string | undefined): SWRResponse<GraphQLResponse<Query{{Singular}}Data>, Error> =>
11
12
  useSWR(
12
13
  id === undefined ? null : ([QUERY_{{singularUpper}}_SWR_KEY, id] as const),
13
14
  ([, rowId]): Promise<GraphQLResponse<Query{{Singular}}Data>> => query{{Singular}}({ request: { id: rowId } }),
@@ -23,7 +23,7 @@ export interface CreateAuthApolloClientOptions {
23
23
  * retry (transient network failures) -> error (report) -> timeout -> refresh (one token refresh and replay on
24
24
  * UNAUTHENTICATED) -> locale (`Accept-Language`) -> access token (`Authorization: Bearer`, from modules/auth) -> http (terminal).
25
25
  */
26
- export const createAuthApolloClient = ({ signal, report }: CreateAuthApolloClientOptions) =>
26
+ export const createAuthApolloClient = ({ signal, report }: CreateAuthApolloClientOptions): ApolloClient =>
27
27
  new ApolloClient({
28
28
  link: ApolloLink.from([
29
29
  createRetryLink(),
@@ -19,7 +19,7 @@ export interface CreateNoAuthApolloClientOptions {
19
19
  * The anonymous Apollo client for public operations: retry -> error -> timeout -> locale -> http. It never attaches a
20
20
  * token, so a public operation stays public even for a signed-in reader.
21
21
  */
22
- export const createNoAuthApolloClient = ({ signal, report }: CreateNoAuthApolloClientOptions) =>
22
+ export const createNoAuthApolloClient = ({ signal, report }: CreateNoAuthApolloClientOptions): ApolloClient =>
23
23
  new ApolloClient({
24
24
  link: ApolloLink.from([
25
25
  createRetryLink(),
@@ -2,7 +2,7 @@ import { SetContextLink } from "@apollo/client/link/context"
2
2
  import { contextHeaders } from "./context-headers"
3
3
 
4
4
  /** Attaches `Authorization: Bearer <access token>` from modules/auth when a session exists; an anonymous call goes out without it. */
5
- export const createAttachAccessTokenLink = (accessToken: () => Promise<string | undefined>) =>
5
+ export const createAttachAccessTokenLink = (accessToken: () => Promise<string | undefined>): SetContextLink =>
6
6
  new SetContextLink(async (context) => {
7
7
  const token = await accessToken()
8
8
  if (token === undefined) return {}
@@ -15,7 +15,7 @@ const codeOf = (error: GraphQLErrorEntry): string =>
15
15
  * Reports what came back from downstream to `report` when the app passes one. The operation maps every error to its
16
16
  * GraphQLResponse, so this link never swallows or rewrites one.
17
17
  */
18
- export const createErrorLink = (report?: ErrorReporter) =>
18
+ export const createErrorLink = (report?: ErrorReporter): ErrorLink =>
19
19
  new ErrorLink(({ error, operation }) => {
20
20
  if (report === undefined) return
21
21
  if (CombinedGraphQLErrors.is(error)) {
@@ -8,7 +8,7 @@ export interface CreateHttpLinkParams {
8
8
  }
9
9
 
10
10
  /** The terminal link: POSTs the operation to the back end's GraphQL endpoint (modules/config). */
11
- export const createHttpLink = ({ withCredentials = false, signal }: CreateHttpLinkParams) =>
11
+ export const createHttpLink = ({ withCredentials = false, signal }: CreateHttpLinkParams): HttpLink =>
12
12
  new HttpLink({
13
13
  uri: graphqlUrl(),
14
14
  credentials: withCredentials ? "include" : "same-origin",
@@ -6,7 +6,7 @@ const documentLocale = (): string | undefined =>
6
6
  typeof document === "undefined" || document.documentElement.lang === "" ? undefined : document.documentElement.lang
7
7
 
8
8
  /** The one writer of `Accept-Language`: every operation declares the language the back end answers and localizes its errors in. */
9
- export const createAttachLocaleLink = (locale: () => string | undefined = documentLocale) =>
9
+ export const createAttachLocaleLink = (locale: () => string | undefined = documentLocale): SetContextLink =>
10
10
  new SetContextLink((context) => {
11
11
  const value = locale()
12
12
  if (value === undefined) return {}
@@ -15,7 +15,7 @@ const isUnauthenticated = (error: unknown): boolean =>
15
15
  * On an UNAUTHENTICATED answer, asks modules/auth for a fresh access token once and replays the operation with it. When no
16
16
  * token comes back (the session ended) the original error travels on.
17
17
  */
18
- export const createRefreshTokenLink = (refreshAccessToken: () => Promise<string | undefined>) =>
18
+ export const createRefreshTokenLink = (refreshAccessToken: () => Promise<string | undefined>): ErrorLink =>
19
19
  new ErrorLink(({ error, operation, forward }) => {
20
20
  if (!isUnauthenticated(error) || operation.getContext()[REFRESHED] === true) return
21
21
  return new Observable((observer) => {
@@ -6,7 +6,7 @@ import { RETRY_POLICY } from "./options"
6
6
  * Retries a transient network failure with jittered exponential backoff. A GraphQL error is an answer of the back end and
7
7
  * is never retried. First in the chain, so every other link sees the operation once per attempt.
8
8
  */
9
- export const createRetryLink = () =>
9
+ export const createRetryLink = (): RetryLink =>
10
10
  new RetryLink({
11
11
  delay: { initial: RETRY_POLICY.initialDelayMs, max: RETRY_POLICY.maxDelayMs, jitter: true },
12
12
  attempts: {
@@ -2,7 +2,7 @@ import { ApolloLink, Observable } from "@apollo/client"
2
2
  import { OPERATION_TIMEOUT_MS } from "./options"
3
3
 
4
4
  /** Fails the operation when no answer arrived within the timeout; the upstream links see the timeout as an error. */
5
- export const createTimeoutLink = (timeoutMs = OPERATION_TIMEOUT_MS) =>
5
+ export const createTimeoutLink = (timeoutMs: number = OPERATION_TIMEOUT_MS): ApolloLink =>
6
6
  new ApolloLink(
7
7
  (operation, forward) =>
8
8
  new Observable((observer) => {
@@ -1,4 +1,5 @@
1
1
  import type { Metadata } from "next"
2
+ import type { ReactElement } from "react"
2
3
  import { getTranslations } from "next-intl/server"
3
4
  import { APP_ROUTES } from "@/modules/routes"
4
5
  import { {{Name}}Base } from "./component"
@@ -23,7 +24,7 @@ const copyOf = async (isLoading: boolean): Promise<{{Name}}BaseProps["props"]> =
23
24
  }
24
25
 
25
26
  /** The {{route}} page: a server owner that resolves its copy and composes its blocks; it reads no data. */
26
- export const {{Name}} = async () => <{{Name}}Base props={await copyOf(false)} />
27
+ export const {{Name}} = async (): Promise<ReactElement> => <{{Name}}Base props={await copyOf(false)} />
27
28
 
28
29
  /** The {{route}} route's loading view: the same page drawn in skeleton mode, never a separate layout (STATE-8). */
29
- export const {{LoadingName}} = async () => <{{Name}}Base props={await copyOf(true)} />
30
+ export const {{LoadingName}} = async (): Promise<ReactElement> => <{{Name}}Base props={await copyOf(true)} />
@@ -15,7 +15,7 @@ type SlotViewProps<T> = {
15
15
  }
16
16
 
17
17
  /** The one status view (FE-ERROR-1): every region renders its loading, forbidden, error and empty status through here. */
18
- export const SlotView = <T,>(props: SlotViewProps<T>) => {
18
+ export const SlotView = <T,>(props: SlotViewProps<T>): ReactNode => {
19
19
  const { slot } = props
20
20
  if (slot.isLoading === true) return props.render(props.placeholder, true)
21
21
  if (slot.isForbidden === true) return props.forbidden === "hide" ? null : <EmptyNotice message={props.labels.forbidden} />