@tulipes/cli 0.1.0 → 0.2.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -8
- package/dist/agent.d.ts +12 -0
- package/dist/agent.js +29 -0
- package/dist/agent.js.map +1 -0
- package/dist/app-core.d.ts +8 -6
- package/dist/app-core.js +23 -14
- package/dist/app-core.js.map +1 -1
- package/dist/bin.js +3 -3
- package/dist/bin.js.map +1 -1
- package/dist/config-check.d.ts +8 -0
- package/dist/config-check.js +43 -0
- package/dist/config-check.js.map +1 -0
- package/dist/config-import.d.ts +20 -0
- package/dist/config-import.js +133 -0
- package/dist/config-import.js.map +1 -0
- package/dist/dev.d.ts +4 -4
- package/dist/dev.js +7 -10
- package/dist/dev.js.map +1 -1
- package/dist/init.d.ts +6 -11
- package/dist/init.js +17 -6420
- package/dist/init.js.map +1 -1
- package/dist/inspection.d.ts +9 -8
- package/dist/inspection.js +10 -11
- package/dist/inspection.js.map +1 -1
- package/dist/main.js +82 -11
- package/dist/main.js.map +1 -1
- package/dist/migrate.d.ts +30 -0
- package/dist/migrate.js +772 -0
- package/dist/migrate.js.map +1 -0
- package/dist/new-module.js +87 -46
- package/dist/new-module.js.map +1 -1
- package/dist/routes.d.ts +3 -5
- package/dist/routes.js +4 -6
- package/dist/routes.js.map +1 -1
- package/dist/spec.d.ts +2 -5
- package/dist/spec.js +13 -6
- package/dist/spec.js.map +1 -1
- package/dist/sync.d.ts +3 -11
- package/dist/sync.js +81 -20
- package/dist/sync.js.map +1 -1
- package/dist/template.d.ts +58 -0
- package/dist/template.js +244 -0
- package/dist/template.js.map +1 -0
- package/dist/update.js +4 -4
- package/package.json +17 -16
- package/templates/AGENTS.md +162 -0
- package/templates/CLAUDE.md +11 -118
- package/templates/app/.envs/.env.development +18 -0
- package/templates/app/.envs/.env.production +19 -0
- package/templates/app/.envs/.env.staging +19 -0
- package/templates/app/README.md +327 -0
- package/templates/app/app.ts +3 -0
- package/templates/app/config/app.config.ts +73 -0
- package/templates/app/docs/agent-manifest.json +724 -0
- package/templates/app/ecosystem.config.cjs +60 -0
- package/templates/app/modules/agent/controllers/agent.controllers.ts +31 -0
- package/templates/app/modules/agent/i18n/en.json +5 -0
- package/templates/app/modules/agent/i18n/fr.json +5 -0
- package/templates/app/modules/agent/module.acl.ts +14 -0
- package/templates/app/modules/agent/package.json +21 -0
- package/templates/app/modules/agent/routes/agent.routes.ts +29 -0
- package/templates/app/modules/agent/schemas/agent.schemas.ts +9 -0
- package/templates/app/modules/auth/bootstrap/credentials.bootstrap.ts +26 -0
- package/templates/app/modules/auth/bootstrap/seed-admin.bootstrap.ts +44 -0
- package/templates/app/modules/auth/controllers/auth.controllers.ts +312 -0
- package/templates/app/modules/auth/helpers/access-check.ts +42 -0
- package/templates/app/modules/auth/helpers/auth.ts +48 -0
- package/templates/app/modules/auth/helpers/documents.ts +57 -0
- package/templates/app/modules/auth/helpers/passport.ts +180 -0
- package/templates/app/modules/auth/helpers/passwords.ts +58 -0
- package/templates/app/modules/auth/helpers/session-cache.ts +67 -0
- package/templates/app/modules/auth/helpers/tokens.ts +329 -0
- package/templates/app/modules/auth/i18n/en.json +19 -0
- package/templates/app/modules/auth/i18n/fr.json +19 -0
- package/templates/app/modules/auth/lib/index.ts +12 -0
- package/templates/app/modules/auth/models/credential.model.ts +31 -0
- package/templates/app/modules/auth/models/revoked-token.model.ts +20 -0
- package/templates/app/modules/auth/models/session.model.ts +42 -0
- package/templates/app/modules/auth/module.acl.ts +41 -0
- package/templates/app/modules/auth/module.config.ts +30 -0
- package/templates/app/modules/auth/package.json +61 -0
- package/templates/app/modules/auth/routes/auth.routes.ts +94 -0
- package/templates/app/modules/auth/schemas/auth.schemas.ts +37 -0
- package/templates/app/modules/auth/tests/auth.test.ts +1079 -0
- package/templates/app/modules/config/controllers/config.controllers.ts +89 -0
- package/templates/app/modules/config/helpers/audit.ts +41 -0
- package/templates/app/modules/config/helpers/describe.ts +72 -0
- package/templates/app/modules/config/i18n/en.json +6 -0
- package/templates/app/modules/config/i18n/fr.json +6 -0
- package/templates/app/modules/config/models/config-audit.model.ts +26 -0
- package/templates/app/modules/config/module.acl.ts +12 -0
- package/templates/app/modules/config/package.json +23 -0
- package/templates/app/modules/config/routes/config.routes.ts +37 -0
- package/templates/app/modules/config/schemas/config.schemas.ts +13 -0
- package/templates/app/modules/config/tests/config.test.ts +126 -0
- package/templates/app/modules/core/bootstrap/cache.bootstrap.ts +12 -0
- package/templates/app/modules/core/helpers/cache/index.ts +60 -0
- package/templates/app/modules/core/helpers/cache/memory-cache.ts +53 -0
- package/templates/app/modules/core/helpers/cache/redis-cache.ts +68 -0
- package/templates/app/modules/core/helpers/cache/types.ts +37 -0
- package/templates/app/modules/core/helpers/lazy.ts +13 -0
- package/templates/app/modules/core/helpers/logger/index.ts +115 -0
- package/templates/app/modules/core/helpers/mongoose/index.ts +27 -0
- package/templates/app/modules/core/helpers/mongoose/plugins/base.plugin.ts +14 -0
- package/templates/app/modules/core/helpers/mongoose/plugins/soft-delete.plugin.ts +124 -0
- package/templates/app/modules/core/helpers/mongoose/plugins/timestamps.plugin.ts +13 -0
- package/templates/app/modules/core/helpers/mongoose/plugins/types.ts +45 -0
- package/templates/app/modules/core/helpers/mongoose/register.ts +20 -0
- package/templates/app/modules/core/lib/index.ts +34 -0
- package/templates/app/modules/core/module.config.ts +24 -0
- package/templates/app/modules/core/package.json +80 -0
- package/templates/app/modules/core/routes/core.routes.ts +24 -0
- package/templates/app/modules/core/tests/core.test.ts +19 -0
- package/templates/app/modules/hello/agent/hello.agent.ts +33 -0
- package/templates/app/modules/hello/bootstrap/seed-greetings.bootstrap.ts +27 -0
- package/templates/app/modules/hello/controllers/greetings.controllers.ts +43 -0
- package/templates/app/modules/hello/controllers/hello.controllers.ts +50 -0
- package/templates/app/modules/hello/helpers/greeting.ts +8 -0
- package/templates/app/modules/hello/i18n/en.json +8 -0
- package/templates/app/modules/hello/i18n/fr.json +8 -0
- package/templates/app/modules/hello/models/greeting.model.ts +24 -0
- package/templates/app/modules/hello/module.acl.ts +16 -0
- package/templates/app/modules/hello/module.config.ts +14 -0
- package/templates/app/modules/hello/package.json +24 -0
- package/templates/app/modules/hello/queues/hello.queues.ts +38 -0
- package/templates/app/modules/hello/routes/greetings.routes.ts +25 -0
- package/templates/app/modules/hello/routes/hello.routes.ts +44 -0
- package/templates/app/modules/hello/sockets/hello.sockets.ts +21 -0
- package/templates/app/modules/hello/tests/hello.test.ts +19 -0
- package/templates/app/modules/security/bootstrap/rate-limit.bootstrap.ts +8 -0
- package/templates/app/modules/security/helpers/hardening.ts +65 -0
- package/templates/app/modules/security/helpers/proxy.ts +16 -0
- package/templates/app/modules/security/helpers/rate-limit.ts +150 -0
- package/templates/app/modules/security/module.config.ts +59 -0
- package/templates/app/modules/security/package.json +26 -0
- package/templates/app/modules/security/routes/security.routes.ts +42 -0
- package/templates/app/modules/security/tests/security.test.ts +174 -0
- package/templates/app/modules/settings/bootstrap/log-streams.bootstrap.ts +12 -0
- package/templates/app/modules/settings/controllers/settings.controllers.ts +58 -0
- package/templates/app/modules/settings/helpers/access-lease.ts +99 -0
- package/templates/app/modules/settings/helpers/log-stream.ts +272 -0
- package/templates/app/modules/settings/helpers/log-tail.ts +253 -0
- package/templates/app/modules/settings/i18n/en.json +3 -0
- package/templates/app/modules/settings/i18n/fr.json +3 -0
- package/templates/app/modules/settings/module.acl.ts +12 -0
- package/templates/app/modules/settings/package.json +21 -0
- package/templates/app/modules/settings/routes/settings.routes.ts +33 -0
- package/templates/app/modules/settings/schemas/settings.schemas.ts +8 -0
- package/templates/app/modules/settings/tests/settings.test.ts +409 -0
- package/templates/app/modules/users/controllers/users.controllers.ts +113 -0
- package/templates/app/modules/users/i18n/en.json +14 -0
- package/templates/app/modules/users/i18n/fr.json +14 -0
- package/templates/app/modules/users/models/user.model.ts +18 -0
- package/templates/app/modules/users/module.acl.ts +10 -0
- package/templates/app/modules/users/module.config.ts +11 -0
- package/templates/app/modules/users/package.json +24 -0
- package/templates/app/modules/users/queues/users.queues.ts +31 -0
- package/templates/app/modules/users/routes/users.routes.ts +60 -0
- package/templates/app/modules/users/schemas/users.schemas.ts +30 -0
- package/templates/app/modules/users/sockets/users.sockets.ts +18 -0
- package/templates/app/nginx/development.conf +44 -0
- package/templates/app/nginx/production.conf +75 -0
- package/templates/app/nginx/staging.conf +51 -0
- package/templates/app/package.json +58 -0
- package/templates/app/scripts/count-greetings.ts +29 -0
- package/templates/app/scripts/deploy-nginx.ts +143 -0
- package/templates/app/scripts/list-users.ts +34 -0
- package/templates/app/tests/app.test.ts +28 -0
- package/templates/app/tests/support.ts +36 -0
- package/templates/app/tsconfig.json +28 -0
- package/templates/app/vitest.config.ts +6 -0
- package/templates/app/worker.ts +6 -0
- package/templates/claude/skills/tulipes-agent-tool/SKILL.md +83 -0
- package/templates/claude/skills/tulipes-boot-errors/SKILL.md +11 -5
- package/templates/claude/skills/tulipes-config/SKILL.md +74 -0
- package/templates/claude/skills/tulipes-endpoint/SKILL.md +38 -25
- package/templates/claude/skills/tulipes-env-variable/SKILL.md +54 -26
- package/templates/claude/skills/tulipes-model/SKILL.md +11 -4
- package/templates/claude/skills/tulipes-module/SKILL.md +11 -10
- package/templates/claude/skills/tulipes-queue/SKILL.md +20 -6
- package/templates/claude/skills/tulipes-settings/SKILL.md +49 -100
- package/templates/claude/skills/tulipes-spec/SKILL.md +10 -12
- package/templates/minimal/README.md +21 -0
- package/templates/minimal/app.ts +3 -0
- package/templates/minimal/config/app.config.ts +16 -0
- package/templates/minimal/modules/agent/module.acl.ts +3 -0
- package/templates/minimal/modules/agent/package.json +14 -0
- package/templates/minimal/modules/agent/routes/agent.routes.ts +20 -0
- package/templates/minimal/modules/core/module.acl.ts +4 -0
- package/templates/minimal/modules/core/package.json +21 -0
- package/templates/minimal/modules/hello/i18n/en.json +3 -0
- package/templates/minimal/modules/hello/module.acl.ts +2 -0
- package/templates/minimal/modules/hello/package.json +11 -0
- package/templates/minimal/modules/hello/routes/health.routes.ts +9 -0
- package/templates/minimal/modules/hello/routes/hello.routes.ts +10 -0
- package/templates/minimal/modules/security/package.json +15 -0
- package/templates/minimal/modules/security/routes/security.routes.ts +8 -0
- package/templates/minimal/package.json +38 -0
- package/templates/minimal/tests/app.test.ts +52 -0
- package/templates/minimal/tsconfig.json +22 -0
- package/templates/minimal/vitest.config.ts +5 -0
- package/templates/overlays/no-queues/ecosystem.config.cjs +52 -0
- package/templates/overlays/no-queues/modules/hello/controllers/greetings.controllers.ts +41 -0
- package/templates/overlays/no-queues/modules/users/controllers/users.controllers.ts +107 -0
- package/templates/versions.json +11 -0
- package/dist/minimal.d.ts +0 -2
- package/dist/minimal.js +0 -2200
- package/dist/minimal.js.map +0 -1
- /package/templates/{browser-auth.md → app/docs/browser-auth.md} +0 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import type { BackendCtx } from "@tulipes/core/boot";
|
|
3
|
+
import { z } from "zod/v4";
|
|
4
|
+
|
|
5
|
+
import { UsersController } from "../controllers/users.controllers.ts";
|
|
6
|
+
import { createBody, emailParam, listQuery, userShape } from "../schemas/users.schemas.ts";
|
|
7
|
+
|
|
8
|
+
export default function usersRoutes(ctx: BackendCtx): void {
|
|
9
|
+
const { rai, routes } = ctx;
|
|
10
|
+
const router = Router();
|
|
11
|
+
const users = new UsersController(ctx);
|
|
12
|
+
|
|
13
|
+
router.get("/", rai({
|
|
14
|
+
id: "users:read",
|
|
15
|
+
name: "List users",
|
|
16
|
+
description: "Page through accounts, clamped by the shared bounds",
|
|
17
|
+
// Validated on the way in, and described on the way out: one
|
|
18
|
+
// declaration, so the spec cannot drift from what is enforced.
|
|
19
|
+
query: listQuery,
|
|
20
|
+
returns: z.array(userShape),
|
|
21
|
+
}), users.list());
|
|
22
|
+
|
|
23
|
+
router.delete("/:email", rai({
|
|
24
|
+
id: "users:delete",
|
|
25
|
+
name: "Soft-delete a user",
|
|
26
|
+
description: "Flags the account; ordinary queries stop returning it",
|
|
27
|
+
params: emailParam,
|
|
28
|
+
}), users.softDelete());
|
|
29
|
+
|
|
30
|
+
router.get("/deleted", rai({
|
|
31
|
+
id: "users:read-deleted",
|
|
32
|
+
name: "List soft-deleted users",
|
|
33
|
+
}), users.listDeleted());
|
|
34
|
+
|
|
35
|
+
router.post("/", rai({
|
|
36
|
+
id: "users:create",
|
|
37
|
+
status: 201,
|
|
38
|
+
name: "Create a user",
|
|
39
|
+
description: "Adds the account and queues its welcome email",
|
|
40
|
+
body: createBody,
|
|
41
|
+
}), users.create());
|
|
42
|
+
|
|
43
|
+
router.get("/ping", rai({
|
|
44
|
+
id: "users:ping",
|
|
45
|
+
name: "Users module status",
|
|
46
|
+
}), users.ping());
|
|
47
|
+
|
|
48
|
+
router.get("/missing", rai({
|
|
49
|
+
id: "users:demo-missing",
|
|
50
|
+
name: "HttpError demo",
|
|
51
|
+
}), users.demoMissing());
|
|
52
|
+
|
|
53
|
+
router.get("/boom", rai({
|
|
54
|
+
id: "users:demo-boom",
|
|
55
|
+
name: "Unhandled error demo",
|
|
56
|
+
}), users.demoBoom());
|
|
57
|
+
|
|
58
|
+
// Deliberately outside the API prefix, to show a mount of its own.
|
|
59
|
+
routes.mount(router, "/users");
|
|
60
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from "zod/v4";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shapes for the users endpoints. Declared once and used twice: `rai()`
|
|
5
|
+
* validates the request against them, and `tulipes spec` describes them
|
|
6
|
+
* in the generated OpenAPI document and Postman collection — so the
|
|
7
|
+
* documentation can never claim a shape the endpoint would reject.
|
|
8
|
+
*
|
|
9
|
+
* zod/v4, not the classic import: `z.toJSONSchema` is what the generator
|
|
10
|
+
* uses, and it only understands v4 schemas.
|
|
11
|
+
*/
|
|
12
|
+
export const listQuery = z.object({
|
|
13
|
+
page: z.coerce.number().int().min(1).optional(),
|
|
14
|
+
per_page: z.coerce.number().int().min(1).max(100).optional(),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const createBody = z.object({
|
|
18
|
+
// The message doubles as an i18n key: the error handler translates it
|
|
19
|
+
// in this module's namespace, and falls back to the text as written.
|
|
20
|
+
email: z.email("emailInvalid"),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export const emailParam = z.object({
|
|
24
|
+
email: z.email("emailInvalid"),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export const userShape = z.object({
|
|
28
|
+
email: z.string(),
|
|
29
|
+
role: z.string(),
|
|
30
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Ctx } from "@tulipes/core/boot";
|
|
2
|
+
import type { SocketRegistry } from "@tulipes/socket.io";
|
|
3
|
+
|
|
4
|
+
export default function usersSockets({ config }: Ctx, sockets: SocketRegistry): void {
|
|
5
|
+
sockets.namespace("/users", (nsp) => {
|
|
6
|
+
nsp.on("connection", (socket) => {
|
|
7
|
+
socket.emit("welcome", {
|
|
8
|
+
module: "users",
|
|
9
|
+
signupMode: config.users?.signupMode,
|
|
10
|
+
online: nsp.sockets.size,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
socket.on("whoami", (ack: (reply: { id: string }) => void) => {
|
|
14
|
+
ack({ id: socket.id });
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# {{DOMAIN}} — rendered from nginx/development.conf by scripts/deploy-nginx.ts.
|
|
2
|
+
#
|
|
3
|
+
# Development rarely needs nginx at all: `pnpm dev` already serves on
|
|
4
|
+
# {{PORT}}. This exists so the proxy layer itself can be exercised locally —
|
|
5
|
+
# websocket upgrades and forwarded headers behave differently through a
|
|
6
|
+
# proxy, and finding that out in staging is finding out late.
|
|
7
|
+
#
|
|
8
|
+
# The deploy script refuses while core.publicDomain is still "localhost", so
|
|
9
|
+
# using this means pointing a real hostname at the machine first.
|
|
10
|
+
|
|
11
|
+
server {
|
|
12
|
+
listen 80;
|
|
13
|
+
server_name {{DOMAIN}};
|
|
14
|
+
|
|
15
|
+
access_log /var/log/nginx/{{DOMAIN}}.access.log;
|
|
16
|
+
error_log /var/log/nginx/{{DOMAIN}}.error.log;
|
|
17
|
+
|
|
18
|
+
client_max_body_size 1m;
|
|
19
|
+
|
|
20
|
+
location / {
|
|
21
|
+
proxy_pass http://127.0.0.1:{{PORT}};
|
|
22
|
+
proxy_http_version 1.1;
|
|
23
|
+
|
|
24
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
25
|
+
proxy_set_header Connection $http_connection;
|
|
26
|
+
|
|
27
|
+
# Single public ingress: overwrite client-supplied forwarding headers.
|
|
28
|
+
# For this local upstream, set TRUST_PROXY_CIDRS=127.0.0.1/32.
|
|
29
|
+
#
|
|
30
|
+
# That trust is only as good as the port below: the app binds every
|
|
31
|
+
# interface, so if {{PORT}} is reachable from outside this host a
|
|
32
|
+
# client can skip nginx entirely — no client_max_body_size, no TLS —
|
|
33
|
+
# and anything on the host can state its own X-Forwarded-For. Firewall
|
|
34
|
+
# the port to loopback, or run the app where the platform does it.
|
|
35
|
+
proxy_set_header Host $host;
|
|
36
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
37
|
+
proxy_set_header X-Forwarded-For $remote_addr;
|
|
38
|
+
proxy_set_header X-Forwarded-Host $host;
|
|
39
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
40
|
+
|
|
41
|
+
proxy_read_timeout 3600s;
|
|
42
|
+
proxy_send_timeout 3600s;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# {{DOMAIN}} — rendered from nginx/production.conf by scripts/deploy-nginx.ts.
|
|
2
|
+
# Placeholders are substituted at deploy time; edit this template, never the
|
|
3
|
+
# copy in /etc/nginx/sites-available.
|
|
4
|
+
#
|
|
5
|
+
# Debian/Ubuntu layout (sites-available + sites-enabled).
|
|
6
|
+
|
|
7
|
+
server {
|
|
8
|
+
listen 80;
|
|
9
|
+
listen [::]:80;
|
|
10
|
+
server_name {{DOMAIN}};
|
|
11
|
+
|
|
12
|
+
access_log /var/log/nginx/{{DOMAIN}}.access.log;
|
|
13
|
+
error_log /var/log/nginx/{{DOMAIN}}.error.log;
|
|
14
|
+
|
|
15
|
+
# Certbot's HTTP-01 challenge, kept above the proxy so certificate
|
|
16
|
+
# issuance and renewal work without touching this file.
|
|
17
|
+
location /.well-known/acme-challenge/ {
|
|
18
|
+
root /var/www/html;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
# Keep in step with the BODY_LIMIT variable. If nginx is stricter,
|
|
22
|
+
# oversized requests die here as an nginx HTML page instead of the
|
|
23
|
+
# API's JSON 413.
|
|
24
|
+
client_max_body_size 1m;
|
|
25
|
+
|
|
26
|
+
location / {
|
|
27
|
+
proxy_pass http://127.0.0.1:{{PORT}};
|
|
28
|
+
proxy_http_version 1.1;
|
|
29
|
+
|
|
30
|
+
# Socket.IO upgrades. Without these the handshake still succeeds and
|
|
31
|
+
# every connection silently degrades to HTTP polling.
|
|
32
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
33
|
+
# Passing the client's own Connection header rather than a mapped
|
|
34
|
+
# variable: a `map` block here would collide the moment a second
|
|
35
|
+
# site on this server defines the same variable.
|
|
36
|
+
proxy_set_header Connection $http_connection;
|
|
37
|
+
|
|
38
|
+
proxy_set_header Host $host;
|
|
39
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
40
|
+
proxy_set_header X-Forwarded-For $remote_addr;
|
|
41
|
+
proxy_set_header X-Forwarded-Host $host;
|
|
42
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
43
|
+
|
|
44
|
+
# For this local upstream, set TRUST_PROXY_CIDRS=127.0.0.1/32.
|
|
45
|
+
#
|
|
46
|
+
# That trust is only as good as the port below: the app binds every
|
|
47
|
+
# interface, so if {{PORT}} is reachable from outside this host a
|
|
48
|
+
# client can skip nginx entirely — no client_max_body_size, no TLS —
|
|
49
|
+
# and anything on the host can state its own X-Forwarded-For. Firewall
|
|
50
|
+
# the port to loopback, or run the app where the platform does it.
|
|
51
|
+
# These single-ingress templates overwrite client-supplied forwarding headers.
|
|
52
|
+
|
|
53
|
+
# Long-lived websockets must outlive nginx's 60s default.
|
|
54
|
+
proxy_read_timeout 3600s;
|
|
55
|
+
proxy_send_timeout 3600s;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# ── TLS ────────────────────────────────────────────────────────────────────
|
|
60
|
+
# Commented out on purpose: nginx -t fails on certificate paths that do not
|
|
61
|
+
# exist yet, which would make the very first deploy impossible. Install the
|
|
62
|
+
# certificate, then uncomment this block, redeploy with --force, and add a
|
|
63
|
+
# 301 redirect to the :80 server above.
|
|
64
|
+
#
|
|
65
|
+
# server {
|
|
66
|
+
# listen 443 ssl;
|
|
67
|
+
# listen [::]:443 ssl;
|
|
68
|
+
# http2 on;
|
|
69
|
+
# server_name {{DOMAIN}};
|
|
70
|
+
#
|
|
71
|
+
# ssl_certificate /etc/letsencrypt/live/{{DOMAIN}}/fullchain.pem;
|
|
72
|
+
# ssl_certificate_key /etc/letsencrypt/live/{{DOMAIN}}/privkey.pem;
|
|
73
|
+
#
|
|
74
|
+
# # …repeat the location / block from above…
|
|
75
|
+
# }
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# {{DOMAIN}} — rendered from nginx/staging.conf by scripts/deploy-nginx.ts.
|
|
2
|
+
# Placeholders are substituted at deploy time; edit this template, never the
|
|
3
|
+
# copy in /etc/nginx/sites-available.
|
|
4
|
+
#
|
|
5
|
+
# Identical to production except for the noindex header below.
|
|
6
|
+
|
|
7
|
+
server {
|
|
8
|
+
listen 80;
|
|
9
|
+
listen [::]:80;
|
|
10
|
+
server_name {{DOMAIN}};
|
|
11
|
+
|
|
12
|
+
access_log /var/log/nginx/{{DOMAIN}}.access.log;
|
|
13
|
+
error_log /var/log/nginx/{{DOMAIN}}.error.log;
|
|
14
|
+
|
|
15
|
+
# Staging must never reach a search index, even if a link leaks.
|
|
16
|
+
add_header X-Robots-Tag "noindex, nofollow" always;
|
|
17
|
+
|
|
18
|
+
location /.well-known/acme-challenge/ {
|
|
19
|
+
root /var/www/html;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
client_max_body_size 1m;
|
|
23
|
+
|
|
24
|
+
location / {
|
|
25
|
+
proxy_pass http://127.0.0.1:{{PORT}};
|
|
26
|
+
proxy_http_version 1.1;
|
|
27
|
+
|
|
28
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
29
|
+
proxy_set_header Connection $http_connection;
|
|
30
|
+
|
|
31
|
+
# Single public ingress: overwrite client-supplied forwarding headers.
|
|
32
|
+
# For this local upstream, set TRUST_PROXY_CIDRS=127.0.0.1/32.
|
|
33
|
+
#
|
|
34
|
+
# That trust is only as good as the port below: the app binds every
|
|
35
|
+
# interface, so if {{PORT}} is reachable from outside this host a
|
|
36
|
+
# client can skip nginx entirely — no client_max_body_size, no TLS —
|
|
37
|
+
# and anything on the host can state its own X-Forwarded-For. Firewall
|
|
38
|
+
# the port to loopback, or run the app where the platform does it.
|
|
39
|
+
proxy_set_header Host $host;
|
|
40
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
41
|
+
proxy_set_header X-Forwarded-For $remote_addr;
|
|
42
|
+
proxy_set_header X-Forwarded-Host $host;
|
|
43
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
44
|
+
|
|
45
|
+
proxy_read_timeout 3600s;
|
|
46
|
+
proxy_send_timeout 3600s;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# TLS: see nginx/production.conf — same block, same reason it ships
|
|
51
|
+
# commented out.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tulipes-example/app",
|
|
3
|
+
"tulipes": {
|
|
4
|
+
"providers": {
|
|
5
|
+
"models": "@tulipes/mongoose",
|
|
6
|
+
"settings": "@tulipes/mongoose/settings",
|
|
7
|
+
"queues": "@tulipes/bullmq",
|
|
8
|
+
"sockets": "@tulipes/socket.io"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"private": true,
|
|
12
|
+
"type": "module",
|
|
13
|
+
"scripts": {
|
|
14
|
+
"dev": "tulipes dev",
|
|
15
|
+
"worker": "node worker.ts",
|
|
16
|
+
"sync": "tulipes sync",
|
|
17
|
+
"check": "tulipes env:check",
|
|
18
|
+
"typecheck": "tsc --noEmit",
|
|
19
|
+
"script": "node",
|
|
20
|
+
"spec": "tulipes spec --openapi docs/openapi.json --postman docs/collection.json",
|
|
21
|
+
"manifest": "tulipes agent manifest --out docs/agent-manifest.json",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"start": "node app.ts",
|
|
24
|
+
"pm2:start": "pm2 start ecosystem.config.cjs",
|
|
25
|
+
"pm2:start:staging": "pm2 start ecosystem.config.cjs --env staging",
|
|
26
|
+
"pm2:start:production": "pm2 start ecosystem.config.cjs --env production",
|
|
27
|
+
"pm2:stop": "pm2 stop ecosystem.config.cjs",
|
|
28
|
+
"pm2:restart": "pm2 restart ecosystem.config.cjs",
|
|
29
|
+
"pm2:delete": "pm2 delete ecosystem.config.cjs",
|
|
30
|
+
"pm2:logs": "pm2 logs",
|
|
31
|
+
"pm2:status": "pm2 status"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@tulipes/bullmq": "workspace:*",
|
|
35
|
+
"@tulipes/core": "workspace:*",
|
|
36
|
+
"@tulipes/mongoose": "workspace:*",
|
|
37
|
+
"@tulipes/socket.io": "workspace:*",
|
|
38
|
+
"ioredis": "^5",
|
|
39
|
+
"mongoose": "^8",
|
|
40
|
+
"zod": "^3.25"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@tulipes/cli": "workspace:*",
|
|
44
|
+
"@tulipes/spec": "workspace:*",
|
|
45
|
+
"@tulipes/testing": "workspace:*",
|
|
46
|
+
"@types/express": "^5",
|
|
47
|
+
"@types/jsonwebtoken": "^9",
|
|
48
|
+
"@types/node": "^24",
|
|
49
|
+
"jsonwebtoken": "^9",
|
|
50
|
+
"pm2": "^7.0.4",
|
|
51
|
+
"typescript": "^7",
|
|
52
|
+
"vite": "^8.2.2",
|
|
53
|
+
"vitest": "^5.0.0"
|
|
54
|
+
},
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": ">=24.0.0 <25"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example maintenance script. Run it with:
|
|
3
|
+
*
|
|
4
|
+
* pnpm script scripts/count-greetings.ts
|
|
5
|
+
*
|
|
6
|
+
* Script mode runs every phase the backend runs — environment, modules,
|
|
7
|
+
* config, ACL, database, models, queue producers — and then starts nothing.
|
|
8
|
+
* So a script gets exactly what a route handler gets, minus the server.
|
|
9
|
+
*
|
|
10
|
+
* The one thing to always copy from this file is the try/finally: the mongo
|
|
11
|
+
* and redis connections hold the event loop open, so a script that skips
|
|
12
|
+
* shutdown() prints its output and then hangs forever.
|
|
13
|
+
*/
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
|
|
16
|
+
import { boot } from "@tulipes/core/boot";
|
|
17
|
+
|
|
18
|
+
const handle = await boot({ rootDir: join(import.meta.dirname, ".."), mode: "script" });
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const { models, config } = handle.ctx;
|
|
22
|
+
|
|
23
|
+
const greetings = await models!.get("Greeting").find().select("name times_used -_id").lean();
|
|
24
|
+
|
|
25
|
+
console.log(`${config.app?.name} — ${greetings.length} greeting(s)\n`);
|
|
26
|
+
console.table(greetings);
|
|
27
|
+
} finally {
|
|
28
|
+
await handle.shutdown();
|
|
29
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install this app's nginx site config.
|
|
3
|
+
*
|
|
4
|
+
* sudo -E APP_ENV=production pnpm script scripts/deploy-nginx.ts
|
|
5
|
+
* APP_ENV=production pnpm script scripts/deploy-nginx.ts --dry-run
|
|
6
|
+
*
|
|
7
|
+
* APP_ENV picks everything at once — which template is rendered, and the
|
|
8
|
+
* domain and port rendered into it — so the three can never disagree.
|
|
9
|
+
*
|
|
10
|
+
* It boots in script mode like every other script, which means the database
|
|
11
|
+
* and redis must be reachable from wherever you run it. That is the price
|
|
12
|
+
* of every script looking the same; run --dry-run from anywhere to preview.
|
|
13
|
+
*
|
|
14
|
+
* Debian/Ubuntu only: it writes sites-available and links sites-enabled.
|
|
15
|
+
* Certificates are yours to install — the rendered conf is HTTP-only, with
|
|
16
|
+
* a commented TLS block to uncomment once they exist.
|
|
17
|
+
*/
|
|
18
|
+
import { execFileSync } from "node:child_process";
|
|
19
|
+
import { existsSync, lstatSync, readFileSync, readlinkSync, symlinkSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
|
|
22
|
+
import { boot } from "@tulipes/core/boot";
|
|
23
|
+
|
|
24
|
+
const SITES_AVAILABLE = "/etc/nginx/sites-available";
|
|
25
|
+
const SITES_ENABLED = "/etc/nginx/sites-enabled";
|
|
26
|
+
|
|
27
|
+
const flags = new Set(process.argv.slice(2));
|
|
28
|
+
const dryRun = flags.has("--dry-run");
|
|
29
|
+
const force = flags.has("--force");
|
|
30
|
+
|
|
31
|
+
const rootDir = join(import.meta.dirname, "..");
|
|
32
|
+
const handle = await boot({ rootDir, mode: "script" });
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
await deploy();
|
|
36
|
+
} finally {
|
|
37
|
+
// Without this the open mongo and redis handles keep the process alive
|
|
38
|
+
// and the script appears to hang after doing its work.
|
|
39
|
+
await handle.shutdown();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function deploy(): Promise<void> {
|
|
43
|
+
const { appEnv } = handle.env;
|
|
44
|
+
const domain = String(handle.ctx.config.core?.publicDomain ?? "localhost").trim();
|
|
45
|
+
const port = Number(handle.env.get("PORT"));
|
|
46
|
+
|
|
47
|
+
const templatePath = join(rootDir, "nginx", `${appEnv}.conf`);
|
|
48
|
+
if (!existsSync(templatePath)) {
|
|
49
|
+
return fail(`no template for APP_ENV=${appEnv} — expected nginx/${appEnv}.conf`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const conf = readFileSync(templatePath, "utf8")
|
|
53
|
+
.replaceAll("{{DOMAIN}}", domain)
|
|
54
|
+
.replaceAll("{{PORT}}", String(port));
|
|
55
|
+
|
|
56
|
+
// Preview stays available everywhere, including a laptop with no nginx
|
|
57
|
+
// and a domain still set to localhost.
|
|
58
|
+
if (dryRun) {
|
|
59
|
+
console.log(`# ${templatePath} → ${SITES_AVAILABLE}/${domain}\n`);
|
|
60
|
+
console.log(conf);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (domain === "localhost" || domain === "") {
|
|
65
|
+
return fail(
|
|
66
|
+
`core.publicDomain is "${domain}" — set a real hostname through the console (PATCH /config/core) before deploying`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// getuid is absent on Windows; its absence is itself a "not supported here".
|
|
71
|
+
if (process.getuid?.() !== 0) {
|
|
72
|
+
return fail("must run as root to write /etc/nginx — re-run with sudo -E");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!existsSync(SITES_AVAILABLE)) {
|
|
76
|
+
return fail(
|
|
77
|
+
`${SITES_AVAILABLE} does not exist — this script targets the Debian/Ubuntu nginx layout`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const availablePath = join(SITES_AVAILABLE, domain);
|
|
82
|
+
if (existsSync(availablePath) && !force) {
|
|
83
|
+
return fail(
|
|
84
|
+
`${availablePath} already exists — re-run with --force to overwrite it (it may have been hand-tuned on this box)`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
writeFileSync(availablePath, conf);
|
|
89
|
+
console.log(`wrote ${availablePath}`);
|
|
90
|
+
|
|
91
|
+
const enabledPath = join(SITES_ENABLED, domain);
|
|
92
|
+
if (!linkSite(availablePath, enabledPath)) return;
|
|
93
|
+
|
|
94
|
+
if (!run("nginx", ["-t"], "nginx rejected the configuration — nothing was reloaded")) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (!run("systemctl", ["reload", "nginx"], "nginx reload failed")) return;
|
|
98
|
+
|
|
99
|
+
console.log(`\n${domain} → 127.0.0.1:${port} (${appEnv}) — nginx reloaded`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Idempotent: a link already pointing at the right file is the goal state. */
|
|
103
|
+
function linkSite(target: string, link: string): boolean {
|
|
104
|
+
if (existsSync(link) || isSymlink(link)) {
|
|
105
|
+
const current = isSymlink(link) ? readlinkSync(link) : "(a regular file)";
|
|
106
|
+
if (current === target) {
|
|
107
|
+
console.log(`link ${link} already correct`);
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
fail(`${link} exists and points at ${current} — remove it and re-run`);
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
symlinkSync(target, link);
|
|
115
|
+
console.log(`linked ${link}`);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function isSymlink(path: string): boolean {
|
|
120
|
+
try {
|
|
121
|
+
return lstatSync(path).isSymbolicLink();
|
|
122
|
+
} catch {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function run(command: string, args: string[], message: string): boolean {
|
|
128
|
+
try {
|
|
129
|
+
execFileSync(command, args, { stdio: "inherit" });
|
|
130
|
+
return true;
|
|
131
|
+
} catch (error) {
|
|
132
|
+
// A missing binary is not a rejected config: reporting the latter would
|
|
133
|
+
// send you hunting a syntax error that does not exist.
|
|
134
|
+
const missing = (error as NodeJS.ErrnoException).code === "ENOENT";
|
|
135
|
+
fail(missing ? `could not run "${command}" — is it installed and on PATH?` : message);
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function fail(message: string): void {
|
|
141
|
+
console.error(`deploy-nginx: ${message}`);
|
|
142
|
+
process.exitCode = 1;
|
|
143
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example maintenance script. Run it with:
|
|
3
|
+
*
|
|
4
|
+
* pnpm script scripts/list-users.ts
|
|
5
|
+
*
|
|
6
|
+
* Script mode runs every phase the backend runs — environment, modules,
|
|
7
|
+
* config, ACL, database, models, queue producers — and then starts nothing.
|
|
8
|
+
* So a script gets exactly what a route handler gets, minus the server.
|
|
9
|
+
*
|
|
10
|
+
* The one thing to always copy from this file is the try/finally: the mongo
|
|
11
|
+
* and redis connections hold the event loop open, so a script that skips
|
|
12
|
+
* shutdown() prints its output and then hangs forever.
|
|
13
|
+
*/
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
|
|
16
|
+
import { boot } from "@tulipes/core/boot";
|
|
17
|
+
|
|
18
|
+
const handle = await boot({ rootDir: join(import.meta.dirname, ".."), mode: "script" });
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const { models, config } = handle.ctx;
|
|
22
|
+
|
|
23
|
+
const users = await models!
|
|
24
|
+
.get("User")
|
|
25
|
+
.find()
|
|
26
|
+
.select("email role -_id")
|
|
27
|
+
.limit(Number(config.pagination?.maxLimit ?? 100))
|
|
28
|
+
.lean();
|
|
29
|
+
|
|
30
|
+
console.log(`${config.app?.name} — ${users.length} user(s)\n`);
|
|
31
|
+
console.table(users);
|
|
32
|
+
} finally {
|
|
33
|
+
await handle.shutdown();
|
|
34
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { afterAll, beforeAll, expect, test } from "vitest";
|
|
3
|
+
import { boot } from "@tulipes/core/boot";
|
|
4
|
+
import { extract, toOpenApi, toPostman } from "@tulipes/spec";
|
|
5
|
+
import { useTestApp } from "@tulipes/testing";
|
|
6
|
+
|
|
7
|
+
import { testEnv, testSettings } from "./support.ts";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* App-level: what belongs to no module. Each module's tests live beside
|
|
11
|
+
* it, in modules/<name>/tests/, and boot the app the same way.
|
|
12
|
+
*/
|
|
13
|
+
const rootDir = fileURLToPath(new URL("../", import.meta.url));
|
|
14
|
+
const app = useTestApp({ rootDir, env: testEnv(), settings: testSettings() }, { beforeAll, afterAll });
|
|
15
|
+
|
|
16
|
+
test("an inspection boot describes the same routes and schemas as the running app", async () => {
|
|
17
|
+
const inspection = await boot({ rootDir, mode: "backend", inspect: true, handleSignals: false });
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const inspected = { ...extract(inspection.ctx), app: { name: "parity" } };
|
|
21
|
+
const runtime = { ...extract(app.handle.ctx), app: { name: "parity" } };
|
|
22
|
+
|
|
23
|
+
expect(await toOpenApi(inspected)).toEqual(await toOpenApi(runtime));
|
|
24
|
+
expect(await toPostman(inspected)).toEqual(await toPostman(runtime));
|
|
25
|
+
} finally {
|
|
26
|
+
await inspection.shutdown();
|
|
27
|
+
}
|
|
28
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What every module test boots the app with. The services are dedicated:
|
|
5
|
+
* the suite writes users, sessions and settings. The variables are the
|
|
6
|
+
* eleven the app declares; everything else the request pipeline needs is a
|
|
7
|
+
* setting, written after the boot the way an operator writes it.
|
|
8
|
+
*/
|
|
9
|
+
export function testEnv(): Record<string, string> {
|
|
10
|
+
if (!process.env.TEST_MONGO_URI || !process.env.TEST_REDIS_URL) {
|
|
11
|
+
throw new Error("Tests require dedicated TEST_MONGO_URI and TEST_REDIS_URL services");
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
MONGO_URI: process.env.TEST_MONGO_URI,
|
|
15
|
+
REDIS_URL: process.env.TEST_REDIS_URL,
|
|
16
|
+
JWT_ACCESS_SECRET: randomBytes(32).toString("hex"),
|
|
17
|
+
JWT_REFRESH_SECRET: randomBytes(32).toString("hex"),
|
|
18
|
+
AUTH_ADMIN_EMAIL: "test-admin@example.test",
|
|
19
|
+
AUTH_ADMIN_PASSWORD: randomBytes(32).toString("hex"),
|
|
20
|
+
LOG_LEVEL: "fatal",
|
|
21
|
+
LOG_FILE: "false",
|
|
22
|
+
LOG_CONSOLE: "false",
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function testSettings(): Record<string, Record<string, unknown>> {
|
|
27
|
+
return {
|
|
28
|
+
security: {
|
|
29
|
+
trustProxyCidrs: "127.0.0.1/32,::1/128",
|
|
30
|
+
corsOrigins: "https://console.example.test",
|
|
31
|
+
corsCredentials: false,
|
|
32
|
+
rateLimits: { namespace: "test-" + randomBytes(12).toString("hex"), windowMs: 60_000, auth: 200, session: 1000, config: 200 },
|
|
33
|
+
},
|
|
34
|
+
hello: { greeting: "Hello" },
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2023",
|
|
4
|
+
"lib": [
|
|
5
|
+
"ES2023"
|
|
6
|
+
],
|
|
7
|
+
"module": "NodeNext",
|
|
8
|
+
"moduleResolution": "NodeNext",
|
|
9
|
+
"allowImportingTsExtensions": true,
|
|
10
|
+
"rewriteRelativeImportExtensions": true,
|
|
11
|
+
"erasableSyntaxOnly": true,
|
|
12
|
+
"strict": true,
|
|
13
|
+
"noUncheckedIndexedAccess": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"skipLibCheck": true,
|
|
16
|
+
"forceConsistentCasingInFileNames": true,
|
|
17
|
+
"noEmit": true
|
|
18
|
+
},
|
|
19
|
+
"include": [
|
|
20
|
+
"app.ts",
|
|
21
|
+
"worker.ts",
|
|
22
|
+
"types",
|
|
23
|
+
"modules",
|
|
24
|
+
"config",
|
|
25
|
+
"scripts",
|
|
26
|
+
"tests"
|
|
27
|
+
]
|
|
28
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
|
|
3
|
+
// Node strips types natively, so the app's own files import without a loader.
|
|
4
|
+
// Every file boots the app once (@tulipes/testing) against one database, so
|
|
5
|
+
// files run one after another.
|
|
6
|
+
export default defineConfig({ test: { include: ["tests/**/*.test.ts", "modules/*/tests/**/*.test.ts"], fileParallelism: false } });
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { boot } from "@tulipes/core/boot";
|
|
2
|
+
|
|
3
|
+
// Worker mode refuses to start until at least one module registers a
|
|
4
|
+
// queue processor — a consumer with nothing to consume is a
|
|
5
|
+
// misconfigured deployment, not a valid idle state.
|
|
6
|
+
await boot({ rootDir: import.meta.dirname, mode: "worker" });
|