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.
- package/dist/index.js +1727 -0
- package/package.json +37 -0
- package/template/.agents/plugins/marketplace.json +21 -0
- package/template/.claude/settings.json +6 -0
- package/template/.claude/skills/frontend-design/LICENSE.txt +177 -0
- package/template/.claude/skills/frontend-design/SKILL.md +154 -0
- package/template/.claude/skills/theme-factory/SKILL.md +95 -0
- package/template/.claude/skills/theme-factory/references/schema.md +104 -0
- package/template/.claude/skills/theme-factory/scripts/theme.mjs +261 -0
- package/template/.dockerignore +43 -0
- package/template/.editorconfig +10 -0
- package/template/.github/workflows/ci.yaml +289 -0
- package/template/.github/workflows/publish-app.yaml +66 -0
- package/template/.github/workflows/publish.yaml +148 -0
- package/template/.github/workflows/snyk.yaml +34 -0
- package/template/.vscode/settings.json +8 -0
- package/template/AGENTS.md +42 -0
- package/template/Dockerfile.base +9 -0
- package/template/README.md +54 -0
- package/template/_gitignore +10 -0
- package/template/apps/api/.env.example +32 -0
- package/template/apps/api/AGENTS.md +44 -0
- package/template/apps/api/Dockerfile +69 -0
- package/template/apps/api/_gitignore +8 -0
- package/template/apps/api/knip.config.ts +22 -0
- package/template/apps/api/kustomize/base/application.properties +16 -0
- package/template/apps/api/kustomize/base/deployment.yaml +94 -0
- package/template/apps/api/kustomize/base/kustomization.yaml +17 -0
- package/template/apps/api/kustomize/base/service.yaml +10 -0
- package/template/apps/api/kustomize/overlays/prod/kustomization.yaml +17 -0
- package/template/apps/api/kustomize/overlays/staging/application.properties +1 -0
- package/template/apps/api/kustomize/overlays/staging/kustomization.yaml +21 -0
- package/template/apps/api/kustomize/secret.keys +14 -0
- package/template/apps/api/oxfmt.config.ts +8 -0
- package/template/apps/api/oxlint.config.ts +107 -0
- package/template/apps/api/package.json +79 -0
- package/template/apps/api/prisma/migrations/20260927000000_init/migration.sql +113 -0
- package/template/apps/api/prisma/migrations/migration_lock.toml +3 -0
- package/template/apps/api/prisma/schema.prisma +113 -0
- package/template/apps/api/prisma.config.ts +7 -0
- package/template/apps/api/scripts/auth.cli.ts +19 -0
- package/template/apps/api/scripts/seal-secret.sh +171 -0
- package/template/apps/api/sonar-project.properties +9 -0
- package/template/apps/api/src/app-info/app-info.dto.ts +10 -0
- package/template/apps/api/src/app-info/app-info.module.ts +7 -0
- package/template/apps/api/src/app-info/app-info.resolver.spec.ts +14 -0
- package/template/apps/api/src/app-info/app-info.resolver.ts +21 -0
- package/template/apps/api/src/app.module.spec.ts +93 -0
- package/template/apps/api/src/app.module.ts +77 -0
- package/template/apps/api/src/auth/auth.guard.spec.ts +90 -0
- package/template/apps/api/src/auth/auth.guard.ts +97 -0
- package/template/apps/api/src/auth/auth.ts +129 -0
- package/template/apps/api/src/auth/decorator/allow-anonymous.decorator.ts +17 -0
- package/template/apps/api/src/auth/decorator/is-email-verified.decorator.spec.ts +41 -0
- package/template/apps/api/src/auth/decorator/is-email-verified.decorator.ts +20 -0
- package/template/apps/api/src/auth/decorator/roles.decorator.ts +27 -0
- package/template/apps/api/src/auth/testing/make-session.ts +31 -0
- package/template/apps/api/src/auth/types/auth.types.ts +27 -0
- package/template/apps/api/src/bootstrap/register-graphql-enums.ts +12 -0
- package/template/apps/api/src/bootstrap/security.ts +24 -0
- package/template/apps/api/src/bootstrap/swagger.ts +28 -0
- package/template/apps/api/src/config/app.config.spec.ts +22 -0
- package/template/apps/api/src/config/app.config.ts +122 -0
- package/template/apps/api/src/config/graphql-depth-limit.ts +103 -0
- package/template/apps/api/src/config/graphql.config.ts +77 -0
- package/template/apps/api/src/config/resources/app.yaml +29 -0
- package/template/apps/api/src/errors/app-parse-uuid.pipe.ts +27 -0
- package/template/apps/api/src/errors/app.exception.ts +47 -0
- package/template/apps/api/src/errors/error-code.enum.ts +54 -0
- package/template/apps/api/src/errors/http-status-fallback-code.util.ts +27 -0
- package/template/apps/api/src/errors/validation-exception.factory.ts +43 -0
- package/template/apps/api/src/health/health.controller.spec.ts +82 -0
- package/template/apps/api/src/health/health.controller.ts +57 -0
- package/template/apps/api/src/health/health.module.ts +12 -0
- package/template/apps/api/src/logging/context-logger.ts +18 -0
- package/template/apps/api/src/logging/logger.config.ts +23 -0
- package/template/apps/api/src/logging/logging.module.ts +22 -0
- package/template/apps/api/src/mail/mail.module.ts +12 -0
- package/template/apps/api/src/mail/mail.service.spec.ts +47 -0
- package/template/apps/api/src/mail/mail.service.ts +123 -0
- package/template/apps/api/src/main.ts +41 -0
- package/template/apps/api/src/prisma/prisma.module.ts +12 -0
- package/template/apps/api/src/prisma/prisma.service.spec.ts +136 -0
- package/template/apps/api/src/prisma/prisma.service.ts +138 -0
- package/template/apps/api/src/queue/dto/enqueue-example.input.ts +11 -0
- package/template/apps/api/src/queue/example-queue.resolver.ts +14 -0
- package/template/apps/api/src/queue/example-queue.service.spec.ts +21 -0
- package/template/apps/api/src/queue/example-queue.service.ts +32 -0
- package/template/apps/api/src/queue/queue.module.ts +27 -0
- package/template/apps/api/src/r2/r2.module.ts +12 -0
- package/template/apps/api/src/r2/r2.service.spec.ts +117 -0
- package/template/apps/api/src/r2/r2.service.ts +131 -0
- package/template/apps/api/src/user/dto/user.dto.ts +26 -0
- package/template/apps/api/src/user/user.module.ts +10 -0
- package/template/apps/api/src/user/user.resolver.spec.ts +18 -0
- package/template/apps/api/src/user/user.resolver.ts +24 -0
- package/template/apps/api/src/utils/catch-everything-exception.filter.spec.ts +292 -0
- package/template/apps/api/src/utils/catch-everything-exception.filter.ts +70 -0
- package/template/apps/api/src/utils/error-handler.util.spec.ts +20 -0
- package/template/apps/api/src/utils/error-handler.util.ts +12 -0
- package/template/apps/api/src/utils/escape-like-pattern.util.spec.ts +29 -0
- package/template/apps/api/src/utils/escape-like-pattern.util.ts +11 -0
- package/template/apps/api/src/utils/pagination.input.ts +20 -0
- package/template/apps/api/src/utils/prisma-exception.filter.spec.ts +186 -0
- package/template/apps/api/src/utils/prisma-exception.filter.ts +45 -0
- package/template/apps/api/src/utils/prisma-graphql-exception.filter.spec.ts +216 -0
- package/template/apps/api/src/utils/prisma-graphql-exception.filter.ts +43 -0
- package/template/apps/api/tests/app.e2e-spec.ts +35 -0
- package/template/apps/api/tests/auth.e2e-spec.ts +38 -0
- package/template/apps/api/tests/helpers/api.ts +27 -0
- package/template/apps/api/tsconfig.json +11 -0
- package/template/apps/api/vitest.config.e2e.ts +16 -0
- package/template/apps/api/vitest.config.ts +40 -0
- package/template/apps/web/.env.example +7 -0
- package/template/apps/web/.storybook/decorators.tsx +38 -0
- package/template/apps/web/.storybook/main.ts +17 -0
- package/template/apps/web/.storybook/preview.ts +38 -0
- package/template/apps/web/.storybook/vitest.setup.ts +5 -0
- package/template/apps/web/AGENTS.md +29 -0
- package/template/apps/web/Dockerfile +49 -0
- package/template/apps/web/_gitignore +16 -0
- package/template/apps/web/knip.config.ts +15 -0
- package/template/apps/web/kustomize/base/application.properties +5 -0
- package/template/apps/web/kustomize/base/deployment.yaml +60 -0
- package/template/apps/web/kustomize/base/kustomization.yaml +17 -0
- package/template/apps/web/kustomize/base/service.yaml +10 -0
- package/template/apps/web/kustomize/overlays/prod/kustomization.yaml +12 -0
- package/template/apps/web/kustomize/overlays/staging/application.properties +2 -0
- package/template/apps/web/kustomize/overlays/staging/kustomization.yaml +18 -0
- package/template/apps/web/messages/de.json +124 -0
- package/template/apps/web/messages/en.json +124 -0
- package/template/apps/web/next-env.d.ts +7 -0
- package/template/apps/web/next.config.ts +39 -0
- package/template/apps/web/oxfmt.config.ts +9 -0
- package/template/apps/web/oxlint.config.ts +36 -0
- package/template/apps/web/package.json +80 -0
- package/template/apps/web/playwright.config.ts +34 -0
- package/template/apps/web/postcss.config.mjs +7 -0
- package/template/apps/web/sonar-project.properties +9 -0
- package/template/apps/web/src/app/[locale]/(auth)/recovery/page.tsx +22 -0
- package/template/apps/web/src/app/[locale]/(auth)/reset-password/page.tsx +29 -0
- package/template/apps/web/src/app/[locale]/(auth)/sign-in/page.tsx +24 -0
- package/template/apps/web/src/app/[locale]/(auth)/sign-up/page.tsx +24 -0
- package/template/apps/web/src/app/[locale]/(auth)/verify/page.tsx +24 -0
- package/template/apps/web/src/app/[locale]/dashboard/page.tsx +34 -0
- package/template/apps/web/src/app/[locale]/layout.tsx +60 -0
- package/template/apps/web/src/app/[locale]/page.tsx +23 -0
- package/template/apps/web/src/app/api/health/route.ts +3 -0
- package/template/apps/web/src/app/global-error.tsx +19 -0
- package/template/apps/web/src/app/globals.css +98 -0
- package/template/apps/web/src/app/icon.svg +1 -0
- package/template/apps/web/src/app/robots.ts +15 -0
- package/template/apps/web/src/app/sitemap.ts +24 -0
- package/template/apps/web/src/components/alert/alert.stories.tsx +25 -0
- package/template/apps/web/src/components/alert/alert.tsx +50 -0
- package/template/apps/web/src/components/avatar/avatar.stories.tsx +116 -0
- package/template/apps/web/src/components/avatar/avatar.tsx +88 -0
- package/template/apps/web/src/components/button/button-variants.ts +28 -0
- package/template/apps/web/src/components/button/button.stories.tsx +33 -0
- package/template/apps/web/src/components/button/button.tsx +32 -0
- package/template/apps/web/src/components/checkbox/checkbox.stories.tsx +312 -0
- package/template/apps/web/src/components/checkbox/checkbox.tsx +38 -0
- package/template/apps/web/src/components/dialog/danger-panel.stories.tsx +41 -0
- package/template/apps/web/src/components/dialog/danger-panel.tsx +32 -0
- package/template/apps/web/src/components/dialog/dialog.stories.tsx +239 -0
- package/template/apps/web/src/components/dialog/dialog.tsx +136 -0
- package/template/apps/web/src/components/dialog-buttons.stories.tsx +55 -0
- package/template/apps/web/src/components/dialog-buttons.tsx +48 -0
- package/template/apps/web/src/components/drawer/drawer.stories.tsx +251 -0
- package/template/apps/web/src/components/drawer/drawer.tsx +120 -0
- package/template/apps/web/src/components/dropdown-menu/dropdown-menu.stories.tsx +329 -0
- package/template/apps/web/src/components/dropdown-menu/dropdown-menu.tsx +263 -0
- package/template/apps/web/src/components/empty-state.stories.tsx +49 -0
- package/template/apps/web/src/components/empty-state.tsx +33 -0
- package/template/apps/web/src/components/field-group.stories.tsx +47 -0
- package/template/apps/web/src/components/field-group.tsx +20 -0
- package/template/apps/web/src/components/field-label.stories.tsx +34 -0
- package/template/apps/web/src/components/field-label.tsx +28 -0
- package/template/apps/web/src/components/icons.tsx +25 -0
- package/template/apps/web/src/components/input/input-otp.stories.tsx +86 -0
- package/template/apps/web/src/components/input/input-otp.tsx +184 -0
- package/template/apps/web/src/components/input/input.stories.tsx +35 -0
- package/template/apps/web/src/components/input/input.tsx +180 -0
- package/template/apps/web/src/components/popover/popover.stories.tsx +479 -0
- package/template/apps/web/src/components/popover/popover.tsx +45 -0
- package/template/apps/web/src/components/responsive-dialog/responsive-dialog.stories.tsx +307 -0
- package/template/apps/web/src/components/responsive-dialog/responsive-dialog.tsx +96 -0
- package/template/apps/web/src/components/searchbar/searchbar.stories.tsx +59 -0
- package/template/apps/web/src/components/searchbar/searchbar.tsx +64 -0
- package/template/apps/web/src/components/select/select.stories.tsx +293 -0
- package/template/apps/web/src/components/select/select.tsx +445 -0
- package/template/apps/web/src/components/skeleton/skeleton.stories.tsx +76 -0
- package/template/apps/web/src/components/skeleton/skeleton.tsx +13 -0
- package/template/apps/web/src/components/spinner/spinner.tsx +18 -0
- package/template/apps/web/src/components/switch/switch.stories.tsx +71 -0
- package/template/apps/web/src/components/switch/switch.tsx +35 -0
- package/template/apps/web/src/components/tabs/tabs.stories.tsx +209 -0
- package/template/apps/web/src/components/tabs/tabs.tsx +69 -0
- package/template/apps/web/src/components/tabs/url-tabs.stories.tsx +68 -0
- package/template/apps/web/src/components/tabs/url-tabs.tsx +42 -0
- package/template/apps/web/src/components/textarea/textarea.stories.tsx +144 -0
- package/template/apps/web/src/components/textarea/textarea.tsx +115 -0
- package/template/apps/web/src/components/toast/toaster.tsx +38 -0
- package/template/apps/web/src/components/tooltip/tooltip.stories.tsx +237 -0
- package/template/apps/web/src/components/tooltip/tooltip.tsx +49 -0
- package/template/apps/web/src/config/csp.ts +59 -0
- package/template/apps/web/src/config/routes.ts +14 -0
- package/template/apps/web/src/config/site.ts +5 -0
- package/template/apps/web/src/env.ts +15 -0
- package/template/apps/web/src/features/auth/auth-card.tsx +20 -0
- package/template/apps/web/src/features/auth/auth-divider.tsx +14 -0
- package/template/apps/web/src/features/auth/google-auth-button.tsx +49 -0
- package/template/apps/web/src/features/auth/google-one-tap.tsx +23 -0
- package/template/apps/web/src/features/auth/lib/auth-error.test.ts +22 -0
- package/template/apps/web/src/features/auth/lib/auth-error.ts +16 -0
- package/template/apps/web/src/features/auth/lib/return-to.test.ts +41 -0
- package/template/apps/web/src/features/auth/lib/return-to.ts +19 -0
- package/template/apps/web/src/features/auth/lib/validator.test.ts +45 -0
- package/template/apps/web/src/features/auth/lib/validator.ts +49 -0
- package/template/apps/web/src/features/auth/passkey-settings.tsx +106 -0
- package/template/apps/web/src/features/auth/recovery-form.tsx +61 -0
- package/template/apps/web/src/features/auth/resend-verification-button.tsx +36 -0
- package/template/apps/web/src/features/auth/reset-password-form.tsx +62 -0
- package/template/apps/web/src/features/auth/sign-in-form.stories.tsx +22 -0
- package/template/apps/web/src/features/auth/sign-in-form.tsx +116 -0
- package/template/apps/web/src/features/auth/sign-out-button.tsx +26 -0
- package/template/apps/web/src/features/auth/sign-up-form.tsx +107 -0
- package/template/apps/web/src/features/shared/provider.tsx +16 -0
- package/template/apps/web/src/features/shared/site-header.tsx +48 -0
- package/template/apps/web/src/features/shared/theme-toggle.tsx +24 -0
- package/template/apps/web/src/i18n/navigation.ts +5 -0
- package/template/apps/web/src/i18n/request.ts +21 -0
- package/template/apps/web/src/i18n/routing.ts +6 -0
- package/template/apps/web/src/lib/auth-client.ts +19 -0
- package/template/apps/web/src/lib/auth-server.ts +46 -0
- package/template/apps/web/src/lib/auth.ts +8 -0
- package/template/apps/web/src/lib/codegen.ts +30 -0
- package/template/apps/web/src/lib/graphql-client.ts +29 -0
- package/template/apps/web/src/lib/graphql-server.ts +28 -0
- package/template/apps/web/src/lib/graphql-utils.test.ts +32 -0
- package/template/apps/web/src/lib/graphql-utils.ts +76 -0
- package/template/apps/web/src/lib/hooks/use-media-query.ts +21 -0
- package/template/apps/web/src/lib/metadata.ts +17 -0
- package/template/apps/web/src/lib/network-error.test.ts +30 -0
- package/template/apps/web/src/lib/network-error.ts +17 -0
- package/template/apps/web/src/lib/proxy-utils.ts +42 -0
- package/template/apps/web/src/lib/query-client.ts +29 -0
- package/template/apps/web/src/lib/utils.ts +5 -0
- package/template/apps/web/src/proxy.ts +57 -0
- package/template/apps/web/test-results/.last-run.json +4 -0
- package/template/apps/web/tests/auth.spec.ts +17 -0
- package/template/apps/web/tests/csp-theme-script.spec.ts +37 -0
- package/template/apps/web/tests/home.spec.ts +25 -0
- package/template/apps/web/tsconfig.build.json +4 -0
- package/template/apps/web/tsconfig.json +28 -0
- package/template/apps/web/vitest.config.ts +40 -0
- package/template/apps/web/vitest.shims.d.ts +1 -0
- package/template/apps/worker/.env.example +4 -0
- package/template/apps/worker/AGENTS.md +11 -0
- package/template/apps/worker/Dockerfile +35 -0
- package/template/apps/worker/knip.config.ts +3 -0
- package/template/apps/worker/kustomize/base/application.properties +4 -0
- package/template/apps/worker/kustomize/base/deployment.yaml +62 -0
- package/template/apps/worker/kustomize/base/kustomization.yaml +16 -0
- package/template/apps/worker/kustomize/overlays/prod/kustomization.yaml +12 -0
- package/template/apps/worker/kustomize/overlays/staging/application.properties +1 -0
- package/template/apps/worker/kustomize/overlays/staging/kustomization.yaml +18 -0
- package/template/apps/worker/oxfmt.config.ts +8 -0
- package/template/apps/worker/oxlint.config.ts +15 -0
- package/template/apps/worker/package.json +33 -0
- package/template/apps/worker/sonar-project.properties +8 -0
- package/template/apps/worker/src/env.ts +13 -0
- package/template/apps/worker/src/lib/graceful-shutdown.ts +27 -0
- package/template/apps/worker/src/lib/logger.ts +20 -0
- package/template/apps/worker/src/main.ts +17 -0
- package/template/apps/worker/src/processors/example-processor.ts +15 -0
- package/template/apps/worker/src/worker/redis.ts +19 -0
- package/template/apps/worker/src/worker/worker.ts +37 -0
- package/template/apps/worker/tests/example-processor.test.ts +13 -0
- package/template/apps/worker/tests/graceful-shutdown.test.ts +34 -0
- package/template/apps/worker/tsconfig.json +4 -0
- package/template/apps/worker/vitest.config.ts +13 -0
- package/template/bun.lock +3053 -0
- package/template/bunfig.toml +2 -0
- package/template/compose.yaml +53 -0
- package/template/docker-bake.hcl +86 -0
- package/template/package.json +47 -0
- package/template/packages/lint/knip.ts +40 -0
- package/template/packages/lint/oxfmt.ts +8 -0
- package/template/packages/lint/oxlint.ts +136 -0
- package/template/packages/lint/package.json +13 -0
- package/template/packages/queues/oxfmt.config.ts +8 -0
- package/template/packages/queues/oxlint.config.ts +7 -0
- package/template/packages/queues/package.json +15 -0
- package/template/packages/queues/src/example-job.ts +11 -0
- package/template/packages/queues/src/index.ts +3 -0
- package/template/packages/queues/src/queue-names.ts +2 -0
- package/template/packages/queues/src/redis-connection.ts +27 -0
- package/template/packages/queues/tsconfig.json +4 -0
- package/template/packages/tsconfig/base.json +23 -0
- package/template/packages/tsconfig/node.json +11 -0
- package/template/packages/tsconfig/package.json +4 -0
- package/template/turbo.json +50 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { storybookTest } from "@storybook/addon-vitest/vitest-plugin";
|
|
3
|
+
import { playwright } from "@vitest/browser-playwright";
|
|
4
|
+
import { defineConfig } from "vitest/config";
|
|
5
|
+
|
|
6
|
+
const dirname = import.meta.dirname;
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
resolve: {
|
|
10
|
+
alias: {
|
|
11
|
+
"@": path.join(dirname, "src"),
|
|
12
|
+
"~sb": path.join(dirname, ".storybook"),
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
test: {
|
|
16
|
+
projects: [
|
|
17
|
+
{
|
|
18
|
+
extends: true,
|
|
19
|
+
test: {
|
|
20
|
+
include: ["src/**/*.test.ts"],
|
|
21
|
+
name: "unit",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
extends: true,
|
|
26
|
+
plugins: [storybookTest({ configDir: path.join(dirname, ".storybook") })],
|
|
27
|
+
test: {
|
|
28
|
+
browser: {
|
|
29
|
+
enabled: true,
|
|
30
|
+
headless: true,
|
|
31
|
+
instances: [{ browser: "chromium", viewport: { height: 900, width: 1440 } }],
|
|
32
|
+
provider: playwright({}),
|
|
33
|
+
},
|
|
34
|
+
name: "storybook",
|
|
35
|
+
setupFiles: [".storybook/vitest.setup.ts"],
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="@vitest/browser/providers/playwright" />
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# worker (BullMQ)
|
|
2
|
+
|
|
3
|
+
Plain bun process (no NestJS) that consumes the queues apps/api enqueues. Queue names,
|
|
4
|
+
payload types and Redis connection options live in `packages/queues` — change them
|
|
5
|
+
there so producer and consumer stay in sync.
|
|
6
|
+
|
|
7
|
+
- Processors in `src/processors/` are pure functions of the job; unit-test them
|
|
8
|
+
without Redis (`bun run test:unit`).
|
|
9
|
+
- `src/worker/worker.ts` wires processors to BullMQ; shutdown closes the worker before
|
|
10
|
+
quitting Redis (`src/lib/graceful-shutdown.ts`).
|
|
11
|
+
- Deploy the worker before an API change that enqueues a new job shape.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
ARG BUN_VERSION
|
|
2
|
+
|
|
3
|
+
# ---------------------------------------------------
|
|
4
|
+
# Stage 1: Pruner
|
|
5
|
+
# ---------------------------------------------------
|
|
6
|
+
FROM toolchain AS pruner
|
|
7
|
+
RUN turbo prune @nexst/worker --docker
|
|
8
|
+
|
|
9
|
+
# ---------------------------------------------------
|
|
10
|
+
# Stage 2: Deps
|
|
11
|
+
# ---------------------------------------------------
|
|
12
|
+
FROM dhi.io/bun:${BUN_VERSION}-alpine-dev AS deps
|
|
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 --production --omit=peer
|
|
17
|
+
|
|
18
|
+
# ---------------------------------------------------
|
|
19
|
+
# Stage 3: Runtime
|
|
20
|
+
# ---------------------------------------------------
|
|
21
|
+
FROM dhi.io/bun:${BUN_VERSION}-alpine AS runtime
|
|
22
|
+
WORKDIR /app
|
|
23
|
+
ENV NODE_ENV=production
|
|
24
|
+
|
|
25
|
+
COPY --from=deps --chown=nonroot:nonroot /app/node_modules ./node_modules
|
|
26
|
+
COPY --from=deps --chown=nonroot:nonroot /app/apps/worker/node_modules ./apps/worker/node_modules
|
|
27
|
+
COPY --from=pruner --chown=nonroot:nonroot /app/out/full/packages/queues ./packages/queues
|
|
28
|
+
COPY --from=pruner --chown=nonroot:nonroot /app/out/full/apps/worker ./apps/worker
|
|
29
|
+
|
|
30
|
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \
|
|
31
|
+
CMD ["bun", "-e", "new (require('ioredis').default)({ host: process.env.REDIS_HOST || 'localhost', port: Number(process.env.REDIS_PORT) || 6379 }).ping().then(() => process.exit(0), () => process.exit(1))"]
|
|
32
|
+
|
|
33
|
+
STOPSIGNAL SIGTERM
|
|
34
|
+
WORKDIR /app/apps/worker
|
|
35
|
+
CMD ["bun", "src/main.ts"]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
apiVersion: apps/v1
|
|
2
|
+
kind: Deployment
|
|
3
|
+
metadata:
|
|
4
|
+
name: nexst-worker
|
|
5
|
+
spec:
|
|
6
|
+
replicas: 1
|
|
7
|
+
selector:
|
|
8
|
+
matchLabels: {}
|
|
9
|
+
template:
|
|
10
|
+
spec:
|
|
11
|
+
serviceAccountName: ghcr-puller
|
|
12
|
+
securityContext:
|
|
13
|
+
runAsNonRoot: true
|
|
14
|
+
runAsUser: 10001
|
|
15
|
+
runAsGroup: 10001
|
|
16
|
+
fsGroup: 10001
|
|
17
|
+
seccompProfile:
|
|
18
|
+
type: RuntimeDefault
|
|
19
|
+
containers:
|
|
20
|
+
- name: nexst-worker
|
|
21
|
+
image: nexst-worker
|
|
22
|
+
imagePullPolicy: IfNotPresent
|
|
23
|
+
securityContext:
|
|
24
|
+
allowPrivilegeEscalation: false
|
|
25
|
+
runAsNonRoot: true
|
|
26
|
+
runAsUser: 10001
|
|
27
|
+
runAsGroup: 10001
|
|
28
|
+
readOnlyRootFilesystem: true
|
|
29
|
+
capabilities:
|
|
30
|
+
drop: [ALL]
|
|
31
|
+
seccompProfile:
|
|
32
|
+
type: RuntimeDefault
|
|
33
|
+
envFrom:
|
|
34
|
+
- configMapRef:
|
|
35
|
+
name: nexst-worker-config
|
|
36
|
+
resources:
|
|
37
|
+
requests:
|
|
38
|
+
memory: 128Mi
|
|
39
|
+
cpu: 50m
|
|
40
|
+
limits:
|
|
41
|
+
memory: 512Mi
|
|
42
|
+
cpu: 500m
|
|
43
|
+
volumeMounts:
|
|
44
|
+
- name: tmp
|
|
45
|
+
mountPath: /tmp
|
|
46
|
+
readinessProbe:
|
|
47
|
+
exec:
|
|
48
|
+
command: &redis-ping
|
|
49
|
+
- bun
|
|
50
|
+
- -e
|
|
51
|
+
- "new (require('ioredis').default)({host:process.env.REDIS_HOST,port:Number(process.env.REDIS_PORT)}).ping().then(()=>process.exit(0),()=>process.exit(1))"
|
|
52
|
+
initialDelaySeconds: 10
|
|
53
|
+
periodSeconds: 30
|
|
54
|
+
livenessProbe:
|
|
55
|
+
exec:
|
|
56
|
+
command: *redis-ping
|
|
57
|
+
initialDelaySeconds: 20
|
|
58
|
+
periodSeconds: 30
|
|
59
|
+
volumes:
|
|
60
|
+
- name: tmp
|
|
61
|
+
emptyDir:
|
|
62
|
+
sizeLimit: 256Mi
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
labels:
|
|
2
|
+
- pairs:
|
|
3
|
+
app.kubernetes.io/name: nexst-worker
|
|
4
|
+
app.kubernetes.io/part-of: nexst
|
|
5
|
+
app.kubernetes.io/managed-by: kustomize
|
|
6
|
+
app.kubernetes.io/created-by: your-org
|
|
7
|
+
app.kubernetes.io/component: worker
|
|
8
|
+
includeSelectors: true
|
|
9
|
+
|
|
10
|
+
resources:
|
|
11
|
+
- deployment.yaml
|
|
12
|
+
|
|
13
|
+
configMapGenerator:
|
|
14
|
+
- name: nexst-worker-config
|
|
15
|
+
envs:
|
|
16
|
+
- application.properties
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Staging overrides for base/application.properties.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
apiVersion: kustomize.config.k8s.io/v1beta1
|
|
2
|
+
kind: Kustomization
|
|
3
|
+
|
|
4
|
+
resources:
|
|
5
|
+
- ../../base
|
|
6
|
+
|
|
7
|
+
namespace: nexst-staging
|
|
8
|
+
|
|
9
|
+
images:
|
|
10
|
+
- name: nexst-worker
|
|
11
|
+
newName: ghcr.io/your-org/nexst-worker
|
|
12
|
+
newTag: test
|
|
13
|
+
|
|
14
|
+
configMapGenerator:
|
|
15
|
+
- name: nexst-worker-config
|
|
16
|
+
behavior: merge
|
|
17
|
+
envs:
|
|
18
|
+
- application.properties
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import base, { baseIgnorePatterns } from "@nexst/lint/oxlint";
|
|
2
|
+
import { defineConfig } from "oxlint";
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
extends: [base],
|
|
6
|
+
ignorePatterns: baseIgnorePatterns,
|
|
7
|
+
rules: {
|
|
8
|
+
"typescript/no-floating-promises": "error",
|
|
9
|
+
"typescript/no-misused-promises": "error",
|
|
10
|
+
"typescript/no-unnecessary-type-assertion": "warn",
|
|
11
|
+
"typescript/switch-exhaustiveness-check": "warn",
|
|
12
|
+
|
|
13
|
+
"vitest/require-mock-type-parameters": "off",
|
|
14
|
+
},
|
|
15
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nexst/worker",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"imports": {
|
|
7
|
+
"#src/*": "./src/*",
|
|
8
|
+
"#tests/*": "./tests/*"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"dev": "bun --watch src/main.ts",
|
|
12
|
+
"lint": "oxlint",
|
|
13
|
+
"lint:fix": "oxlint --fix",
|
|
14
|
+
"lint:ts": "tsc --noEmit",
|
|
15
|
+
"fmt": "oxfmt",
|
|
16
|
+
"fmt:check": "oxfmt --check",
|
|
17
|
+
"unused": "knip --no-config-hints",
|
|
18
|
+
"test:unit": "vitest run",
|
|
19
|
+
"test:cov": "vitest run --coverage"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@nexst/queues": "workspace:*",
|
|
23
|
+
"@t3-oss/env-core": "^0.13.11",
|
|
24
|
+
"bullmq": "^6.3.9",
|
|
25
|
+
"ioredis": "6.0.0",
|
|
26
|
+
"pino": "^10.3.1",
|
|
27
|
+
"zod": "^4.6.5"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^26.6.3",
|
|
31
|
+
"pino-pretty": "^13.1.3"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createEnv } from "@t3-oss/env-core";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export const env = createEnv({
|
|
5
|
+
runtimeEnv: process.env,
|
|
6
|
+
server: {
|
|
7
|
+
LOG_LEVEL: z.enum(["fatal", "error", "warn", "info", "debug", "trace"]).default("info"),
|
|
8
|
+
REDIS_HOST: z.string().min(1).default("redis"),
|
|
9
|
+
REDIS_PORT: z.coerce.number().min(1).max(65_535).default(6379),
|
|
10
|
+
REDIS_RETRY_LIMIT: z.coerce.number().min(1).default(5),
|
|
11
|
+
WORKER_CONCURRENCY: z.coerce.number().int().min(1).optional(),
|
|
12
|
+
},
|
|
13
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Worker } from "bullmq";
|
|
2
|
+
import type { Redis } from "ioredis";
|
|
3
|
+
import { logger } from "#src/lib/logger.js";
|
|
4
|
+
|
|
5
|
+
type ClosableWorker = Pick<Worker, "close">;
|
|
6
|
+
type QuitableRedis = Pick<Redis, "quit">;
|
|
7
|
+
|
|
8
|
+
export function createGracefulShutdown(worker: ClosableWorker, redis: QuitableRedis) {
|
|
9
|
+
let shutdownPromise: Promise<void> | undefined;
|
|
10
|
+
|
|
11
|
+
return (signal: string): Promise<void> => {
|
|
12
|
+
if (shutdownPromise) {
|
|
13
|
+
return shutdownPromise;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
logger.info(`Received ${signal}, closing server...`);
|
|
17
|
+
shutdownPromise = (async () => {
|
|
18
|
+
try {
|
|
19
|
+
await worker.close();
|
|
20
|
+
} finally {
|
|
21
|
+
await redis.quit();
|
|
22
|
+
}
|
|
23
|
+
})();
|
|
24
|
+
|
|
25
|
+
return shutdownPromise;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import pino from "pino";
|
|
2
|
+
import { env } from "#src/env.js";
|
|
3
|
+
|
|
4
|
+
export const logger = pino({
|
|
5
|
+
formatters: {
|
|
6
|
+
level: (label: string) => ({ level: label.toUpperCase() }),
|
|
7
|
+
},
|
|
8
|
+
level: env.LOG_LEVEL,
|
|
9
|
+
...(process.env.NODE_ENV !== "production" && {
|
|
10
|
+
transport: {
|
|
11
|
+
options: {
|
|
12
|
+
colorize: true,
|
|
13
|
+
ignore: "pid,hostname,req,res,context",
|
|
14
|
+
singleLine: true,
|
|
15
|
+
translateTime: "dd.mm.yyyy, HH:MM:ss",
|
|
16
|
+
},
|
|
17
|
+
target: "pino-pretty",
|
|
18
|
+
},
|
|
19
|
+
}),
|
|
20
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createGracefulShutdown } from "#src/lib/graceful-shutdown.js";
|
|
2
|
+
import { logger } from "#src/lib/logger.js";
|
|
3
|
+
import { redis } from "#src/worker/redis.js";
|
|
4
|
+
import { createWorker } from "#src/worker/worker.js";
|
|
5
|
+
|
|
6
|
+
const worker = createWorker(redis);
|
|
7
|
+
const gracefulShutdown = createGracefulShutdown(worker, redis);
|
|
8
|
+
|
|
9
|
+
const handleShutdown = (signal: string) => {
|
|
10
|
+
void gracefulShutdown(signal).catch((error: unknown) => {
|
|
11
|
+
logger.error({ error }, "Error during shutdown");
|
|
12
|
+
});
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
process.once("SIGINT", () => handleShutdown("SIGINT"));
|
|
16
|
+
|
|
17
|
+
process.once("SIGTERM", () => handleShutdown("SIGTERM"));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ExampleJob, ExampleJobResult } from "@nexst/queues";
|
|
2
|
+
import type { Job } from "bullmq";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Handles one `EXAMPLE_QUEUE` job. Replace with real work — keep processors
|
|
6
|
+
* pure functions of the job so they stay unit-testable without Redis.
|
|
7
|
+
*/
|
|
8
|
+
export function processExampleJob(job: Pick<Job<ExampleJob>, "data">): ExampleJobResult {
|
|
9
|
+
return {
|
|
10
|
+
processedAt: new Date().toISOString(),
|
|
11
|
+
reversed: Array.from(new Intl.Segmenter().segment(job.data.message), (part) => part.segment)
|
|
12
|
+
.toReversed()
|
|
13
|
+
.join(""),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createRedisConnectionOptions } from "@nexst/queues";
|
|
2
|
+
import { Redis } from "ioredis";
|
|
3
|
+
import { env } from "#src/env.js";
|
|
4
|
+
import { logger } from "#src/lib/logger.js";
|
|
5
|
+
|
|
6
|
+
export const redis = new Redis(
|
|
7
|
+
createRedisConnectionOptions({
|
|
8
|
+
host: env.REDIS_HOST,
|
|
9
|
+
port: env.REDIS_PORT,
|
|
10
|
+
retryLimit: env.REDIS_RETRY_LIMIT,
|
|
11
|
+
}),
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
redis.on("connect", () => {
|
|
15
|
+
logger.info("Connected to Redis");
|
|
16
|
+
});
|
|
17
|
+
redis.on("error", (error) => {
|
|
18
|
+
logger.error({ error }, "Redis connection error");
|
|
19
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import { EXAMPLE_QUEUE } from "@nexst/queues";
|
|
3
|
+
import type { ExampleJob, ExampleJobResult } from "@nexst/queues";
|
|
4
|
+
import type { Job } from "bullmq";
|
|
5
|
+
import { Worker } from "bullmq";
|
|
6
|
+
import type { Redis } from "ioredis";
|
|
7
|
+
import { env } from "#src/env.js";
|
|
8
|
+
import { logger } from "#src/lib/logger.js";
|
|
9
|
+
import { processExampleJob } from "#src/processors/example-processor.js";
|
|
10
|
+
|
|
11
|
+
const workerConcurrency = env.WORKER_CONCURRENCY ?? Math.max(4, os.cpus().length * 2);
|
|
12
|
+
|
|
13
|
+
export function createWorker(connection: Redis): Worker<ExampleJob, ExampleJobResult> {
|
|
14
|
+
const worker = new Worker<ExampleJob, ExampleJobResult>(
|
|
15
|
+
EXAMPLE_QUEUE,
|
|
16
|
+
async (job: Job<ExampleJob>) => processExampleJob(job),
|
|
17
|
+
{
|
|
18
|
+
concurrency: workerConcurrency,
|
|
19
|
+
connection,
|
|
20
|
+
removeOnComplete: { age: 24 * 3600, count: 100 },
|
|
21
|
+
removeOnFail: { count: 500 },
|
|
22
|
+
stalledInterval: 30_000,
|
|
23
|
+
},
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
worker.on("completed", (job: Job<ExampleJob>, result: ExampleJobResult) => {
|
|
27
|
+
logger.info({ jobId: job.id, result }, "Job completed");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
worker.on("failed", (job: Job<ExampleJob> | undefined, error: unknown) => {
|
|
31
|
+
logger.error({ error: String(error), jobId: job?.id }, "Job failed");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
logger.info({ concurrency: workerConcurrency, queue: EXAMPLE_QUEUE }, "Worker started");
|
|
35
|
+
|
|
36
|
+
return worker;
|
|
37
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { processExampleJob } from "#src/processors/example-processor.js";
|
|
3
|
+
|
|
4
|
+
describe("processExampleJob", () => {
|
|
5
|
+
it("reverses the message and stamps the processing time", () => {
|
|
6
|
+
const result = processExampleJob({
|
|
7
|
+
data: { message: "hello", requestedAt: new Date(0).toISOString() },
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
expect(result.reversed).toBe("olleh");
|
|
11
|
+
expect(Number.isNaN(Date.parse(result.processedAt))).toBe(false);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { createGracefulShutdown } from "#src/lib/graceful-shutdown.js";
|
|
3
|
+
|
|
4
|
+
vi.mock("#src/lib/logger.js", () => ({
|
|
5
|
+
logger: {
|
|
6
|
+
info: vi.fn(),
|
|
7
|
+
},
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
describe("createGracefulShutdown", () => {
|
|
11
|
+
it("shares one shutdown when multiple signals arrive", async () => {
|
|
12
|
+
let finishWorkerClose!: () => void;
|
|
13
|
+
const workerClose = vi.fn(
|
|
14
|
+
() =>
|
|
15
|
+
new Promise<void>((resolve) => {
|
|
16
|
+
finishWorkerClose = resolve;
|
|
17
|
+
}),
|
|
18
|
+
);
|
|
19
|
+
const redisQuit = vi.fn().mockResolvedValue("OK");
|
|
20
|
+
const shutdown = createGracefulShutdown({ close: workerClose }, { quit: redisQuit });
|
|
21
|
+
|
|
22
|
+
const firstShutdown = shutdown("SIGINT");
|
|
23
|
+
const secondShutdown = shutdown("SIGINT");
|
|
24
|
+
|
|
25
|
+
expect(secondShutdown).toBe(firstShutdown);
|
|
26
|
+
expect(workerClose).toHaveBeenCalledOnce();
|
|
27
|
+
expect(redisQuit).not.toHaveBeenCalled();
|
|
28
|
+
|
|
29
|
+
finishWorkerClose();
|
|
30
|
+
await firstShutdown;
|
|
31
|
+
|
|
32
|
+
expect(redisQuit).toHaveBeenCalledOnce();
|
|
33
|
+
});
|
|
34
|
+
});
|