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,261 @@
1
+ #!/usr/bin/env node
2
+ // Theme factory: validate a theme JSON and emit Tailwind v4 @theme CSS from it.
3
+ //
4
+ // node theme.mjs validate <theme.json>
5
+ // node theme.mjs emit <theme.json> [--out file.css]
6
+ // node theme.mjs contrast <theme.json>
7
+ //
8
+ // No dependencies. The theme JSON is the source of truth; the CSS is a build
9
+ // product. Never hand-edit the emitted CSS — change the JSON and re-emit.
10
+
11
+ import { readFileSync, writeFileSync } from 'node:fs';
12
+
13
+ /* ---------------------------------------------------------------- color --- */
14
+
15
+ const hex = (c) => {
16
+ const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(c.trim());
17
+ if (!m) return null;
18
+ const h = m[1].length === 3 ? [...m[1]].map((d) => d + d).join('') : m[1];
19
+ return [0, 2, 4].map((i) => Number.parseInt(h.slice(i, i + 2), 16));
20
+ };
21
+
22
+ const luminance = (rgb) => {
23
+ const [r, g, b] = rgb.map((v) => {
24
+ const s = v / 255;
25
+ return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
26
+ });
27
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
28
+ };
29
+
30
+ // WCAG 2.1 contrast ratio, 1..21.
31
+ const contrast = (a, b) => {
32
+ const [la, lb] = [luminance(a), luminance(b)];
33
+ const [hi, lo] = la > lb ? [la, lb] : [lb, la];
34
+ return (hi + 0.05) / (lo + 0.05);
35
+ };
36
+
37
+ // Flatten a colour that carries alpha over an opaque backdrop.
38
+ const over = (fg, bg, alpha) => fg.map((v, i) => Math.round(v * alpha + bg[i] * (1 - alpha)));
39
+
40
+ /* ------------------------------------------------------------- resolving --- */
41
+
42
+ // Semantic values may point at a ramp entry ("brand-blue-500") or be a literal
43
+ // hex. Resolve to hex so contrast can be measured on real pixels.
44
+ function resolve(value, theme, mode) {
45
+ const seen = new Set();
46
+ let v = value;
47
+ while (typeof v === 'string' && !v.startsWith('#')) {
48
+ if (seen.has(v)) throw new Error(`circular colour reference: ${value}`);
49
+ seen.add(v);
50
+ const ramp = Object.entries(theme.ramps).find(([name]) => v.startsWith(`${name}-`));
51
+ if (ramp) {
52
+ const step = v.slice(ramp[0].length + 1);
53
+ v = ramp[1][step];
54
+ if (!v) throw new Error(`unknown ramp step: ${value}`);
55
+ continue;
56
+ }
57
+ const next = theme.semantic[mode][v] ?? theme.semantic.light[v];
58
+ if (!next) throw new Error(`unresolvable colour: ${value}`);
59
+ v = next;
60
+ }
61
+ return v;
62
+ }
63
+
64
+ /* ------------------------------------------------------------ validation --- */
65
+
66
+ // Pairs that must stay legible. [foreground, background, minimum ratio].
67
+ // 4.5 for body text, 3.0 for large text / UI boundaries per WCAG 1.4.3 + 1.4.11.
68
+ const PAIRS = [
69
+ ['foreground', 'background', 4.5],
70
+ ['foreground', 'card', 4.5],
71
+ ['muted-foreground', 'background', 4.5],
72
+ ['muted-foreground', 'card', 4.5],
73
+ ['desc', 'card', 4.5],
74
+ ['primary-foreground', 'primary', 4.5],
75
+ ['secondary-foreground', 'secondary', 4.5],
76
+ ['destructive-foreground', 'destructive-solid', 4.5],
77
+ ['accent-foreground', 'accent', 4.5],
78
+ ['destructive', 'card', 4.5],
79
+ ['border', 'background', 1.0],
80
+ ['ring', 'background', 3.0],
81
+ ];
82
+
83
+ function validate(theme) {
84
+ const problems = [];
85
+ const warn = (m) => problems.push({ level: 'warn', message: m });
86
+ const fail = (m) => problems.push({ level: 'fail', message: m });
87
+
88
+ for (const field of ['name', 'ramps', 'semantic', 'radius', 'type', 'space', 'font']) {
89
+ if (!theme[field]) fail(`missing top-level field: ${field}`);
90
+ }
91
+ if (problems.length) return problems;
92
+
93
+ for (const [name, ramp] of Object.entries(theme.ramps)) {
94
+ for (const [step, value] of Object.entries(ramp)) {
95
+ if (!hex(value)) fail(`ramp ${name}-${step} is not a hex colour: ${value}`);
96
+ }
97
+ const steps = Object.keys(ramp).map(Number).sort((a, b) => a - b);
98
+ const lums = steps.map((s) => luminance(hex(ramp[String(s)])));
99
+ for (let i = 1; i < lums.length; i++) {
100
+ if (lums[i] >= lums[i - 1]) {
101
+ warn(`ramp ${name} is not monotonically darkening at step ${steps[i]}`);
102
+ }
103
+ }
104
+ }
105
+
106
+ const modes = Object.keys(theme.semantic);
107
+ const [first, ...rest] = modes;
108
+ for (const mode of rest) {
109
+ for (const key of Object.keys(theme.semantic[first])) {
110
+ // A mode may inherit a token by omitting it, but only if the inherited
111
+ // value is a ramp reference — a literal hex tuned for light will not hold
112
+ // on a dark surface. List deliberate exceptions in `verifiedInherits`.
113
+ if (!(key in theme.semantic[mode]) && !theme.verifiedInherits?.includes(key)) {
114
+ const inherited = theme.semantic[first][key];
115
+ if (typeof inherited === 'string' && inherited.startsWith('#')) {
116
+ warn(`${mode} inherits literal ${key}: ${inherited} — verify on the ${mode} surface`);
117
+ }
118
+ }
119
+ }
120
+ }
121
+
122
+ for (const mode of modes) {
123
+ for (const [fg, bg, min] of PAIRS) {
124
+ if (!(fg in theme.semantic[mode]) && !(fg in theme.semantic[first])) continue;
125
+ if (!(bg in theme.semantic[mode]) && !(bg in theme.semantic[first])) continue;
126
+ let ratio;
127
+ try {
128
+ ratio = contrast(hex(resolve(fg, theme, mode)), hex(resolve(bg, theme, mode)));
129
+ } catch (error) {
130
+ fail(`${mode}: ${error.message}`);
131
+ continue;
132
+ }
133
+ if (ratio < min) {
134
+ fail(`${mode}: ${fg} on ${bg} is ${ratio.toFixed(2)}:1, needs ${min}:1`);
135
+ }
136
+ }
137
+ }
138
+
139
+ // Categorical hues must be distinguishable from each other and from the
140
+ // surface they sit on, in every mode.
141
+ const categorical = Object.keys(theme.semantic[first]).filter((k) => k.startsWith('kind-'));
142
+ for (const mode of modes) {
143
+ const surface = hex(resolve('card', theme, mode));
144
+ for (const key of categorical) {
145
+ const c = hex(resolve(key, theme, mode));
146
+ const ratio = contrast(c, surface);
147
+ if (ratio < 3) fail(`${mode}: ${key} on card is ${ratio.toFixed(2)}:1, needs 3:1`);
148
+ }
149
+ for (let i = 0; i < categorical.length; i++) {
150
+ for (let j = i + 1; j < categorical.length; j++) {
151
+ const a = hex(resolve(categorical[i], theme, mode));
152
+ const b = hex(resolve(categorical[j], theme, mode));
153
+ const dl = Math.abs(luminance(a) - luminance(b));
154
+ const dh = Math.max(...[0, 1, 2].map((k) => Math.abs(a[k] - b[k])));
155
+ if (dl < 0.02 && dh < 60) {
156
+ warn(`${mode}: ${categorical[i]} and ${categorical[j]} may be hard to tell apart`);
157
+ }
158
+ }
159
+ }
160
+ }
161
+
162
+ // A single radius that every named step resolves to is a deliberate choice;
163
+ // a scale with one odd step out is usually drift.
164
+ const radii = Object.values(theme.radius).filter((v) => !v.startsWith('calc'));
165
+ if (new Set(radii).size > 4) warn(`${new Set(radii).size} distinct radii — is every one earning its place?`);
166
+
167
+ return problems;
168
+ }
169
+
170
+ /* -------------------------------------------------------------- emitting --- */
171
+
172
+ const cssColor = (value, theme) =>
173
+ value.startsWith('#') ? value : `var(--color-${value})`;
174
+
175
+ function emit(theme) {
176
+ const modes = Object.keys(theme.semantic);
177
+ const [base, ...overrides] = modes;
178
+ const out = [];
179
+
180
+ out.push(`/* Generated by theme-factory from ${theme.name}.theme.json — do not hand-edit. */`);
181
+ out.push('@theme {');
182
+ for (const [name, ramp] of Object.entries(theme.ramps)) {
183
+ if (theme.rampNotes?.[name]) out.push(` /* ${theme.rampNotes[name]} */`);
184
+ for (const [step, value] of Object.entries(ramp)) {
185
+ out.push(` --color-${name}-${step}: ${value};`);
186
+ }
187
+ out.push('');
188
+ }
189
+ for (const [key, value] of Object.entries(theme.semantic[base])) {
190
+ if (theme.notes?.[key]) out.push(` /* ${theme.notes[key]} */`);
191
+ out.push(` --color-${key}: ${cssColor(value, theme)};`);
192
+ }
193
+ out.push('');
194
+ for (const [key, value] of Object.entries(theme.radius)) {
195
+ out.push(` --${key}: ${value};`);
196
+ }
197
+ out.push('}');
198
+
199
+ for (const mode of overrides) {
200
+ out.push('');
201
+ out.push('@layer theme {');
202
+ out.push(` .${mode} {`);
203
+ for (const [key, value] of Object.entries(theme.semantic[mode])) {
204
+ out.push(` --color-${key}: ${cssColor(value, theme)};`);
205
+ }
206
+ out.push(' }');
207
+ out.push('}');
208
+ }
209
+ return `${out.join('\n')}\n`;
210
+ }
211
+
212
+ function report(theme) {
213
+ const modes = Object.keys(theme.semantic);
214
+ const rows = [];
215
+ for (const mode of modes) {
216
+ for (const [fg, bg, min] of PAIRS) {
217
+ try {
218
+ const ratio = contrast(hex(resolve(fg, theme, mode)), hex(resolve(bg, theme, mode)));
219
+ rows.push([mode, `${fg} on ${bg}`, `${ratio.toFixed(2)}:1`, ratio >= min ? 'pass' : 'FAIL']);
220
+ } catch {
221
+ /* reported by validate */
222
+ }
223
+ }
224
+ }
225
+ const w = [0, 1, 2].map((i) => Math.max(...rows.map((r) => r[i].length)));
226
+ return rows.map((r) => `${r[0].padEnd(w[0])} ${r[1].padEnd(w[1])} ${r[2].padStart(w[2])} ${r[3]}`).join('\n');
227
+ }
228
+
229
+ /* ------------------------------------------------------------------ cli --- */
230
+
231
+ const [command, file, ...args] = process.argv.slice(2);
232
+ if (!command || !file) {
233
+ console.error('usage: theme.mjs <validate|emit|contrast> <theme.json> [--out file.css]');
234
+ process.exit(2);
235
+ }
236
+
237
+ const theme = JSON.parse(readFileSync(file, 'utf8'));
238
+
239
+ if (command === 'validate') {
240
+ const problems = validate(theme);
241
+ for (const p of problems) console.log(`${p.level === 'fail' ? 'FAIL' : 'warn'} ${p.message}`);
242
+ const failed = problems.filter((p) => p.level === 'fail').length;
243
+ console.log(failed ? `\n${failed} failure(s)` : `\n${theme.name}: ok (${problems.length} warning(s))`);
244
+ process.exit(failed ? 1 : 0);
245
+ } else if (command === 'emit') {
246
+ const css = emit(theme);
247
+ const outIndex = args.indexOf('--out');
248
+ if (outIndex !== -1 && args[outIndex + 1]) {
249
+ writeFileSync(args[outIndex + 1], css);
250
+ console.log(`wrote ${args[outIndex + 1]}`);
251
+ } else {
252
+ process.stdout.write(css);
253
+ }
254
+ } else if (command === 'contrast') {
255
+ console.log(report(theme));
256
+ } else {
257
+ console.error(`unknown command: ${command}`);
258
+ process.exit(2);
259
+ }
260
+
261
+ export { contrast, emit, hex, luminance, over, resolve, validate };
@@ -0,0 +1,43 @@
1
+ # Deny everything by default.
2
+ **
3
+
4
+ !package.json
5
+ !bun.lock
6
+ !turbo.json
7
+ !bunfig.toml
8
+
9
+ !apps/web/package.json
10
+ !apps/api/package.json
11
+ # @if worker
12
+ !apps/worker/package.json
13
+ # @endif
14
+
15
+ !apps/web/next.config.ts
16
+ !apps/web/postcss.config.mjs
17
+ !apps/web/tsconfig.json
18
+ !apps/web/tsconfig.build.json
19
+ !apps/web/public/
20
+ !apps/web/messages/
21
+ !apps/web/src/
22
+
23
+ !apps/api/tsconfig.json
24
+ !apps/api/prisma.config.ts
25
+ !apps/api/src/
26
+ !apps/api/prisma/
27
+
28
+ # @if worker
29
+ !apps/worker/src/
30
+
31
+ # @endif
32
+ !packages/lint/package.json
33
+ !packages/lint/oxlint.ts
34
+ !packages/lint/oxfmt.ts
35
+
36
+ !packages/tsconfig/package.json
37
+ !packages/tsconfig/base.json
38
+ !packages/tsconfig/node.json
39
+ # @if worker
40
+
41
+ !packages/queues/package.json
42
+ !packages/queues/src/
43
+ # @endif
@@ -0,0 +1,10 @@
1
+ root = true
2
+
3
+ [*]
4
+ charset = utf-8
5
+ end_of_line = lf
6
+ indent_size = 2
7
+ indent_style = space
8
+ insert_final_newline = true
9
+ max_line_length = 80
10
+ trim_trailing_whitespace = true
@@ -0,0 +1,289 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ concurrency:
10
+ group: ${{ github.workflow }}-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ permissions:
14
+ contents: read
15
+
16
+ env:
17
+ TURBO_TELEMETRY_DISABLED: '1'
18
+
19
+ jobs:
20
+ # Decides which app(s) a push/PR touches so only the affected pipeline(s)
21
+ # run. Shared root files (lockfile, workspace/turbo config) affect all.
22
+ changes:
23
+ runs-on: ubuntu-latest
24
+ outputs:
25
+ web: ${{ steps.filter.outputs.web }}
26
+ api: ${{ steps.filter.outputs.api }}
27
+ # @if worker
28
+ worker: ${{ steps.filter.outputs.worker }}
29
+ # @endif
30
+ steps:
31
+ - uses: actions/checkout@v6
32
+ - uses: dorny/paths-filter@v3
33
+ id: filter
34
+ with:
35
+ filters: |
36
+ shared: &shared
37
+ - 'package.json'
38
+ - 'bun.lock'
39
+ - 'bunfig.toml'
40
+ - 'turbo.json'
41
+ - 'packages/**'
42
+ web:
43
+ - *shared
44
+ - 'apps/web/**'
45
+ api:
46
+ - *shared
47
+ - 'apps/api/**'
48
+ # @if worker
49
+ worker:
50
+ - *shared
51
+ - 'apps/worker/**'
52
+ # @endif
53
+
54
+ outdated:
55
+ runs-on: ubuntu-latest
56
+ continue-on-error: true
57
+ steps:
58
+ - uses: actions/checkout@v6
59
+ - uses: oven-sh/setup-bun@v2
60
+ with:
61
+ bun-version-file: package.json
62
+ - run: bun install --frozen-lockfile --ignore-scripts
63
+ - run: bun outdated -r
64
+
65
+ # ---------------------------------------------------------------- web ----
66
+ web:
67
+ needs: changes
68
+ if: needs.changes.outputs.web == 'true'
69
+ runs-on: ubuntu-latest
70
+ timeout-minutes: 30
71
+ steps:
72
+ - uses: actions/checkout@v6
73
+ - uses: oven-sh/setup-bun@v2
74
+ with:
75
+ bun-version-file: package.json
76
+ - run: bun install --frozen-lockfile --ignore-scripts
77
+
78
+ - uses: actions/cache@v4
79
+ with:
80
+ path: .turbo/cache
81
+ key: turbo-web-${{ github.sha }}
82
+ restore-keys: turbo-web-
83
+
84
+ - name: Typecheck
85
+ run: bunx turbo run lint:ts --filter=@nexst/web
86
+
87
+ - name: Lint
88
+ run: bunx turbo run lint fmt:check --filter=@nexst/web
89
+
90
+ - name: Build
91
+ run: bunx turbo run build --filter=@nexst/web
92
+
93
+ - name: Cache Playwright browsers
94
+ uses: actions/cache@v4
95
+ with:
96
+ path: ~/.cache/ms-playwright
97
+ key: ${{ runner.os }}-playwright-${{ hashFiles('bun.lock') }}
98
+
99
+ - name: Install Playwright browsers
100
+ run: cd apps/web && bunx playwright install --with-deps chromium
101
+
102
+ - name: Vitest (unit + Storybook interaction/a11y)
103
+ run: cd apps/web && bun run test:unit
104
+
105
+ - name: Playwright
106
+ run: cd apps/web && bun run test
107
+
108
+ - name: Upload Playwright report
109
+ if: failure()
110
+ uses: actions/upload-artifact@v4
111
+ with:
112
+ name: playwright-report
113
+ path: apps/web/playwright-report/
114
+ retention-days: 7
115
+
116
+ # ---------------------------------------------------------------- api ----
117
+ api:
118
+ needs: changes
119
+ if: needs.changes.outputs.api == 'true'
120
+ runs-on: ubuntu-latest
121
+ steps:
122
+ - uses: actions/checkout@v6
123
+ - uses: oven-sh/setup-bun@v2
124
+ with:
125
+ bun-version-file: package.json
126
+ - run: bun install --frozen-lockfile --ignore-scripts
127
+
128
+ - uses: actions/cache@v4
129
+ with:
130
+ path: .turbo/cache
131
+ key: turbo-api-${{ github.sha }}
132
+ restore-keys: turbo-api-
133
+
134
+ - name: Generate Prisma client
135
+ run: cd apps/api && bunx --bun prisma generate
136
+
137
+ - name: Typecheck
138
+ run: bunx turbo run lint:ts --filter=@nexst/api
139
+
140
+ - name: Lint
141
+ run: bunx turbo run lint fmt:check --filter=@nexst/api
142
+
143
+ - name: Codes-only error responses
144
+ run: cd apps/api && bun run lint:error-codes
145
+
146
+ - name: Unit tests
147
+ run: cd apps/api && bun run test:cov
148
+
149
+ - uses: actions/upload-artifact@v4
150
+ with:
151
+ name: api-coverage
152
+ path: apps/api/coverage/lcov.info
153
+ retention-days: 1
154
+
155
+ api-e2e:
156
+ needs: changes
157
+ if: needs.changes.outputs.api == 'true'
158
+ runs-on: ubuntu-latest
159
+ services:
160
+ postgres:
161
+ image: postgres:18-alpine
162
+ env:
163
+ POSTGRES_USER: nexst_user
164
+ POSTGRES_PASSWORD: test-pw
165
+ POSTGRES_DB: nexst_db
166
+ ports: ['5432:5432']
167
+ options: >-
168
+ --health-cmd "pg_isready -U nexst_user -d nexst_db"
169
+ --health-interval 5s --health-timeout 3s --health-retries 10
170
+ # @if worker
171
+ redis:
172
+ image: redis:8-alpine
173
+ ports: ['6379:6379']
174
+ options: >-
175
+ --health-cmd "redis-cli ping"
176
+ --health-interval 5s --health-timeout 3s --health-retries 10
177
+ # @endif
178
+ env:
179
+ NODE_ENV: test
180
+ DATABASE_URL: postgresql://nexst_user:test-pw@localhost:5432/nexst_db?schema=nexst_user
181
+ # @if auth
182
+ BETTER_AUTH_SECRET: ci-only-secret-ci-only-secret-ci-only
183
+ BETTER_AUTH_URL: http://localhost:3001
184
+ # @endif
185
+ # @if r2
186
+ R2_ACCESS_KEY_ID: ci
187
+ R2_SECRET_ACCESS_KEY: ci
188
+ R2_BUCKET: ci
189
+ R2_ENDPOINT: https://ci.r2.cloudflarestorage.com
190
+ # @endif
191
+ # @if worker
192
+ REDIS_HOST: localhost
193
+ # @endif
194
+ steps:
195
+ - uses: actions/checkout@v6
196
+ - uses: oven-sh/setup-bun@v2
197
+ with:
198
+ bun-version-file: package.json
199
+ - run: bun install --frozen-lockfile --ignore-scripts
200
+
201
+ - name: Prepare database
202
+ run: |
203
+ cd apps/api
204
+ bunx --bun prisma generate
205
+ bunx --bun prisma migrate deploy
206
+
207
+ - name: Start API
208
+ run: |
209
+ cd apps/api
210
+ PRISMA_AUTO_MIGRATE=false bun src/main.ts > api.log 2>&1 &
211
+ for i in $(seq 1 30); do
212
+ curl -sf http://localhost:3001/health && exit 0
213
+ sleep 2
214
+ done
215
+ cat api.log
216
+ exit 1
217
+
218
+ - name: E2E tests
219
+ run: cd apps/api && bun run test:e2e
220
+
221
+ - name: API log
222
+ if: failure()
223
+ run: cat apps/api/api.log
224
+ # @if worker
225
+
226
+ # ------------------------------------------------------------- worker ----
227
+ worker:
228
+ needs: changes
229
+ if: needs.changes.outputs.worker == 'true'
230
+ runs-on: ubuntu-latest
231
+ steps:
232
+ - uses: actions/checkout@v6
233
+ - uses: oven-sh/setup-bun@v2
234
+ with:
235
+ bun-version-file: package.json
236
+ - run: bun install --frozen-lockfile --ignore-scripts
237
+
238
+ - uses: actions/cache@v4
239
+ with:
240
+ path: .turbo/cache
241
+ key: turbo-worker-${{ github.sha }}
242
+ restore-keys: turbo-worker-
243
+
244
+ - name: Typecheck
245
+ run: bunx turbo run lint:ts --filter=@nexst/worker --filter=@nexst/queues
246
+
247
+ - name: Lint
248
+ run: bunx turbo run lint fmt:check --filter=@nexst/worker --filter=@nexst/queues
249
+
250
+ - name: Unit tests
251
+ run: cd apps/worker && bun run test:unit
252
+ # @endif
253
+
254
+ # -------------------------------------------------------------- sonar ----
255
+ # Skipped until the SONAR_TOKEN secret exists.
256
+ sonar:
257
+ needs: [changes, web, api]
258
+ if: >-
259
+ always() && !cancelled() && !contains(needs.*.result, 'failure')
260
+ runs-on: ubuntu-latest
261
+ env:
262
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
263
+ strategy:
264
+ fail-fast: false
265
+ matrix:
266
+ # @if worker
267
+ app: [web, api, worker]
268
+ # @endif
269
+ # @if !worker
270
+ # app: [web, api]
271
+ # @endif
272
+ steps:
273
+ - uses: actions/checkout@v6
274
+ if: env.SONAR_TOKEN != '' && needs.changes.outputs[matrix.app] == 'true'
275
+ with:
276
+ fetch-depth: 0
277
+ - name: Download coverage
278
+ if: env.SONAR_TOKEN != '' && needs.changes.outputs[matrix.app] == 'true' && matrix.app == 'api'
279
+ uses: actions/download-artifact@v4
280
+ with:
281
+ name: api-coverage
282
+ path: apps/api/coverage
283
+ - name: SonarQube Scan
284
+ if: env.SONAR_TOKEN != '' && needs.changes.outputs[matrix.app] == 'true'
285
+ uses: SonarSource/sonarqube-scan-action@v8
286
+ with:
287
+ projectBaseDir: apps/${{ matrix.app }}
288
+ env:
289
+ SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
@@ -0,0 +1,66 @@
1
+ name: Publish App Image
2
+
3
+ on:
4
+ workflow_call:
5
+ inputs:
6
+ app:
7
+ # @if worker
8
+ description: Bake target (web, api, api-migrate, worker) — also the image suffix
9
+ # @endif
10
+ # @if !worker
11
+ # description: Bake target (web, api, api-migrate) — also the image suffix
12
+ # @endif
13
+ required: true
14
+ type: string
15
+
16
+ jobs:
17
+ docker:
18
+ # Images target linux/arm64 only: build natively, no QEMU.
19
+ runs-on: ubuntu-24.04-arm
20
+ steps:
21
+ - uses: actions/checkout@v6
22
+
23
+ - name: Image name
24
+ id: image
25
+ run: echo "name=ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/nexst-${{ inputs.app }}" >> "$GITHUB_OUTPUT"
26
+
27
+ - name: Log in to GitHub Container Registry
28
+ uses: docker/login-action@v4
29
+ with:
30
+ registry: ghcr.io
31
+ username: ${{ github.actor }}
32
+ password: ${{ secrets.GITHUB_TOKEN }}
33
+
34
+ - name: Log in to dhi.io
35
+ uses: docker/login-action@v4
36
+ with:
37
+ registry: dhi.io
38
+ username: ${{ secrets.DHI_REGISTRY_USERNAME }}
39
+ password: ${{ secrets.DHI_REGISTRY_PASSWORD }}
40
+
41
+ - name: Extract metadata
42
+ id: meta
43
+ uses: docker/metadata-action@v6
44
+ with:
45
+ images: ${{ steps.image.outputs.name }}
46
+ tags: |
47
+ type=sha,prefix=sha-,format=short
48
+
49
+ - name: Set up Docker Buildx
50
+ uses: docker/setup-buildx-action@v4
51
+
52
+ - name: Build and push
53
+ uses: docker/bake-action@v7
54
+ with:
55
+ files: |
56
+ ./docker-bake.hcl
57
+ cwd://${{ steps.meta.outputs.bake-file }}
58
+ targets: ${{ inputs.app }}
59
+ push: true
60
+ set: |
61
+ ${{ inputs.app }}.cache-from=type=registry,ref=${{ steps.image.outputs.name }}:buildcache
62
+ ${{ inputs.app }}.cache-to=type=registry,ref=${{ steps.image.outputs.name }}:buildcache,mode=max,compression=zstd,ignore-error=true
63
+ env:
64
+ NEXT_PUBLIC_HOST_URL: ${{ vars.NEXT_PUBLIC_HOST_URL }}
65
+ NEXT_PUBLIC_API_URL: ${{ vars.NEXT_PUBLIC_API_URL }}
66
+ NEXT_PUBLIC_GOOGLE_CLIENT_ID: ${{ vars.NEXT_PUBLIC_GOOGLE_CLIENT_ID }} # @if auth