@starci/hfs 3.0.0 → 4.0.1

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 (199) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +25 -21
  3. package/bin/hfs.mjs +59 -37
  4. package/lint/run.mjs +70 -39
  5. package/package.json +2 -2
  6. package/runtime/engine/admission.mjs +3 -3
  7. package/runtime/engine/ledger-db.mjs +2 -2
  8. package/runtime/engine/machine-db.mjs +90 -9
  9. package/runtime/engine/migrations/machine/0002-worktrees-no-workflow-kind.sql +13 -0
  10. package/runtime/engine/migrations/runtime/0005-ended-workflow-views.sql +93 -0
  11. package/runtime/knowledge/hfs/canon-pins.yaml +28 -9
  12. package/runtime/knowledge/hfs/peer-integrations.yaml +18 -0
  13. package/runtime/knowledge/hfs/slots.yaml +193 -128
  14. package/runtime/knowledge/patterns/fe/folder.yaml +36 -36
  15. package/runtime/modules/kernel/failure-codes.yaml +23 -32
  16. package/runtime/scripts/checks/architecture/backend.mjs +1 -1
  17. package/runtime/scripts/checks/architecture/config.mjs +31 -11
  18. package/runtime/scripts/checks/architecture/contracts.mjs +4 -4
  19. package/runtime/scripts/checks/architecture/fe-slot-allows.mjs +7 -3
  20. package/runtime/scripts/checks/architecture/framework-pinned.mjs +5 -47
  21. package/runtime/scripts/checks/architecture/frontend.mjs +6 -4
  22. package/runtime/scripts/checks/architecture/hfs.mjs +104 -66
  23. package/runtime/scripts/checks/architecture/next-data.mjs +3 -2
  24. package/runtime/scripts/checks/architecture/registration.mjs +1 -1
  25. package/runtime/scripts/checks/architecture/symbols.mjs +13 -2
  26. package/runtime/scripts/checks/architecture/test-world-files.mjs +83 -45
  27. package/runtime/scripts/checks/architecture/typescript.mjs +45 -20
  28. package/runtime/scripts/checks/typescript-programs.mjs +2 -2
  29. package/runtime/scripts/lib/hfs-check.mjs +156 -141
  30. package/runtime/scripts/lib/hfs-path-findings.mjs +13 -2
  31. package/runtime/scripts/lib/hfs-rules/contract.mjs +15 -42
  32. package/runtime/scripts/lib/hfs-rules/deps.mjs +4 -2
  33. package/runtime/scripts/lib/hfs-rules/frontend.mjs +37 -36
  34. package/runtime/scripts/lib/hfs-rules/peer-integrations.mjs +44 -0
  35. package/runtime/scripts/lib/hfs-rules/proof-commands.mjs +11 -8
  36. package/runtime/scripts/lib/hfs-slots.mjs +244 -61
  37. package/runtime/scripts/lib/hfs-view.mjs +9 -7
  38. package/runtime/scripts/lib/language.mjs +11 -1
  39. package/runtime/scripts/lib/safe-remove.mjs +95 -10
  40. package/scaffold/app.mjs +205 -0
  41. package/scaffold/service.mjs +26 -16
  42. package/sync/cli.mjs +1 -1
  43. package/sync/hygiene.mjs +11 -8
  44. package/sync/index.mjs +109 -111
  45. package/sync/managed.mjs +9 -8
  46. package/sync/sonar-key.mjs +20 -22
  47. package/templates/{be → app}/ci-workflows/github/workflows/ci.yml +4 -2
  48. package/templates/app/gitignore +6 -0
  49. package/templates/app/hooks/husky/pre-commit +25 -0
  50. package/templates/app/hooks/husky/pre-push +7 -0
  51. package/templates/app/package-scripts/package.json +22 -0
  52. package/templates/{be → app}/quality-config/sonar-project.properties +3 -2
  53. package/templates/app/skeleton/.editorconfig +15 -0
  54. package/templates/app/skeleton/.gitattributes +2 -0
  55. package/templates/app/skeleton/.nvmrc +1 -0
  56. package/templates/app/skeleton/.starciwork/features/index.yaml +7 -0
  57. package/templates/app/skeleton/.starciwork/workspace.yaml +9 -0
  58. package/templates/app/skeleton/README.md +36 -0
  59. package/templates/app/skeleton/scripts/codegen.mjs +4 -0
  60. package/templates/{fe → app}/tool-config/prettierignore +4 -1
  61. package/templates/be/skeleton/.sops.yaml +2 -0
  62. package/templates/be/skeleton/.starcistacks/application-stacks.yaml +10 -0
  63. package/templates/be/skeleton/apps/__app__/src/__app__.options.ts +3 -0
  64. package/templates/be/skeleton/apps/__app__/src/app.module.ts +27 -6
  65. package/templates/be/skeleton/apps/__app__/src/main.ts +4 -1
  66. package/templates/be/skeleton/src/features/system-health/transport/http/live.controller.ts +2 -0
  67. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.ts +11 -0
  68. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.ts +25 -0
  69. package/templates/be/skeleton/src/modules/domain/identity/errors/identity.error.ts +16 -0
  70. package/templates/be/skeleton/src/modules/domain/identity/identity.contracts.ts +11 -0
  71. package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +8 -0
  72. package/templates/be/skeleton/src/modules/domain/identity/identity.module-definition.ts +7 -0
  73. package/templates/be/skeleton/src/modules/domain/identity/identity.module.ts +14 -0
  74. package/templates/be/skeleton/src/modules/domain/identity/identity.options.ts +2 -0
  75. package/templates/be/skeleton/src/modules/domain/identity/index.ts +6 -0
  76. package/templates/be/skeleton/src/modules/domain/identity/messages/identity.messages.ts +11 -0
  77. package/templates/be/skeleton/src/modules/platform/composition/composition.decorators.ts +8 -1
  78. package/templates/be/skeleton/src/modules/platform/composition/index.ts +1 -1
  79. package/templates/be/skeleton/src/modules/platform/config/env-source.config.ts +71 -23
  80. package/templates/be/skeleton/src/modules/platform/config/errors/config.error.ts +14 -16
  81. package/templates/be/skeleton/src/modules/platform/config/index.ts +1 -1
  82. package/templates/be/skeleton/src/modules/platform/config/server.config.ts +2 -12
  83. package/templates/be/skeleton/src/modules/platform/errors/domain.error.ts +16 -7
  84. package/templates/be/skeleton/src/modules/platform/errors/errors/errors.error.ts +16 -0
  85. package/templates/be/skeleton/src/modules/platform/errors/errors.contracts.ts +33 -0
  86. package/templates/be/skeleton/src/modules/platform/errors/errors.decorators.ts +16 -0
  87. package/templates/be/skeleton/src/modules/platform/errors/errors.filter.ts +32 -0
  88. package/templates/be/skeleton/src/modules/platform/errors/errors.log-events.ts +2 -2
  89. package/templates/be/skeleton/src/modules/platform/errors/errors.module-definition.ts +9 -0
  90. package/templates/be/skeleton/src/modules/platform/errors/errors.module.ts +19 -0
  91. package/templates/be/skeleton/src/modules/platform/errors/errors.options.ts +7 -0
  92. package/templates/be/skeleton/src/modules/platform/errors/errors.service.spec.ts +94 -0
  93. package/templates/be/skeleton/src/modules/platform/errors/errors.service.ts +47 -0
  94. package/templates/be/skeleton/src/modules/platform/errors/http-status.policy.ts +13 -0
  95. package/templates/be/skeleton/src/modules/platform/errors/index.ts +4 -1
  96. package/templates/be/skeleton/src/modules/platform/errors/messages/errors.messages.ts +11 -0
  97. package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +19 -0
  98. package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.ts +5 -0
  99. package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +16 -0
  100. package/templates/be/skeleton/src/modules/platform/http-security/http-security.decorators.ts +10 -0
  101. package/templates/be/skeleton/src/modules/platform/http-security/http-security.module-definition.ts +9 -0
  102. package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +13 -0
  103. package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +17 -0
  104. package/templates/be/skeleton/src/modules/platform/http-security/index.ts +7 -0
  105. package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +13 -0
  106. package/templates/be/skeleton/src/modules/platform/http-security/origin.guard.ts +31 -0
  107. package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.ts +68 -0
  108. package/templates/be/skeleton/src/modules/platform/i18n/bundle-message-catalog.service.spec.ts +66 -0
  109. package/templates/be/skeleton/src/modules/platform/i18n/bundle-message-catalog.service.ts +30 -0
  110. package/templates/be/skeleton/src/modules/platform/i18n/i18n.contracts.ts +18 -0
  111. package/templates/be/skeleton/src/modules/platform/i18n/i18n.decorators.ts +23 -0
  112. package/templates/be/skeleton/src/modules/platform/i18n/i18n.module-definition.ts +9 -0
  113. package/templates/be/skeleton/src/modules/platform/i18n/i18n.module.ts +24 -0
  114. package/templates/be/skeleton/src/modules/platform/i18n/i18n.options.ts +7 -0
  115. package/templates/be/skeleton/src/modules/platform/i18n/i18n.port.ts +13 -0
  116. package/templates/be/skeleton/src/modules/platform/i18n/index.ts +4 -0
  117. package/templates/be/skeleton/src/modules/platform/i18n/request-locale.service.spec.ts +45 -0
  118. package/templates/be/skeleton/src/modules/platform/i18n/request-locale.service.ts +19 -0
  119. package/templates/be/skeleton/src/modules/platform/logging/index.ts +1 -1
  120. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +85 -68
  121. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +14 -10
  122. package/templates/be/skeleton/src/modules/platform/logging/logging.decorators.ts +13 -0
  123. package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +4 -0
  124. package/templates/be/skeleton/src/modules/platform/logging/logging.port.ts +0 -1
  125. package/templates/be/skeleton/src/modules/platform/primitives/index.ts +2 -0
  126. package/templates/be/skeleton/src/modules/platform/primitives/outcome.contracts.ts +25 -0
  127. package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.ts +24 -0
  128. package/templates/fe/skeleton/apps/__app__/postcss.config.mjs +7 -0
  129. package/templates/fe/skeleton/apps/__app__/src/app/[locale]/error.tsx +5 -17
  130. package/templates/fe/skeleton/apps/__app__/src/app/[locale]/layout.tsx +11 -22
  131. package/templates/fe/skeleton/apps/__app__/src/app/[locale]/loading.tsx +6 -0
  132. package/templates/fe/skeleton/apps/__app__/src/app/[locale]/not-found.tsx +3 -12
  133. package/templates/fe/skeleton/apps/__app__/src/app/[locale]/page.tsx +6 -24
  134. package/templates/fe/skeleton/apps/__app__/src/app/global-error.tsx +4 -18
  135. package/templates/fe/skeleton/apps/__app__/src/app/globals.css +5 -0
  136. package/templates/fe/skeleton/apps/__app__/src/components/composites/FailureScreen/index.tsx +28 -0
  137. package/templates/fe/skeleton/apps/__app__/src/features/layouts/LocaleShell/index.tsx +35 -0
  138. package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/component.tsx +21 -0
  139. package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/index.tsx +17 -0
  140. package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/component.tsx +26 -0
  141. package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/index.tsx +15 -0
  142. package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/component.tsx +25 -0
  143. package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/index.tsx +15 -0
  144. package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/component.tsx +27 -0
  145. package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/index.tsx +8 -0
  146. package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/component.tsx +32 -0
  147. package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/index.tsx +8 -0
  148. package/templates/fe/skeleton/apps/__app__/src/modules/config/index.ts +12 -0
  149. package/templates/fe/skeleton/apps/__app__/src/modules/i18n/index.ts +2 -0
  150. package/templates/fe/skeleton/apps/__app__/src/modules/i18n/messages/vi.json +6 -0
  151. package/templates/fe/{skeleton-app → skeleton}/apps/__app__/src/modules/i18n/request.ts +1 -0
  152. package/templates/fe/skeleton/apps/__app__/src/modules/routes/index.ts +4 -0
  153. package/templates/fe/{skeleton-app → skeleton}/apps/__app__/src/proxy.ts +1 -1
  154. package/sync/skeleton.mjs +0 -76
  155. package/templates/be/gitignore +0 -2
  156. package/templates/be/hooks/husky/pre-commit +0 -13
  157. package/templates/be/hooks/husky/pre-push +0 -6
  158. package/templates/be/package-scripts/package.json +0 -19
  159. package/templates/be/skeleton/scripts/.gitkeep +0 -0
  160. package/templates/be/skeleton/src/modules/platform/config/config.decorators.ts +0 -9
  161. package/templates/be/skeleton/src/modules/platform/errors/error.filter.ts +0 -20
  162. package/templates/be/tool-config/prettierignore +0 -8
  163. package/templates/fe/ci-workflows/github/workflows/ci.yml +0 -40
  164. package/templates/fe/gitignore +0 -3
  165. package/templates/fe/hooks/husky/pre-commit +0 -16
  166. package/templates/fe/hooks/husky/pre-push +0 -5
  167. package/templates/fe/package-scripts/package.json +0 -13
  168. package/templates/fe/parts/api-client.ts +0 -44
  169. package/templates/fe/parts/api-outcome.ts +0 -7
  170. package/templates/fe/quality-config/sonar-project.properties +0 -8
  171. package/templates/fe/skeleton/scripts/.gitkeep +0 -0
  172. package/templates/fe/skeleton-app/apps/__app__/src/modules/api/client.ts +0 -1
  173. package/templates/fe/skeleton-app/apps/__app__/src/modules/api/index.ts +0 -3
  174. package/templates/fe/skeleton-app/apps/__app__/src/modules/api/outcome.ts +0 -1
  175. package/templates/fe/skeleton-app/apps/__app__/src/modules/i18n/index.ts +0 -4
  176. package/templates/fe/skeleton-app/apps/__app__/src/modules/i18n/navigation.ts +0 -5
  177. package/templates/fe/skeleton-shared/apps/__app__/next.config.ts +0 -12
  178. package/templates/fe/skeleton-shared/apps/__app__/src/modules/api/index.ts +0 -2
  179. package/templates/fe/skeleton-shared/apps/__app__/src/modules/i18n/index.ts +0 -9
  180. package/templates/fe/skeleton-shared/apps/__app__/src/modules/i18n/request.ts +0 -5
  181. package/templates/fe/skeleton-shared/apps/__app__/src/proxy.ts +0 -5
  182. package/templates/fe/skeleton-shared/packages/__family__-api/package.json +0 -12
  183. package/templates/fe/skeleton-shared/packages/__family__-api/src/client.ts +0 -1
  184. package/templates/fe/skeleton-shared/packages/__family__-api/src/index.ts +0 -3
  185. package/templates/fe/skeleton-shared/packages/__family__-api/src/outcome.ts +0 -1
  186. package/templates/fe/skeleton-shared/packages/__family__-api/tsconfig.json +0 -5
  187. package/templates/fe/skeleton-shared/packages/__family__-i18n/package.json +0 -18
  188. package/templates/fe/skeleton-shared/packages/__family__-i18n/src/app.ts +0 -19
  189. package/templates/fe/skeleton-shared/packages/__family__-i18n/src/index.ts +0 -2
  190. package/templates/fe/skeleton-shared/packages/__family__-i18n/src/proxy.ts +0 -12
  191. package/templates/fe/skeleton-shared/packages/__family__-i18n/src/request.ts +0 -15
  192. package/templates/fe/skeleton-shared/packages/__family__-i18n/tsconfig.json +0 -5
  193. package/templates/fe/tool-config/prettierrc +0 -1
  194. /package/templates/{be → app}/ci-workflows/github/workflows/e2e.yml +0 -0
  195. /package/templates/{be → app}/starciwork.gitignore +0 -0
  196. /package/templates/{be → app}/tool-config/prettierrc +0 -0
  197. /package/templates/fe/{skeleton-app → skeleton}/apps/__app__/next.config.ts +0 -0
  198. /package/templates/fe/{skeleton-app → skeleton}/apps/__app__/src/modules/i18n/config.ts +0 -0
  199. /package/templates/fe/{skeleton-app → skeleton}/apps/__app__/src/modules/i18n/routing.ts +0 -0
