create-nexst-monorepo 0.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 (303) hide show
  1. package/dist/index.js +1727 -0
  2. package/package.json +37 -0
  3. package/template/.agents/plugins/marketplace.json +21 -0
  4. package/template/.claude/settings.json +6 -0
  5. package/template/.claude/skills/frontend-design/LICENSE.txt +177 -0
  6. package/template/.claude/skills/frontend-design/SKILL.md +154 -0
  7. package/template/.claude/skills/theme-factory/SKILL.md +95 -0
  8. package/template/.claude/skills/theme-factory/references/schema.md +104 -0
  9. package/template/.claude/skills/theme-factory/scripts/theme.mjs +261 -0
  10. package/template/.dockerignore +43 -0
  11. package/template/.editorconfig +10 -0
  12. package/template/.github/workflows/ci.yaml +289 -0
  13. package/template/.github/workflows/publish-app.yaml +66 -0
  14. package/template/.github/workflows/publish.yaml +148 -0
  15. package/template/.github/workflows/snyk.yaml +34 -0
  16. package/template/.vscode/settings.json +8 -0
  17. package/template/AGENTS.md +42 -0
  18. package/template/Dockerfile.base +9 -0
  19. package/template/README.md +54 -0
  20. package/template/_gitignore +10 -0
  21. package/template/apps/api/.env.example +32 -0
  22. package/template/apps/api/AGENTS.md +44 -0
  23. package/template/apps/api/Dockerfile +69 -0
  24. package/template/apps/api/_gitignore +8 -0
  25. package/template/apps/api/knip.config.ts +22 -0
  26. package/template/apps/api/kustomize/base/application.properties +16 -0
  27. package/template/apps/api/kustomize/base/deployment.yaml +94 -0
  28. package/template/apps/api/kustomize/base/kustomization.yaml +17 -0
  29. package/template/apps/api/kustomize/base/service.yaml +10 -0
  30. package/template/apps/api/kustomize/overlays/prod/kustomization.yaml +17 -0
  31. package/template/apps/api/kustomize/overlays/staging/application.properties +1 -0
  32. package/template/apps/api/kustomize/overlays/staging/kustomization.yaml +21 -0
  33. package/template/apps/api/kustomize/secret.keys +14 -0
  34. package/template/apps/api/oxfmt.config.ts +8 -0
  35. package/template/apps/api/oxlint.config.ts +107 -0
  36. package/template/apps/api/package.json +79 -0
  37. package/template/apps/api/prisma/migrations/20260927000000_init/migration.sql +113 -0
  38. package/template/apps/api/prisma/migrations/migration_lock.toml +3 -0
  39. package/template/apps/api/prisma/schema.prisma +113 -0
  40. package/template/apps/api/prisma.config.ts +7 -0
  41. package/template/apps/api/scripts/auth.cli.ts +19 -0
  42. package/template/apps/api/scripts/seal-secret.sh +171 -0
  43. package/template/apps/api/sonar-project.properties +9 -0
  44. package/template/apps/api/src/app-info/app-info.dto.ts +10 -0
  45. package/template/apps/api/src/app-info/app-info.module.ts +7 -0
  46. package/template/apps/api/src/app-info/app-info.resolver.spec.ts +14 -0
  47. package/template/apps/api/src/app-info/app-info.resolver.ts +21 -0
  48. package/template/apps/api/src/app.module.spec.ts +93 -0
  49. package/template/apps/api/src/app.module.ts +77 -0
  50. package/template/apps/api/src/auth/auth.guard.spec.ts +90 -0
  51. package/template/apps/api/src/auth/auth.guard.ts +97 -0
  52. package/template/apps/api/src/auth/auth.ts +129 -0
  53. package/template/apps/api/src/auth/decorator/allow-anonymous.decorator.ts +17 -0
  54. package/template/apps/api/src/auth/decorator/is-email-verified.decorator.spec.ts +41 -0
  55. package/template/apps/api/src/auth/decorator/is-email-verified.decorator.ts +20 -0
  56. package/template/apps/api/src/auth/decorator/roles.decorator.ts +27 -0
  57. package/template/apps/api/src/auth/testing/make-session.ts +31 -0
  58. package/template/apps/api/src/auth/types/auth.types.ts +27 -0
  59. package/template/apps/api/src/bootstrap/register-graphql-enums.ts +12 -0
  60. package/template/apps/api/src/bootstrap/security.ts +24 -0
  61. package/template/apps/api/src/bootstrap/swagger.ts +28 -0
  62. package/template/apps/api/src/config/app.config.spec.ts +22 -0
  63. package/template/apps/api/src/config/app.config.ts +122 -0
  64. package/template/apps/api/src/config/graphql-depth-limit.ts +103 -0
  65. package/template/apps/api/src/config/graphql.config.ts +77 -0
  66. package/template/apps/api/src/config/resources/app.yaml +29 -0
  67. package/template/apps/api/src/errors/app-parse-uuid.pipe.ts +27 -0
  68. package/template/apps/api/src/errors/app.exception.ts +47 -0
  69. package/template/apps/api/src/errors/error-code.enum.ts +54 -0
  70. package/template/apps/api/src/errors/http-status-fallback-code.util.ts +27 -0
  71. package/template/apps/api/src/errors/validation-exception.factory.ts +43 -0
  72. package/template/apps/api/src/health/health.controller.spec.ts +82 -0
  73. package/template/apps/api/src/health/health.controller.ts +57 -0
  74. package/template/apps/api/src/health/health.module.ts +12 -0
  75. package/template/apps/api/src/logging/context-logger.ts +18 -0
  76. package/template/apps/api/src/logging/logger.config.ts +23 -0
  77. package/template/apps/api/src/logging/logging.module.ts +22 -0
  78. package/template/apps/api/src/mail/mail.module.ts +12 -0
  79. package/template/apps/api/src/mail/mail.service.spec.ts +47 -0
  80. package/template/apps/api/src/mail/mail.service.ts +123 -0
  81. package/template/apps/api/src/main.ts +41 -0
  82. package/template/apps/api/src/prisma/prisma.module.ts +12 -0
  83. package/template/apps/api/src/prisma/prisma.service.spec.ts +136 -0
  84. package/template/apps/api/src/prisma/prisma.service.ts +138 -0
  85. package/template/apps/api/src/queue/dto/enqueue-example.input.ts +11 -0
  86. package/template/apps/api/src/queue/example-queue.resolver.ts +14 -0
  87. package/template/apps/api/src/queue/example-queue.service.spec.ts +21 -0
  88. package/template/apps/api/src/queue/example-queue.service.ts +32 -0
  89. package/template/apps/api/src/queue/queue.module.ts +27 -0
  90. package/template/apps/api/src/r2/r2.module.ts +12 -0
  91. package/template/apps/api/src/r2/r2.service.spec.ts +117 -0
  92. package/template/apps/api/src/r2/r2.service.ts +131 -0
  93. package/template/apps/api/src/user/dto/user.dto.ts +26 -0
  94. package/template/apps/api/src/user/user.module.ts +10 -0
  95. package/template/apps/api/src/user/user.resolver.spec.ts +18 -0
  96. package/template/apps/api/src/user/user.resolver.ts +24 -0
  97. package/template/apps/api/src/utils/catch-everything-exception.filter.spec.ts +292 -0
  98. package/template/apps/api/src/utils/catch-everything-exception.filter.ts +70 -0
  99. package/template/apps/api/src/utils/error-handler.util.spec.ts +20 -0
  100. package/template/apps/api/src/utils/error-handler.util.ts +12 -0
  101. package/template/apps/api/src/utils/escape-like-pattern.util.spec.ts +29 -0
  102. package/template/apps/api/src/utils/escape-like-pattern.util.ts +11 -0
  103. package/template/apps/api/src/utils/pagination.input.ts +20 -0
  104. package/template/apps/api/src/utils/prisma-exception.filter.spec.ts +186 -0
  105. package/template/apps/api/src/utils/prisma-exception.filter.ts +45 -0
  106. package/template/apps/api/src/utils/prisma-graphql-exception.filter.spec.ts +216 -0
  107. package/template/apps/api/src/utils/prisma-graphql-exception.filter.ts +43 -0
  108. package/template/apps/api/tests/app.e2e-spec.ts +35 -0
  109. package/template/apps/api/tests/auth.e2e-spec.ts +38 -0
  110. package/template/apps/api/tests/helpers/api.ts +27 -0
  111. package/template/apps/api/tsconfig.json +11 -0
  112. package/template/apps/api/vitest.config.e2e.ts +16 -0
  113. package/template/apps/api/vitest.config.ts +40 -0
  114. package/template/apps/web/.env.example +7 -0
  115. package/template/apps/web/.storybook/decorators.tsx +38 -0
  116. package/template/apps/web/.storybook/main.ts +17 -0
  117. package/template/apps/web/.storybook/preview.ts +38 -0
  118. package/template/apps/web/.storybook/vitest.setup.ts +5 -0
  119. package/template/apps/web/AGENTS.md +29 -0
  120. package/template/apps/web/Dockerfile +49 -0
  121. package/template/apps/web/_gitignore +16 -0
  122. package/template/apps/web/knip.config.ts +15 -0
  123. package/template/apps/web/kustomize/base/application.properties +5 -0
  124. package/template/apps/web/kustomize/base/deployment.yaml +60 -0
  125. package/template/apps/web/kustomize/base/kustomization.yaml +17 -0
  126. package/template/apps/web/kustomize/base/service.yaml +10 -0
  127. package/template/apps/web/kustomize/overlays/prod/kustomization.yaml +12 -0
  128. package/template/apps/web/kustomize/overlays/staging/application.properties +2 -0
  129. package/template/apps/web/kustomize/overlays/staging/kustomization.yaml +18 -0
  130. package/template/apps/web/messages/de.json +124 -0
  131. package/template/apps/web/messages/en.json +124 -0
  132. package/template/apps/web/next-env.d.ts +7 -0
  133. package/template/apps/web/next.config.ts +39 -0
  134. package/template/apps/web/oxfmt.config.ts +9 -0
  135. package/template/apps/web/oxlint.config.ts +36 -0
  136. package/template/apps/web/package.json +80 -0
  137. package/template/apps/web/playwright.config.ts +34 -0
  138. package/template/apps/web/postcss.config.mjs +7 -0
  139. package/template/apps/web/sonar-project.properties +9 -0
  140. package/template/apps/web/src/app/[locale]/(auth)/recovery/page.tsx +22 -0
  141. package/template/apps/web/src/app/[locale]/(auth)/reset-password/page.tsx +29 -0
  142. package/template/apps/web/src/app/[locale]/(auth)/sign-in/page.tsx +24 -0
  143. package/template/apps/web/src/app/[locale]/(auth)/sign-up/page.tsx +24 -0
  144. package/template/apps/web/src/app/[locale]/(auth)/verify/page.tsx +24 -0
  145. package/template/apps/web/src/app/[locale]/dashboard/page.tsx +34 -0
  146. package/template/apps/web/src/app/[locale]/layout.tsx +60 -0
  147. package/template/apps/web/src/app/[locale]/page.tsx +23 -0
  148. package/template/apps/web/src/app/api/health/route.ts +3 -0
  149. package/template/apps/web/src/app/global-error.tsx +19 -0
  150. package/template/apps/web/src/app/globals.css +98 -0
  151. package/template/apps/web/src/app/icon.svg +1 -0
  152. package/template/apps/web/src/app/robots.ts +15 -0
  153. package/template/apps/web/src/app/sitemap.ts +24 -0
  154. package/template/apps/web/src/components/alert/alert.stories.tsx +25 -0
  155. package/template/apps/web/src/components/alert/alert.tsx +50 -0
  156. package/template/apps/web/src/components/avatar/avatar.stories.tsx +116 -0
  157. package/template/apps/web/src/components/avatar/avatar.tsx +88 -0
  158. package/template/apps/web/src/components/button/button-variants.ts +28 -0
  159. package/template/apps/web/src/components/button/button.stories.tsx +33 -0
  160. package/template/apps/web/src/components/button/button.tsx +32 -0
  161. package/template/apps/web/src/components/checkbox/checkbox.stories.tsx +312 -0
  162. package/template/apps/web/src/components/checkbox/checkbox.tsx +38 -0
  163. package/template/apps/web/src/components/dialog/danger-panel.stories.tsx +41 -0
  164. package/template/apps/web/src/components/dialog/danger-panel.tsx +32 -0
  165. package/template/apps/web/src/components/dialog/dialog.stories.tsx +239 -0
  166. package/template/apps/web/src/components/dialog/dialog.tsx +136 -0
  167. package/template/apps/web/src/components/dialog-buttons.stories.tsx +55 -0
  168. package/template/apps/web/src/components/dialog-buttons.tsx +48 -0
  169. package/template/apps/web/src/components/drawer/drawer.stories.tsx +251 -0
  170. package/template/apps/web/src/components/drawer/drawer.tsx +120 -0
  171. package/template/apps/web/src/components/dropdown-menu/dropdown-menu.stories.tsx +329 -0
  172. package/template/apps/web/src/components/dropdown-menu/dropdown-menu.tsx +263 -0
  173. package/template/apps/web/src/components/empty-state.stories.tsx +49 -0
  174. package/template/apps/web/src/components/empty-state.tsx +33 -0
  175. package/template/apps/web/src/components/field-group.stories.tsx +47 -0
  176. package/template/apps/web/src/components/field-group.tsx +20 -0
  177. package/template/apps/web/src/components/field-label.stories.tsx +34 -0
  178. package/template/apps/web/src/components/field-label.tsx +28 -0
  179. package/template/apps/web/src/components/icons.tsx +25 -0
  180. package/template/apps/web/src/components/input/input-otp.stories.tsx +86 -0
  181. package/template/apps/web/src/components/input/input-otp.tsx +184 -0
  182. package/template/apps/web/src/components/input/input.stories.tsx +35 -0
  183. package/template/apps/web/src/components/input/input.tsx +180 -0
  184. package/template/apps/web/src/components/popover/popover.stories.tsx +479 -0
  185. package/template/apps/web/src/components/popover/popover.tsx +45 -0
  186. package/template/apps/web/src/components/responsive-dialog/responsive-dialog.stories.tsx +307 -0
  187. package/template/apps/web/src/components/responsive-dialog/responsive-dialog.tsx +96 -0
  188. package/template/apps/web/src/components/searchbar/searchbar.stories.tsx +59 -0
  189. package/template/apps/web/src/components/searchbar/searchbar.tsx +64 -0
  190. package/template/apps/web/src/components/select/select.stories.tsx +293 -0
  191. package/template/apps/web/src/components/select/select.tsx +445 -0
  192. package/template/apps/web/src/components/skeleton/skeleton.stories.tsx +76 -0
  193. package/template/apps/web/src/components/skeleton/skeleton.tsx +13 -0
  194. package/template/apps/web/src/components/spinner/spinner.tsx +18 -0
  195. package/template/apps/web/src/components/switch/switch.stories.tsx +71 -0
  196. package/template/apps/web/src/components/switch/switch.tsx +35 -0
  197. package/template/apps/web/src/components/tabs/tabs.stories.tsx +209 -0
  198. package/template/apps/web/src/components/tabs/tabs.tsx +69 -0
  199. package/template/apps/web/src/components/tabs/url-tabs.stories.tsx +68 -0
  200. package/template/apps/web/src/components/tabs/url-tabs.tsx +42 -0
  201. package/template/apps/web/src/components/textarea/textarea.stories.tsx +144 -0
  202. package/template/apps/web/src/components/textarea/textarea.tsx +115 -0
  203. package/template/apps/web/src/components/toast/toaster.tsx +38 -0
  204. package/template/apps/web/src/components/tooltip/tooltip.stories.tsx +237 -0
  205. package/template/apps/web/src/components/tooltip/tooltip.tsx +49 -0
  206. package/template/apps/web/src/config/csp.ts +59 -0
  207. package/template/apps/web/src/config/routes.ts +14 -0
  208. package/template/apps/web/src/config/site.ts +5 -0
  209. package/template/apps/web/src/env.ts +15 -0
  210. package/template/apps/web/src/features/auth/auth-card.tsx +20 -0
  211. package/template/apps/web/src/features/auth/auth-divider.tsx +14 -0
  212. package/template/apps/web/src/features/auth/google-auth-button.tsx +49 -0
  213. package/template/apps/web/src/features/auth/google-one-tap.tsx +23 -0
  214. package/template/apps/web/src/features/auth/lib/auth-error.test.ts +22 -0
  215. package/template/apps/web/src/features/auth/lib/auth-error.ts +16 -0
  216. package/template/apps/web/src/features/auth/lib/return-to.test.ts +41 -0
  217. package/template/apps/web/src/features/auth/lib/return-to.ts +19 -0
  218. package/template/apps/web/src/features/auth/lib/validator.test.ts +45 -0
  219. package/template/apps/web/src/features/auth/lib/validator.ts +49 -0
  220. package/template/apps/web/src/features/auth/passkey-settings.tsx +106 -0
  221. package/template/apps/web/src/features/auth/recovery-form.tsx +61 -0
  222. package/template/apps/web/src/features/auth/resend-verification-button.tsx +36 -0
  223. package/template/apps/web/src/features/auth/reset-password-form.tsx +62 -0
  224. package/template/apps/web/src/features/auth/sign-in-form.stories.tsx +22 -0
  225. package/template/apps/web/src/features/auth/sign-in-form.tsx +116 -0
  226. package/template/apps/web/src/features/auth/sign-out-button.tsx +26 -0
  227. package/template/apps/web/src/features/auth/sign-up-form.tsx +107 -0
  228. package/template/apps/web/src/features/shared/provider.tsx +16 -0
  229. package/template/apps/web/src/features/shared/site-header.tsx +48 -0
  230. package/template/apps/web/src/features/shared/theme-toggle.tsx +24 -0
  231. package/template/apps/web/src/i18n/navigation.ts +5 -0
  232. package/template/apps/web/src/i18n/request.ts +21 -0
  233. package/template/apps/web/src/i18n/routing.ts +6 -0
  234. package/template/apps/web/src/lib/auth-client.ts +19 -0
  235. package/template/apps/web/src/lib/auth-server.ts +46 -0
  236. package/template/apps/web/src/lib/auth.ts +8 -0
  237. package/template/apps/web/src/lib/codegen.ts +30 -0
  238. package/template/apps/web/src/lib/graphql-client.ts +29 -0
  239. package/template/apps/web/src/lib/graphql-server.ts +28 -0
  240. package/template/apps/web/src/lib/graphql-utils.test.ts +32 -0
  241. package/template/apps/web/src/lib/graphql-utils.ts +76 -0
  242. package/template/apps/web/src/lib/hooks/use-media-query.ts +21 -0
  243. package/template/apps/web/src/lib/metadata.ts +17 -0
  244. package/template/apps/web/src/lib/network-error.test.ts +30 -0
  245. package/template/apps/web/src/lib/network-error.ts +17 -0
  246. package/template/apps/web/src/lib/proxy-utils.ts +42 -0
  247. package/template/apps/web/src/lib/query-client.ts +29 -0
  248. package/template/apps/web/src/lib/utils.ts +5 -0
  249. package/template/apps/web/src/proxy.ts +57 -0
  250. package/template/apps/web/test-results/.last-run.json +4 -0
  251. package/template/apps/web/tests/auth.spec.ts +17 -0
  252. package/template/apps/web/tests/csp-theme-script.spec.ts +37 -0
  253. package/template/apps/web/tests/home.spec.ts +25 -0
  254. package/template/apps/web/tsconfig.build.json +4 -0
  255. package/template/apps/web/tsconfig.json +28 -0
  256. package/template/apps/web/vitest.config.ts +40 -0
  257. package/template/apps/web/vitest.shims.d.ts +1 -0
  258. package/template/apps/worker/.env.example +4 -0
  259. package/template/apps/worker/AGENTS.md +11 -0
  260. package/template/apps/worker/Dockerfile +35 -0
  261. package/template/apps/worker/knip.config.ts +3 -0
  262. package/template/apps/worker/kustomize/base/application.properties +4 -0
  263. package/template/apps/worker/kustomize/base/deployment.yaml +62 -0
  264. package/template/apps/worker/kustomize/base/kustomization.yaml +16 -0
  265. package/template/apps/worker/kustomize/overlays/prod/kustomization.yaml +12 -0
  266. package/template/apps/worker/kustomize/overlays/staging/application.properties +1 -0
  267. package/template/apps/worker/kustomize/overlays/staging/kustomization.yaml +18 -0
  268. package/template/apps/worker/oxfmt.config.ts +8 -0
  269. package/template/apps/worker/oxlint.config.ts +15 -0
  270. package/template/apps/worker/package.json +33 -0
  271. package/template/apps/worker/sonar-project.properties +8 -0
  272. package/template/apps/worker/src/env.ts +13 -0
  273. package/template/apps/worker/src/lib/graceful-shutdown.ts +27 -0
  274. package/template/apps/worker/src/lib/logger.ts +20 -0
  275. package/template/apps/worker/src/main.ts +17 -0
  276. package/template/apps/worker/src/processors/example-processor.ts +15 -0
  277. package/template/apps/worker/src/worker/redis.ts +19 -0
  278. package/template/apps/worker/src/worker/worker.ts +37 -0
  279. package/template/apps/worker/tests/example-processor.test.ts +13 -0
  280. package/template/apps/worker/tests/graceful-shutdown.test.ts +34 -0
  281. package/template/apps/worker/tsconfig.json +4 -0
  282. package/template/apps/worker/vitest.config.ts +13 -0
  283. package/template/bun.lock +3053 -0
  284. package/template/bunfig.toml +2 -0
  285. package/template/compose.yaml +53 -0
  286. package/template/docker-bake.hcl +86 -0
  287. package/template/package.json +47 -0
  288. package/template/packages/lint/knip.ts +40 -0
  289. package/template/packages/lint/oxfmt.ts +8 -0
  290. package/template/packages/lint/oxlint.ts +136 -0
  291. package/template/packages/lint/package.json +13 -0
  292. package/template/packages/queues/oxfmt.config.ts +8 -0
  293. package/template/packages/queues/oxlint.config.ts +7 -0
  294. package/template/packages/queues/package.json +15 -0
  295. package/template/packages/queues/src/example-job.ts +11 -0
  296. package/template/packages/queues/src/index.ts +3 -0
  297. package/template/packages/queues/src/queue-names.ts +2 -0
  298. package/template/packages/queues/src/redis-connection.ts +27 -0
  299. package/template/packages/queues/tsconfig.json +4 -0
  300. package/template/packages/tsconfig/base.json +23 -0
  301. package/template/packages/tsconfig/node.json +11 -0
  302. package/template/packages/tsconfig/package.json +4 -0
  303. package/template/turbo.json +50 -0