@@ -1 +1,6 @@
1
1
  @import "tailwindcss";
2
+ @import "@heroui/styles/css";
3
+ @import "@starci/grammar/common.css";
4
+
5
+ /* The grammar's renderers spell their layout in Tailwind utilities inside node_modules, which Tailwind never scans by default. */
6
+ @source "../../../../../node_modules/@starci/grammar/dist";
@@ -0,0 +1,28 @@
1
+ import { Button, GrammarRoot, Heading, PageContainer, WorkspaceShell } from "@starci/grammar/common"
2
+
3
+ /** Props for {@link FailureScreen}. */
4
+ type FailureScreenProps = {
5
+ /** What failed, said to the reader. */
6
+ readonly title: string
7
+ /** The label of the one recovery action. */
8
+ readonly retryLabel: string
9
+ /** Runs the recovery: the boundary re-renders what failed. */
10
+ readonly onRetry: () => void
11
+ }
12
+
13
+ /** A whole-screen failure with its one recovery action: the drawing both error boundaries share. */
14
+ export const FailureScreen = (props: FailureScreenProps) => (
15
+ <GrammarRoot>
16
+ <WorkspaceShell
17
+ primaryLabel={props.title}
18
+ primary={
19
+ <PageContainer measure="reading">
20
+ <Heading level={1}>{props.title}</Heading>
21
+ <Button variant="danger" onPress={props.onRetry}>
22
+ {props.retryLabel}
23
+ </Button>
24
+ </PageContainer>
25
+ }
26
+ />
27
+ </GrammarRoot>
28
+ )
@@ -0,0 +1,35 @@
1
+ import type { Metadata } from "next"
2
+ import { notFound } from "next/navigation"
3
+ import { hasLocale, NextIntlClientProvider } from "next-intl"
4
+ import { getMessages, getTranslations, setRequestLocale } from "next-intl/server"
5
+ import type { ReactNode } from "react"
6
+ import { siteUrl } from "@/modules/config"
7
+ import { routing } from "@/modules/i18n"
8
+
9
+ type LocaleShellProps = {
10
+ readonly lang: string
11
+ readonly children: ReactNode
12
+ }
13
+
14
+ /** The document title and the absolute base of every metadata URL, for the language in the address. */
15
+ export const localeMetadata = async (lang: string): Promise<Metadata> => {
16
+ if (!hasLocale(routing.locales, lang)) notFound()
17
+ const t = await getTranslations({ locale: lang, namespace: "app" })
18
+ return { title: t("title"), metadataBase: new URL(siteUrl()) }
19
+ }
20
+
21
+ /** The document of one route language: `<html lang>` from the address and the catalog handed to client components. */
22
+ export const LocaleShell = async (props: LocaleShellProps) => {
23
+ if (!hasLocale(routing.locales, props.lang)) notFound()
24
+ setRequestLocale(props.lang)
25
+ const messages = await getMessages()
26
+ return (
27
+ <html lang={props.lang}>
28
+ <body className="min-h-dvh">
29
+ <NextIntlClientProvider locale={props.lang} messages={messages}>
30
+ {props.children}
31
+ </NextIntlClientProvider>
32
+ </body>
33
+ </html>
34
+ )
35
+ }
@@ -0,0 +1,21 @@
1
+ import { FailureScreen } from "@/components/composites/FailureScreen"
2
+
3
+ /** Props for {@link ErrorPageBase}. */
4
+ export type ErrorPageBaseProps = {
5
+ /** Whole-screen situations this surface settles; the error page only ever shows the failure. */
6
+ readonly state: "failed"
7
+ /** The words the failure shows. */
8
+ readonly props: {
9
+ readonly title: string
10
+ readonly retryLabel: string
11
+ }
12
+ /** What the surface reports upward. */
13
+ readonly on: {
14
+ readonly retry: () => void
15
+ }
16
+ }
17
+
18
+ /** Draw the failure of the locale segment with its one recovery action. */
19
+ export const ErrorPageBase = (props: ErrorPageBaseProps) => (
20
+ <FailureScreen title={props.props.title} retryLabel={props.props.retryLabel} onRetry={props.on.retry} />
21
+ )
@@ -0,0 +1,17 @@
1
+ import { useTranslations } from "next-intl"
2
+ import { ErrorPageBase } from "./component"
3
+
4
+ /** The public props of the locale error page: the boundary hands it the retry. */
5
+ type ErrorPageProps = { readonly onRetry: () => void }
6
+
7
+ /** A render failure of the locale segment shows this instead of a blank page; retry re-renders the segment. */
8
+ export const ErrorPage = (props: ErrorPageProps) => {
9
+ const t = useTranslations("errors.page")
10
+ return (
11
+ <ErrorPageBase
12
+ state="failed"
13
+ props={{ title: t("title"), retryLabel: t("retry") }}
14
+ on={{ retry: props.onRetry }}
15
+ />
16
+ )
17
+ }
@@ -0,0 +1,26 @@
1
+ import { FailureScreen } from "@/components/composites/FailureScreen"
2
+
3
+ /** Props for {@link GlobalErrorPageBase}. */
4
+ export type GlobalErrorPageBaseProps = {
5
+ /** Whole-screen situations this surface settles; the global error page only ever shows the failure. */
6
+ readonly state: "failed"
7
+ /** The language of the document and the words the failure shows. */
8
+ readonly props: {
9
+ readonly lang: string
10
+ readonly title: string
11
+ readonly retryLabel: string
12
+ }
13
+ /** What the surface reports upward. */
14
+ readonly on: {
15
+ readonly retry: () => void
16
+ }
17
+ }
18
+
19
+ /** Draw the whole document, because this boundary replaces the root layout that would have drawn it. */
20
+ export const GlobalErrorPageBase = (props: GlobalErrorPageBaseProps) => (
21
+ <html lang={props.props.lang}>
22
+ <body>
23
+ <FailureScreen title={props.props.title} retryLabel={props.props.retryLabel} onRetry={props.on.retry} />
24
+ </body>
25
+ </html>
26
+ )
@@ -0,0 +1,15 @@
1
+ import { DEFAULT_LOCALE } from "@/modules/i18n"
2
+ import messages from "@/modules/i18n/messages/vi.json"
3
+ import { GlobalErrorPageBase } from "./component"
4
+
5
+ /** The public props of the last-resort error page: the boundary hands it the retry. */
6
+ type GlobalErrorPageProps = { readonly onRetry: () => void }
7
+
8
+ /** The last-resort boundary above the locale layout: it has no provider, so it reads the default catalog directly. */
9
+ export const GlobalErrorPage = (props: GlobalErrorPageProps) => (
10
+ <GlobalErrorPageBase
11
+ state="failed"
12
+ props={{ lang: DEFAULT_LOCALE, title: messages.errors.global.title, retryLabel: messages.errors.global.retry }}
13
+ on={{ retry: props.onRetry }}
14
+ />
15
+ )
@@ -0,0 +1,25 @@
1
+ import { GrammarRoot, Heading, PageContainer, WorkspaceShell } from "@starci/grammar/common"
2
+
3
+ /** Props for {@link HomePageBase}. */
4
+ export type HomePageBaseProps = {
5
+ /** Whole-screen situations this surface settles; the home page only ever shows its title. */
6
+ readonly state: "ready"
7
+ /** The words the page shows. */
8
+ readonly props: { readonly title: string }
9
+ /** What the surface reports upward; the page reports nothing. */
10
+ readonly on: Record<never, never>
11
+ }
12
+
13
+ /** Draw the front door of the app. */
14
+ export const HomePageBase = (props: HomePageBaseProps) => (
15
+ <GrammarRoot>
16
+ <WorkspaceShell
17
+ primaryLabel={props.props.title}
18
+ primary={
19
+ <PageContainer measure="reading">
20
+ <Heading level={1}>{props.props.title}</Heading>
21
+ </PageContainer>
22
+ }
23
+ />
24
+ </GrammarRoot>
25
+ )
@@ -0,0 +1,15 @@
1
+ import type { Metadata } from "next"
2
+ import { getTranslations } from "next-intl/server"
3
+ import { HomePageBase } from "./component"
4
+
5
+ /** The document title of the home page, from the catalog of the requested locale. */
6
+ export const homeMetadata = async (): Promise<Metadata> => {
7
+ const t = await getTranslations("home")
8
+ return { title: t("title") }
9
+ }
10
+
11
+ /** The first page of the app: a server component, so no catalog is shipped for it. */
12
+ export const HomePage = async () => {
13
+ const t = await getTranslations("home")
14
+ return <HomePageBase state="ready" props={{ title: t("title") }} on={{}} />
15
+ }
@@ -0,0 +1,27 @@
1
+ import { GrammarRoot, PageContainer, Text, WorkspaceShell } from "@starci/grammar/common"
2
+
3
+ /** Props for {@link LoadingPageBase}. */
4
+ export type LoadingPageBaseProps = {
5
+ /** Whole-screen situations this surface settles; the loading page only ever waits. */
6
+ readonly state: "loading"
7
+ /** The words the wait announces. */
8
+ readonly props: { readonly message: string }
9
+ /** What the surface reports upward; a wait reports nothing. */
10
+ readonly on: Record<never, never>
11
+ }
12
+
13
+ /** Draw the wait as one politely announced line. */
14
+ export const LoadingPageBase = (props: LoadingPageBaseProps) => (
15
+ <GrammarRoot>
16
+ <WorkspaceShell
17
+ primaryLabel={props.props.message}
18
+ primary={
19
+ <PageContainer measure="reading">
20
+ <Text live="polite" tone="muted">
21
+ {props.props.message}
22
+ </Text>
23
+ </PageContainer>
24
+ }
25
+ />
26
+ </GrammarRoot>
27
+ )
@@ -0,0 +1,8 @@
1
+ import { getTranslations } from "next-intl/server"
2
+ import { LoadingPageBase } from "./component"
3
+
4
+ /** Shown while a route of the locale segment resolves. */
5
+ export const LoadingPage = async () => {
6
+ const t = await getTranslations("loading")
7
+ return <LoadingPageBase state="loading" props={{ message: t("message") }} on={{}} />
8
+ }
@@ -0,0 +1,32 @@
1
+ import { GrammarRoot, Heading, PageContainer, TextAction, WorkspaceShell } from "@starci/grammar/common"
2
+ import { ROUTES } from "@/modules/routes"
3
+
4
+ /** Props for {@link NotFoundPageBase}. */
5
+ export type NotFoundPageBaseProps = {
6
+ /** Whole-screen situations this surface settles; the not-found page only ever says the page is missing. */
7
+ readonly state: "missing"
8
+ /** The words the page shows. */
9
+ readonly props: {
10
+ readonly title: string
11
+ readonly homeLabel: string
12
+ }
13
+ /** What the surface reports upward; the page reports nothing. */
14
+ readonly on: Record<never, never>
15
+ }
16
+
17
+ /** Draw the missing-page message and the way back to the front door. */
18
+ export const NotFoundPageBase = (props: NotFoundPageBaseProps) => (
19
+ <GrammarRoot>
20
+ <WorkspaceShell
21
+ primaryLabel={props.props.title}
22
+ primary={
23
+ <PageContainer measure="reading">
24
+ <Heading level={1}>{props.props.title}</Heading>
25
+ <TextAction appearance="inline" href={ROUTES.home}>
26
+ {props.props.homeLabel}
27
+ </TextAction>
28
+ </PageContainer>
29
+ }
30
+ />
31
+ </GrammarRoot>
32
+ )
@@ -0,0 +1,8 @@
1
+ import { getTranslations } from "next-intl/server"
2
+ import { NotFoundPageBase } from "./component"
3
+
4
+ /** Shown when a route calls `notFound()`. */
5
+ export const NotFoundPage = async () => {
6
+ const t = await getTranslations("notFound")
7
+ return <NotFoundPageBase state="missing" props={{ title: t("title"), homeLabel: t("home") }} on={{}} />
8
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The only reader of `process.env` in this app. A value is read when it is asked for, not when the file loads, and a
3
+ * missing value stops the caller with a named error: there is no localhost fallback.
4
+ */
5
+
6
+ const required = (name: string, value: string | undefined): string => {
7
+ if (value === undefined || value === "") throw new Error(`${name} is not set`)
8
+ return value
9
+ }
10
+
11
+ /** The public origin of the app, the base of every absolute metadata URL; throws when `NEXT_PUBLIC_SITE_URL` is not set. */
12
+ export const siteUrl = (): string => required("NEXT_PUBLIC_SITE_URL", process.env.NEXT_PUBLIC_SITE_URL)
@@ -0,0 +1,2 @@
1
+ export { DEFAULT_LOCALE } from "./config"
2
+ export { routing } from "./routing"
@@ -1,7 +1,13 @@
1
1
  {
2
+ "app": {
3
+ "title": "{{project}}"
4
+ },
2
5
  "home": {
3
6
  "title": "Trang chủ"
4
7
  },
8
+ "loading": {
9
+ "message": "Đang tải"
10
+ },
5
11
  "notFound": {
6
12
  "title": "Không tìm thấy trang",
7
13
  "home": "Về trang chủ"
@@ -1,3 +1,4 @@
1
+ import "server-only"
1
2
  import { hasLocale } from "next-intl"
2
3
  import { getRequestConfig } from "next-intl/server"
3
4
  import { routing } from "./routing"
@@ -0,0 +1,4 @@
1
+ /** Every internal destination the screens link to, named once. */
2
+ export const ROUTES = {
3
+ home: "/",
4
+ } as const
@@ -1,5 +1,5 @@
1
1
  import createMiddleware from "next-intl/middleware"
2
- import { routing } from "./modules/i18n/routing"
2
+ import { routing } from "./modules/i18n"
3
3
 
4
4
  /** Negotiates the locale and redirects; the default locale (vi) is served without a prefix. */
5
5
  export default createMiddleware(routing)
package/sync/skeleton.mjs DELETED
@@ -1,76 +0,0 @@
1
- // hfs sync --init: the first source tree of a new repository (entrypoint, platform config/logging/errors, the health
2
- // endpoint; for a front end the next-intl [locale] shell with vi default, as-needed prefix and proxy.ts).
3
- // Unlike the managed files it is written once and never overwritten: an existing file is skipped, so re-running --init
4
- // on a repository that already grew its own source changes nothing.
5
- //
6
- // A front end's skeleton is the templates/fe/skeleton tree (the app shell, common to every repository) plus ONE of two
7
- // trees for what a repository writes once: with one app the app keeps its own i18n stack and its own API client
8
- // (templates/fe/skeleton-app); with two or more apps the stack and the client are written once, as the packages
9
- // `packages/<project>-i18n` and `packages/<project>-api`, and each app keeps a thin adapter over them
10
- // (templates/fe/skeleton-shared). The choice is the number of apps in hfs.json, nothing else.
11
- import fs from 'node:fs';
12
- import path from 'node:path';
13
- import { TEMPLATES_DIR, SyncError, render, validateHfs } from './index.mjs';
14
-
15
- const APP_DIR = '__app__';
16
- const FAMILY_DIR = '__family__';
17
- /** The slots a multi-app front end must opt into in hfs.json before its shared packages are written. */
18
- export const SHARED_PACKAGE_SLOTS = Object.freeze(['fe.package.i18n', 'fe.package.api']);
19
- const pascal = name => name.split('-').map(part => part[0].toUpperCase() + part.slice(1)).join('');
20
-
21
- function listFiles(dir, base = dir) {
22
- return fs.readdirSync(dir, { withFileTypes: true }).flatMap(entry => {
23
- const full = path.join(dir, entry.name);
24
- return entry.isDirectory() ? listFiles(full, base) : [path.relative(base, full).split(path.sep).join('/')];
25
- });
26
- }
27
-
28
- /** The apps that get a skeleton: a back end's `api` apps, every front-end app. */
29
- export const skeletonApps = hfs => hfs.apps.filter(app => hfs.profile === 'fe' || app.kind === 'api');
30
-
31
- /** True when the repository writes its i18n stack and API client once, as packages: a front end with two or more apps. */
32
- export const sharesPackages = hfs => hfs.profile === 'fe' && hfs.apps.length > 1;
33
-
34
- /** The template directories of a repository, in order: the profile's skeleton, then (front end) the one-app or the shared-package tree. */
35
- export const skeletonDirs = hfs => [path.join(TEMPLATES_DIR, hfs.profile, 'skeleton'), ...(hfs.profile === 'fe' ? [path.join(TEMPLATES_DIR, 'fe', sharesPackages(hfs) ? 'skeleton-shared' : 'skeleton-app')] : [])];
36
-
37
- /** Every skeleton file for this repository: [{ path, content }], the shared ones once and the per-app ones per app. */
38
- export function skeletonFiles(hfs, dirs = skeletonDirs(hfs)) {
39
- validateHfs(hfs);
40
- if (sharesPackages(hfs)) {
41
- const missing = SHARED_PACKAGE_SLOTS.filter(id => !(hfs.optionalSlots ?? []).includes(id));
42
- if (missing.length) throw new SyncError('HFS_SYNC_HFS_INVALID', `a front end with ${hfs.apps.length} apps writes its i18n stack and its API client once, as packages: list ${missing.join(' and ')} in hfs.json optionalSlots before hfs sync --init`);
43
- }
44
- const files = [];
45
- for (const dir of dirs) {
46
- if (!fs.existsSync(dir)) throw new SyncError('HFS_SYNC_SKELETON_MISSING', `no skeleton templates in ${path.relative(TEMPLATES_DIR, dir)}`);
47
- for (const rel of listFiles(dir)) {
48
- const source = fs.readFileSync(path.join(dir, rel), 'utf8').replace(/\r\n/g, '\n');
49
- const once = { family: hfs.project, project: hfs.project };
50
- if (!rel.includes(APP_DIR)) {
51
- files.push({ path: rel.split(FAMILY_DIR).join(hfs.project), content: render(source, once) });
52
- continue;
53
- }
54
- for (const app of skeletonApps(hfs)) {
55
- files.push({ path: rel.split(APP_DIR).join(app.name), content: render(source, { ...once, app: app.name, appPascal: pascal(app.name) }) });
56
- }
57
- }
58
- }
59
- return files;
60
- }
61
-
62
- /** Writes the skeleton under `root`, skipping every file that exists: { created, skipped } path lists. */
63
- export function initSkeleton(root, hfs) {
64
- const created = [], skipped = [];
65
- for (const file of skeletonFiles(hfs)) {
66
- const target = path.join(root, file.path);
67
- if (fs.existsSync(target)) {
68
- skipped.push(file.path);
69
- continue;
70
- }
71
- fs.mkdirSync(path.dirname(target), { recursive: true });
72
- fs.writeFileSync(target, file.content);
73
- created.push(file.path);
74
- }
75
- return { created, skipped };
76
- }
@@ -1,2 +0,0 @@
1
- {{> common/gitignore.base}}
2
- schema.gql
@@ -1,13 +0,0 @@
1
- {{header}}
2
- # Commit gate: the secrets guard and work hygiene of the staged files, types, lint and format of the staged files, the unit specs the staged files touch. Never integration, e2e or contract.
3
- npx hfs work-hygiene
4
- npm run typecheck
5
- sources=$(git diff --cached --name-only --diff-filter=ACMR -- '*.ts' '*.mts' '*.cts' '*.js' '*.mjs' '*.cjs' || true)
6
- if [ -n "$sources" ]; then
7
- npx eslint $sources
8
- npx prettier --check $sources
9
- fi
10
- specs=$(git diff --cached --name-only --diff-filter=ACMR -- '*.ts' | grep -v -E '(^|/)src/tests/(world|integration|e2e|contract)/|\.(integration|e2e|contract)-spec\.ts$' || true)
11
- if [ -n "$specs" ]; then
12
- npm run test:affected -- --findRelatedTests $specs
13
- fi
@@ -1,6 +0,0 @@
1
- {{header}}
2
- # Push gate: types, lint, format, unit specs affected since main. Never integration, e2e or contract.
3
- npm run typecheck
4
- npm run lint
5
- npm run format:check
6
- npm run test:affected -- --changedSince=origin/main
@@ -1,19 +0,0 @@
1
- {
2
- "scripts": {
3
- "build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
4
- {{appScripts}}
5
- "typecheck": "tsc -p tsconfig.json",
6
- "typecheck:tests": "tsc -p src/tests/tsconfig.json",
7
- "lint": "hfs lint",
8
- "lint:fix": "hfs lint --fix",
9
- "format": "prettier --write .",
10
- "format:check": "prettier --check .",
11
- "test": "jest --selectProjects unit --coverage",
12
- "test:affected": "jest --selectProjects unit --passWithNoTests",
13
- "test:integration": "npm run typecheck:tests && jest --selectProjects integration",
14
- "test:e2e": "npm run typecheck:tests && jest --selectProjects e2e",
15
- "test:contract": "npm run typecheck:tests && jest --selectProjects contract",
16
- "test:stack": "starci-test-stack",
17
- "contract:emit": "hfs emit-contracts"
18
- }
19
- }
File without changes
@@ -1,9 +0,0 @@
1
- import { injector } from "@modules/platform/composition"
2
- import type { TypedParameterDecorator } from "@modules/platform/composition"
3
- import type { ServerOptions } from "./server.options"
4
-
5
- /** Token under which the app module provides its {@link ServerOptions}. */
6
- export const SERVER_OPTIONS: unique symbol = Symbol("platform.config.server-options")
7
-
8
- /** Injects the server options of the app. Parameter type: ServerOptions. */
9
- export const InjectServerOptions = (): TypedParameterDecorator<ServerOptions> => injector<ServerOptions>(SERVER_OPTIONS)
@@ -1,20 +0,0 @@
1
- import { Catch, HttpException, HttpStatus } from "@nestjs/common"
2
- import type { ArgumentsHost, ExceptionFilter } from "@nestjs/common"
3
- import type { Response } from "express"
4
- import { InjectLogger } from "@modules/platform/logging"
5
- import type { Logger } from "@modules/platform/logging"
6
- import { ErrorsLogEvent } from "./errors.log-events"
7
-
8
- @Catch()
9
- /** The one filter of an app: logs every failure once and answers with a status and a code that reveal nothing else. */
10
- export class ErrorFilter implements ExceptionFilter {
11
- constructor(@InjectLogger() private readonly logger: Logger) {}
12
-
13
- /** Answers the request; an unknown failure is a 500 whose body names no detail. */
14
- catch(exception: unknown, host: ArgumentsHost): void {
15
- const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR
16
- this.logger.error(ErrorsLogEvent.RequestFailed, exception, { status })
17
- const code = status === HttpStatus.INTERNAL_SERVER_ERROR ? "internal_error" : `http_${status}`
18
- host.switchToHttp().getResponse<Response>().status(status).json({ code })
19
- }
20
- }
@@ -1,8 +0,0 @@
1
- {{header}}
2
- dist/
3
- coverage/
4
- node_modules/
5
- package-lock.json
6
- contracts/
7
- .starcistacks/
8
- .starciwork/
@@ -1,40 +0,0 @@
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: lint
25
- run: npm run lint -- --sonar reports/lint.sonar.json
26
- - name: format
27
- run: npm run format:check
28
- - name: typecheck
29
- run: npm run typecheck
30
- - name: build
31
- run: npm run build
32
- - uses: SonarSource/sonarqube-scan-action@v7
33
- if: ${{ !cancelled() && env.SONAR_TOKEN != '' }}
34
- env:
35
- SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }}
36
- - uses: SonarSource/sonarqube-quality-gate-action@v1
37
- if: ${{ !cancelled() && env.SONAR_TOKEN != '' }}
38
- timeout-minutes: 10
39
- env:
40
- SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }}
@@ -1,3 +0,0 @@
1
- {{> common/gitignore.base}}
2
- .next/
3
- next-env.d.ts
@@ -1,16 +0,0 @@
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
@@ -1,5 +0,0 @@
1
- {{header}}
2
- # Push gate: types, lint (eslint over the repository, stylelint over the CSS), format; one `hfs lint` runs eslint, the repository check and stylelint.
3
- npm run typecheck
4
- npm run lint
5
- npm run format:check
@@ -1,13 +0,0 @@
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 && hfs lint --stylelint \"{{styleGlob}}\"",
9
- "lint:fix": "npm run codegen --silent && hfs lint --fix --stylelint \"{{styleGlob}}\"",
10
- "format": "prettier --write .",
11
- "format:check": "prettier --check ."
12
- }
13
- }
@@ -1,44 +0,0 @@
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
- }
@@ -1,7 +0,0 @@
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" }