@@ -0,0 +1,216 @@
1
+ import type { TestingModule } from "@nestjs/testing";
2
+ import { Test } from "@nestjs/testing";
3
+ import { GraphQLError } from "graphql";
4
+ import { vi } from "vitest";
5
+ import type { MockInstance } from "vitest";
6
+ import { ErrorCode } from "#src/errors/error-code.enum.js";
7
+ import { Prisma } from "#src/generated/prisma/client.js";
8
+ import { ContextLogger } from "#src/logging/context-logger.js";
9
+ import { PrismaGraphQLExceptionFilter } from "#src/utils/prisma-graphql-exception.filter.js";
10
+
11
+ describe("prismaGraphQLExceptionFilter", () => {
12
+ let filter: PrismaGraphQLExceptionFilter;
13
+ let loggerErrorSpy: MockInstance;
14
+
15
+ beforeEach(async () => {
16
+ const mockLogger = { debug: vi.fn(), error: vi.fn(), setContext: vi.fn() };
17
+
18
+ const module: TestingModule = await Test.createTestingModule({
19
+ providers: [PrismaGraphQLExceptionFilter, { provide: ContextLogger, useValue: mockLogger }],
20
+ }).compile();
21
+
22
+ filter = module.get<PrismaGraphQLExceptionFilter>(PrismaGraphQLExceptionFilter);
23
+ loggerErrorSpy = mockLogger.error;
24
+ });
25
+
26
+ afterEach(() => {
27
+ vi.clearAllMocks();
28
+ });
29
+
30
+ describe("catch", () => {
31
+ it("should handle PrismaClientKnownRequestError and return GraphQLError", () => {
32
+ const prismaError = new Prisma.PrismaClientKnownRequestError(
33
+ "Unique constraint failed on the fields: (`email`)",
34
+ {
35
+ clientVersion: "5.0.0",
36
+ code: "P2002",
37
+ },
38
+ );
39
+
40
+ const result = filter.catch(prismaError);
41
+
42
+ expect(result).toBeInstanceOf(GraphQLError);
43
+ expect(result.message).toBe(ErrorCode.INTERNAL_SERVER_ERROR);
44
+ expect(result.extensions?.code).toBe(ErrorCode.INTERNAL_SERVER_ERROR);
45
+ expect(loggerErrorSpy).toHaveBeenCalledWith(
46
+ { error: prismaError.message, stack: prismaError.stack },
47
+ "Prisma error in GraphQL context",
48
+ );
49
+ });
50
+
51
+ it("should handle PrismaClientUnknownRequestError and return GraphQLError", () => {
52
+ const prismaError = new Prisma.PrismaClientUnknownRequestError("Unknown database error", {
53
+ clientVersion: "5.0.0",
54
+ });
55
+
56
+ const result = filter.catch(prismaError);
57
+
58
+ expect(result).toBeInstanceOf(GraphQLError);
59
+ expect(result.message).toBe(ErrorCode.INTERNAL_SERVER_ERROR);
60
+ expect(result.extensions?.code).toBe(ErrorCode.INTERNAL_SERVER_ERROR);
61
+ expect(loggerErrorSpy).toHaveBeenCalledWith(
62
+ { error: prismaError.message, stack: prismaError.stack },
63
+ "Prisma error in GraphQL context",
64
+ );
65
+ });
66
+
67
+ it("should handle PrismaClientValidationError and return GraphQLError", () => {
68
+ const prismaError = new Prisma.PrismaClientValidationError(
69
+ "Invalid query: missing required field",
70
+ { clientVersion: "5.0.0" },
71
+ );
72
+
73
+ const result = filter.catch(prismaError);
74
+
75
+ expect(result).toBeInstanceOf(GraphQLError);
76
+ expect(result.message).toBe(ErrorCode.INTERNAL_SERVER_ERROR);
77
+ expect(result.extensions?.code).toBe(ErrorCode.INTERNAL_SERVER_ERROR);
78
+ expect(loggerErrorSpy).toHaveBeenCalledWith(
79
+ { error: prismaError.message, stack: prismaError.stack },
80
+ "Prisma error in GraphQL context",
81
+ );
82
+ });
83
+
84
+ it("should log error message and stack for Error instances", () => {
85
+ const error = new Error("Test error message");
86
+
87
+ filter.catch(error);
88
+
89
+ expect(loggerErrorSpy).toHaveBeenCalledWith(
90
+ { error: "Test error message", stack: error.stack },
91
+ "Prisma error in GraphQL context",
92
+ );
93
+ });
94
+
95
+ it("should log string representation for non-Error exceptions", () => {
96
+ const exception = { someProperty: "some value" };
97
+
98
+ filter.catch(exception);
99
+
100
+ expect(loggerErrorSpy).toHaveBeenCalledWith(
101
+ { error: "[object Object]", stack: undefined },
102
+ "Prisma error in GraphQL context",
103
+ );
104
+ });
105
+
106
+ it("should log for null exceptions", () => {
107
+ filter.catch(null);
108
+
109
+ expect(loggerErrorSpy).toHaveBeenCalledWith(
110
+ { error: "null", stack: undefined },
111
+ "Prisma error in GraphQL context",
112
+ );
113
+ });
114
+
115
+ it("should log for undefined exceptions", () => {
116
+ filter.catch(undefined);
117
+
118
+ expect(loggerErrorSpy).toHaveBeenCalledWith(
119
+ { error: "undefined", stack: undefined },
120
+ "Prisma error in GraphQL context",
121
+ );
122
+ });
123
+
124
+ it("should always return same GraphQLError structure", () => {
125
+ const error1 = new Prisma.PrismaClientKnownRequestError("Error 1", {
126
+ clientVersion: "5.0.0",
127
+ code: "P2002",
128
+ });
129
+ const error2 = new Prisma.PrismaClientValidationError("Error 2", {
130
+ clientVersion: "5.0.0",
131
+ });
132
+
133
+ const result1 = filter.catch(error1);
134
+ const result2 = filter.catch(error2);
135
+
136
+ expect(result1.message).toBe(result2.message);
137
+ expect(result1.extensions?.code).toBe(result2.extensions?.code);
138
+ });
139
+
140
+ it("should handle PrismaClientKnownRequestError with P2025 code", () => {
141
+ const prismaError = new Prisma.PrismaClientKnownRequestError(
142
+ "Record to delete does not exist",
143
+ {
144
+ clientVersion: "5.0.0",
145
+ code: "P2025",
146
+ },
147
+ );
148
+
149
+ const result = filter.catch(prismaError);
150
+
151
+ expect(result).toBeInstanceOf(GraphQLError);
152
+ expect(result.message).toBe(ErrorCode.INTERNAL_SERVER_ERROR);
153
+ expect(loggerErrorSpy).toHaveBeenCalled();
154
+ });
155
+
156
+ it("should handle PrismaClientKnownRequestError with P2003 code", () => {
157
+ const prismaError = new Prisma.PrismaClientKnownRequestError(
158
+ "Foreign key constraint failed",
159
+ {
160
+ clientVersion: "5.0.0",
161
+ code: "P2003",
162
+ },
163
+ );
164
+
165
+ const result = filter.catch(prismaError);
166
+
167
+ expect(result).toBeInstanceOf(GraphQLError);
168
+ expect(result.message).toBe(ErrorCode.INTERNAL_SERVER_ERROR);
169
+ expect(loggerErrorSpy).toHaveBeenCalledWith(
170
+ { error: prismaError.message, stack: prismaError.stack },
171
+ "Prisma error in GraphQL context",
172
+ );
173
+ });
174
+
175
+ it("should not expose internal error details in GraphQL response", () => {
176
+ const prismaError = new Prisma.PrismaClientKnownRequestError(
177
+ "Sensitive database information: user_id=123, password=secret",
178
+ {
179
+ clientVersion: "5.0.0",
180
+ code: "P2002",
181
+ },
182
+ );
183
+
184
+ const result = filter.catch(prismaError);
185
+
186
+ expect(result.message).toBe(ErrorCode.INTERNAL_SERVER_ERROR);
187
+ expect(result.message).not.toContain("Sensitive");
188
+ expect(result.message).not.toContain("password");
189
+ expect(result.message).not.toContain("user_id");
190
+ });
191
+
192
+ it("should handle string exceptions", () => {
193
+ const stringException = "Simple string error";
194
+
195
+ const result = filter.catch(stringException);
196
+
197
+ expect(result).toBeInstanceOf(GraphQLError);
198
+ expect(loggerErrorSpy).toHaveBeenCalledWith(
199
+ { error: "Simple string error", stack: undefined },
200
+ "Prisma error in GraphQL context",
201
+ );
202
+ });
203
+
204
+ it("should handle number exceptions", () => {
205
+ const numberException = 404;
206
+
207
+ const result = filter.catch(numberException);
208
+
209
+ expect(result).toBeInstanceOf(GraphQLError);
210
+ expect(loggerErrorSpy).toHaveBeenCalledWith(
211
+ { error: "404", stack: undefined },
212
+ "Prisma error in GraphQL context",
213
+ );
214
+ });
215
+ });
216
+ });
@@ -0,0 +1,43 @@
1
+ import { Catch } from "@nestjs/common";
2
+ import type { GqlExceptionFilter } from "@nestjs/graphql";
3
+ import { GraphQLError } from "graphql";
4
+ import { ErrorCode } from "#src/errors/error-code.enum.js";
5
+ import { Prisma } from "#src/generated/prisma/client.js";
6
+ import { ContextLogger } from "#src/logging/context-logger.js";
7
+ import { handleError } from "#src/utils/error-handler.util.js";
8
+
9
+ /**
10
+ * GraphQL exception filter for handling Prisma database errors in GraphQL contexts.
11
+ *
12
+ * This filter intercepts Prisma client errors that occur during GraphQL query/mutation
13
+ * execution and transforms them into standardized GraphQL errors. It prevents sensitive
14
+ * database information from being exposed to clients by returning generic error messages.
15
+ */
16
+ @Catch(
17
+ Prisma.PrismaClientKnownRequestError,
18
+ Prisma.PrismaClientUnknownRequestError,
19
+ Prisma.PrismaClientValidationError,
20
+ )
21
+ export class PrismaGraphQLExceptionFilter implements GqlExceptionFilter {
22
+ constructor(readonly logger: ContextLogger) {}
23
+
24
+ /**
25
+ * Catches and processes Prisma errors in GraphQL context.
26
+ *
27
+ * @param exception - The Prisma error that was thrown
28
+ * @returns A GraphQL error with sanitized error information
29
+ */
30
+ catch(exception: unknown) {
31
+ this.logger.error(
32
+ {
33
+ error: handleError(exception),
34
+ stack: exception instanceof Error ? exception.stack : undefined,
35
+ },
36
+ "Prisma error in GraphQL context",
37
+ );
38
+
39
+ return new GraphQLError(ErrorCode.INTERNAL_SERVER_ERROR, {
40
+ extensions: { code: ErrorCode.INTERNAL_SERVER_ERROR },
41
+ });
42
+ }
43
+ }
@@ -0,0 +1,35 @@
1
+ import { API_URL, graphql } from "#tests/helpers/api.js";
2
+
3
+ describe("app (e2e)", () => {
4
+ describe("health", () => {
5
+ it("is ready", async () => {
6
+ const response = await fetch(`${API_URL}/health`);
7
+
8
+ expect(response.status).toBe(200);
9
+ });
10
+
11
+ it("is live", async () => {
12
+ const response = await fetch(`${API_URL}/health/live`);
13
+
14
+ expect(response.status).toBe(200);
15
+ await expect(response.json()).resolves.toMatchObject({ status: "ok" });
16
+ });
17
+ });
18
+
19
+ describe("graphql", () => {
20
+ it("answers the public appInfo query", async () => {
21
+ const { data, errors } = await graphql<{ appInfo: { title: string } }>(
22
+ "query { appInfo { title version } }",
23
+ );
24
+
25
+ expect(errors).toBeUndefined();
26
+ expect(data?.appInfo.title).toBeTruthy();
27
+ });
28
+
29
+ it("rejects malformed input with a machine-readable code", async () => {
30
+ const { errors } = await graphql("query { doesNotExist }");
31
+
32
+ expect(errors?.[0]?.extensions?.code).toBeTruthy();
33
+ });
34
+ });
35
+ });
@@ -0,0 +1,38 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { API_URL, graphql } from "#tests/helpers/api.js";
3
+
4
+ /**
5
+ * Needs the API running with NODE_ENV=test so sign-up skips the verification
6
+ * mail (see `buildBaseAuthOptions`).
7
+ */
8
+ describe("auth (e2e)", () => {
9
+ const email = `e2e-${randomUUID()}@example.com`;
10
+ const password = "correct-horse-battery-staple";
11
+
12
+ async function signUp(): Promise<string> {
13
+ const response = await fetch(`${API_URL}/api/auth/sign-up/email`, {
14
+ body: JSON.stringify({ email, name: "E2E", password }),
15
+ headers: { "Content-Type": "application/json", Origin: "http://localhost:3000" },
16
+ method: "POST",
17
+ });
18
+ expect(response.status).toBe(200);
19
+ return response.headers.getSetCookie().join("; ");
20
+ }
21
+
22
+ it("rejects `me` without a session", async () => {
23
+ const { errors } = await graphql("query { me { id } }");
24
+
25
+ expect(errors?.[0]?.extensions?.code).toBe("UNAUTHORIZED");
26
+ });
27
+
28
+ it("signs up and resolves `me` from the session cookie", async () => {
29
+ const cookie = await signUp();
30
+
31
+ const { data, errors } = await graphql<{ me: { email: string } }>("query { me { email } }", {
32
+ cookie,
33
+ });
34
+
35
+ expect(errors).toBeUndefined();
36
+ expect(data?.me.email).toBe(email);
37
+ });
38
+ });
@@ -0,0 +1,27 @@
1
+ export const API_URL = process.env.API_URL ?? "http://localhost:3001";
2
+
3
+ type GraphQLResponse<TData> = {
4
+ data?: TData;
5
+ errors?: { extensions?: { code?: string; params?: unknown }; message: string }[];
6
+ };
7
+
8
+ /**
9
+ * Sends one GraphQL operation to the running API.
10
+ *
11
+ * @param query - The operation document
12
+ * @param options - Variables and an optional `Cookie` header value (the session)
13
+ */
14
+ export async function graphql<TData>(
15
+ query: string,
16
+ { cookie, variables }: { cookie?: string; variables?: Record<string, unknown> } = {},
17
+ ): Promise<GraphQLResponse<TData>> {
18
+ const response = await fetch(`${API_URL}/graphql`, {
19
+ body: JSON.stringify({ query, variables }),
20
+ headers: {
21
+ "Content-Type": "application/json",
22
+ ...(cookie ? { Cookie: cookie } : {}),
23
+ },
24
+ method: "POST",
25
+ });
26
+ return (await response.json()) as GraphQLResponse<TData>;
27
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "@nexst/tsconfig/node.json",
3
+ "compilerOptions": {
4
+ "allowImportingTsExtensions": true,
5
+ "emitDecoratorMetadata": true,
6
+ "experimentalDecorators": true,
7
+ "strictPropertyInitialization": false,
8
+ "types": ["vitest/globals", "node"]
9
+ },
10
+ "include": ["**/*.ts"]
11
+ }
@@ -0,0 +1,16 @@
1
+ import { existsSync } from "node:fs";
2
+ import { defineConfig } from "vitest/config";
3
+
4
+ if (existsSync(".env.development")) process.loadEnvFile(".env.development");
5
+
6
+ /**
7
+ * E2E specs talk to a running API (`bun dev` locally, the rolled-out image in
8
+ * CI) at `API_URL`, default http://localhost:3001.
9
+ */
10
+ export default defineConfig({
11
+ test: {
12
+ globals: true,
13
+ include: ["tests/**/*.e2e-spec.ts"],
14
+ root: "./",
15
+ },
16
+ });
@@ -0,0 +1,40 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { defineConfig } from "vitest/config";
3
+
4
+ export default defineConfig({
5
+ resolve: {
6
+ // graphql ships a "development" export condition; without pinning one entry,
7
+ // @nestjs/graphql and the app load two graphql instances ("Cannot use
8
+ // GraphQLSchema from another module or realm").
9
+ alias: [{ find: /^graphql$/, replacement: fileURLToPath(import.meta.resolve("graphql")) }],
10
+ },
11
+ test: {
12
+ coverage: {
13
+ exclude: ["src/generated/**", "**/*.spec.ts"],
14
+ include: ["src/**/*.ts"],
15
+ provider: "v8",
16
+ reporter: ["text", "lcov"],
17
+ },
18
+ // Unit tests never reach real services; these only satisfy config validation.
19
+ env: {
20
+ // @if auth
21
+ BETTER_AUTH_SECRET: "unit-test-secret-unit-test-secret-1234",
22
+ BETTER_AUTH_URL: "http://localhost:3001",
23
+ // @endif
24
+ DATABASE_URL: "postgresql://user:pass@localhost:5432/db",
25
+ // @if r2
26
+ R2_ACCESS_KEY_ID: "unit-test",
27
+ R2_BUCKET: "unit-test",
28
+ R2_ENDPOINT: "https://unit-test.r2.cloudflarestorage.com",
29
+ R2_SECRET_ACCESS_KEY: "unit-test",
30
+ // @endif
31
+ },
32
+ globals: true,
33
+ include: ["src/**/*.spec.ts"],
34
+ isolate: false,
35
+ root: "./",
36
+ // @if worker
37
+ server: { deps: { inline: ["@nestjs/bullmq"] } },
38
+ // @endif
39
+ },
40
+ });
@@ -0,0 +1,7 @@
1
+ # Copy to .env (bun/next load it automatically) and adjust.
2
+ NEXT_PUBLIC_API_URL=http://localhost:3001
3
+ NEXT_PUBLIC_HOST_URL=http://localhost:3000
4
+ # @if auth
5
+ # Google sign-in (button + One Tap); same client ID as GOOGLE_CLIENT_ID in apps/api. Hidden while unset.
6
+ # NEXT_PUBLIC_GOOGLE_CLIENT_ID=<id>.apps.googleusercontent.com
7
+ # @endif
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Shared Storybook decorators. Stories import their providers from here so every
3
+ * story renders inside the same shell as the app.
4
+ */
5
+ import type { Decorator } from "@storybook/nextjs-vite";
6
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
7
+ import { NextIntlClientProvider } from "next-intl";
8
+ import type { ComponentProps } from "react";
9
+ import enMessages from "../messages/en.json";
10
+
11
+ type IntlMessages = ComponentProps<typeof NextIntlClientProvider>["messages"];
12
+
13
+ /** Centers the story and constrains its width — handy for form/card components. */
14
+ export function withContainer(className = "w-96"): Decorator {
15
+ return (Story) => (
16
+ <div className={className}>
17
+ <Story />
18
+ </div>
19
+ );
20
+ }
21
+
22
+ function makeQueryClient() {
23
+ return new QueryClient({
24
+ defaultOptions: {
25
+ mutations: { retry: false },
26
+ queries: { retry: false, staleTime: Number.POSITIVE_INFINITY },
27
+ },
28
+ });
29
+ }
30
+
31
+ /** Intl (English catalog) + a fresh React Query client. Registered globally in preview.ts. */
32
+ export const withProviders: Decorator = (Story) => (
33
+ <QueryClientProvider client={makeQueryClient()}>
34
+ <NextIntlClientProvider locale="en" messages={enMessages as IntlMessages}>
35
+ <Story />
36
+ </NextIntlClientProvider>
37
+ </QueryClientProvider>
38
+ );
@@ -0,0 +1,17 @@
1
+ import type { StorybookConfig } from "@storybook/nextjs-vite";
2
+
3
+ const config: StorybookConfig = {
4
+ addons: [
5
+ "@storybook/addon-docs",
6
+ "@storybook/addon-a11y",
7
+ "@storybook/addon-vitest",
8
+ "@storybook/addon-themes",
9
+ ],
10
+ framework: {
11
+ name: "@storybook/nextjs-vite",
12
+ options: {},
13
+ },
14
+ staticDirs: ["../public"],
15
+ stories: ["../src/**/*.stories.@(ts|tsx)"],
16
+ };
17
+ export default config;
@@ -0,0 +1,38 @@
1
+ import { withThemeByClassName } from "@storybook/addon-themes";
2
+ import type { Preview } from "@storybook/nextjs-vite";
3
+ import { withProviders } from "./decorators";
4
+ import "../src/app/globals.css";
5
+
6
+ const preview: Preview = {
7
+ decorators: [
8
+ withProviders,
9
+ // Globals are "Light"/"Dark" (capitalised) — the addon crashes on other values.
10
+ withThemeByClassName({
11
+ defaultTheme: "Light",
12
+ themes: {
13
+ Dark: "dark",
14
+ Light: "",
15
+ },
16
+ }),
17
+ ],
18
+ parameters: {
19
+ a11y: {
20
+ // Every story is an axe check in `bun run test:unit`; violations fail the run.
21
+ test: "error",
22
+ },
23
+ controls: {
24
+ matchers: {
25
+ color: /(background|color)$/i,
26
+ date: /Date$/i,
27
+ },
28
+ },
29
+ nextjs: {
30
+ appDirectory: true,
31
+ navigation: {
32
+ pathname: "/",
33
+ },
34
+ },
35
+ },
36
+ };
37
+
38
+ export default preview;
@@ -0,0 +1,5 @@
1
+ import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview";
2
+ import { setProjectAnnotations } from "@storybook/nextjs-vite";
3
+ import * as projectAnnotations from "./preview";
4
+
5
+ setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]);
@@ -0,0 +1,29 @@
1
+ <!-- BEGIN:nextjs-agent-rules -->
2
+
3
+ # This is NOT the Next.js you know
4
+
5
+ This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
6
+
7
+ This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
8
+
9
+ <!-- END:nextjs-agent-rules -->
10
+
11
+ # web (Next.js)
12
+
13
+ - i18n via next-intl: every user-facing string goes through `messages/*.json`, in every
14
+ locale file. Routes live under `src/app/[locale]`; use `Link`/`useRouter` from
15
+ `@/i18n/navigation`.
16
+ - Styling: Tailwind 4 with the semantic tokens in `src/app/globals.css`
17
+ (`bg-background`, `text-muted-foreground`, …) — no raw hex or ramp steps in
18
+ components. Reuse `src/components/*`.
19
+ - CSP lives in `src/config/csp.ts` and is applied by `src/proxy.ts`. New external
20
+ origins (API, CDN, images) must be added there. `tests/csp-theme-script.spec.ts`
21
+ guards the inline-script hashes.
22
+ - API calls: `@/lib/graphql-client` (browser, cookies included) or
23
+ `@/lib/graphql-server` (server components, forward the cookie). Type documents with
24
+ `graphql()` from `@/generated/gql` after `bun run codegen` (API must be running);
25
+ never hand-type `TypedDocumentNode`s.
26
+ - Server-only env in `src/env.ts` (`@t3-oss/env-nextjs`); `NEXT_PUBLIC_*` are baked in
27
+ at build time (Docker build args).
28
+ - Tests: `bun run test:unit` (Vitest unit + Storybook stories incl. axe a11y),
29
+ `bun run test` (Playwright against `next start`).
@@ -0,0 +1,49 @@
1
+ ARG BUN_VERSION
2
+
3
+ # ---------------------------------------------------
4
+ # Stage 1: Pruner
5
+ # ---------------------------------------------------
6
+ FROM toolchain AS pruner
7
+ RUN turbo prune @nexst/web --docker
8
+
9
+ # ---------------------------------------------------
10
+ # Stage 2: Builder
11
+ # ---------------------------------------------------
12
+ FROM dhi.io/bun:${BUN_VERSION}-alpine-dev AS builder
13
+ WORKDIR /app
14
+ COPY --from=pruner /app/out/json/ .
15
+ RUN --mount=type=cache,id=bun-install,sharing=locked,target=/root/.bun/install/cache \
16
+ bun install --frozen-lockfile
17
+ COPY --from=pruner /app/out/full/ .
18
+
19
+ ARG NEXT_PUBLIC_HOST_URL
20
+ ARG NEXT_PUBLIC_API_URL
21
+ # @if auth
22
+ ARG NEXT_PUBLIC_GOOGLE_CLIENT_ID
23
+ # @endif
24
+
25
+ RUN cd apps/web && \
26
+ bun run build && \
27
+ cd .next/standalone/node_modules/.bun && \
28
+ rm -rf sharp@* @img+sharp-* node_modules/sharp node_modules/@img/sharp-* next@*/node_modules/sharp
29
+
30
+ # ---------------------------------------------------
31
+ # Stage 3: Runtime
32
+ # ---------------------------------------------------
33
+ FROM dhi.io/bun:${BUN_VERSION}-alpine AS runtime
34
+ WORKDIR /app
35
+ ENV NODE_ENV=production \
36
+ NEXT_TELEMETRY_DISABLED=1 \
37
+ HOSTNAME=0.0.0.0 \
38
+ PORT=3000
39
+
40
+ COPY --from=builder --chown=nonroot:nonroot /app/apps/web/.next/standalone ./
41
+ COPY --from=builder --chown=nonroot:nonroot /app/apps/web/.next/static ./apps/web/.next/static
42
+ COPY --from=builder --chown=nonroot:nonroot /app/apps/web/public ./apps/web/public
43
+
44
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=15s --retries=3 \
45
+ CMD ["bun", "-e", "require('http').get('http://localhost:3000/api/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) }).on('error', () => process.exit(1))"]
46
+
47
+ EXPOSE 3000
48
+ STOPSIGNAL SIGTERM
49
+ CMD ["bun", "apps/web/server.js"]
@@ -0,0 +1,16 @@
1
+ # next.js
2
+ /.next/
3
+ /out/
4
+ next-env.d.ts
5
+
6
+ # graphql codegen output (bun run codegen)
7
+ /src/generated/
8
+
9
+ # playwright
10
+ /playwright-report/
11
+ /playwright/.cache/
12
+ /test-results/
13
+
14
+ # storybook
15
+ *storybook.log
16
+ storybook-static
@@ -0,0 +1,15 @@
1
+ import { createKnipConfig } from "@nexst/lint/knip";
2
+ import type { KnipConfig } from "knip";
3
+
4
+ const config: KnipConfig = createKnipConfig({
5
+ entry: [
6
+ // API clients are template infrastructure: keep them until the first query uses them.
7
+ "src/lib/graphql-client.ts",
8
+ "src/lib/graphql-server.ts",
9
+ // The UI kit is a library: components stay exported until the app uses them.
10
+ "src/components/**/*.tsx", // @if ui
11
+ ],
12
+ project: ["src/**/*.{css,mdx}"],
13
+ });
14
+
15
+ export default config;
@@ -0,0 +1,5 @@
1
+ HOSTNAME=0.0.0.0
2
+ NODE_ENV=production
3
+ LOG_LEVEL=info
4
+ NEXT_PUBLIC_API_URL=https://api.example.com
5
+ NEXT_PUBLIC_HOST_URL=https://example.com