@podosoft/podokit 1.5.0 → 1.6.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/README.md +2 -1
- package/dist/app-proxy-timeouts.d.ts +27 -0
- package/dist/app-proxy-timeouts.js +180 -0
- package/dist/assemble.d.ts +5 -2
- package/dist/assemble.js +14 -4
- package/dist/deploy-checks.d.ts +44 -0
- package/dist/deploy-checks.js +267 -0
- package/dist/deploy-compose-profile.d.ts +6 -2
- package/dist/deploy-compose-profile.js +15 -6
- package/dist/deploy-compose-render.d.ts +12 -4
- package/dist/deploy-compose-render.js +47 -18
- package/dist/deploy-compose.d.ts +2 -5
- package/dist/deploy-compose.js +7 -2
- package/dist/deploy-profile.d.ts +6 -2
- package/dist/deploy-profile.js +11 -7
- package/dist/deploy-render.d.ts +4 -1
- package/dist/deploy-render.js +27 -21
- package/dist/deploy-schema.d.ts +56 -2
- package/dist/deploy-schema.js +130 -14
- package/dist/deploy.d.ts +2 -5
- package/dist/deploy.js +5 -2
- package/dist/dev.d.ts +21 -0
- package/dist/dev.js +78 -3
- package/dist/eject.js +2 -0
- package/dist/index.js +85 -5
- package/dist/lib.d.ts +2 -0
- package/dist/lib.js +5 -2
- package/dist/lockfile.d.ts +24 -0
- package/dist/lockfile.js +74 -4
- package/dist/provider.js +10 -5
- package/dist/remove.js +1 -1
- package/dist/template-files.d.ts +42 -0
- package/dist/template-files.js +102 -0
- package/dist/templates/fullstack/AGENTS.md +28 -4
- package/dist/templates/fullstack/README.md +8 -7
- package/dist/templates/fullstack/apps/api/package.json +4 -4
- package/dist/templates/fullstack/apps/api/scripts/build.extensions.mjs +6 -0
- package/dist/templates/fullstack/apps/api/scripts/build.mjs +65 -16
- package/dist/templates/fullstack/apps/api/scripts/run-migrations.mjs +7 -3
- package/dist/templates/fullstack/apps/api/scripts/verify-api-contract.ts +5 -1
- package/dist/templates/fullstack/apps/api/src/app.spec.ts +22 -0
- package/dist/templates/fullstack/apps/api/src/app.ts +41 -25
- package/dist/templates/fullstack/apps/api/src/build.spec.ts +118 -0
- package/dist/templates/fullstack/apps/api/src/config/env.extensions.ts +20 -0
- package/dist/templates/fullstack/apps/api/src/config/env.validation.spec.ts +76 -0
- package/dist/templates/fullstack/apps/api/src/config/env.validation.ts +18 -3
- package/dist/templates/fullstack/apps/api/src/config/provider-selection.spec.ts +53 -0
- package/dist/templates/fullstack/apps/api/src/config/provider-selection.ts +79 -0
- package/dist/templates/fullstack/apps/api/src/config/providers.ts +7 -3
- package/dist/templates/fullstack/apps/api/src/core/api-contract.extensions.ts +6 -0
- package/dist/templates/fullstack/apps/api/src/core/api-contract.spec.ts +29 -0
- package/dist/templates/fullstack/apps/api/src/core/api-contract.ts +39 -2
- package/dist/templates/fullstack/apps/api/src/core/services.ts +13 -0
- package/dist/templates/fullstack/apps/api/src/database/data-source.spec.ts +27 -0
- package/dist/templates/fullstack/apps/api/src/database/data-source.ts +7 -10
- package/dist/templates/fullstack/apps/api/src/database/database.spec.ts +12 -0
- package/dist/templates/fullstack/apps/api/src/database/database.ts +2 -3
- package/dist/templates/fullstack/apps/api/src/database/entities.ts +12 -0
- package/dist/templates/fullstack/apps/api/src/database/migrate.ts +12 -2
- package/dist/templates/fullstack/apps/api/src/database/migration-files.spec.ts +116 -0
- package/dist/templates/fullstack/apps/api/src/database/migration-files.ts +134 -0
- package/dist/templates/fullstack/apps/api/src/database/migration-readiness.spec.ts +36 -0
- package/dist/templates/fullstack/apps/api/src/database/migration-readiness.ts +25 -0
- package/dist/templates/fullstack/apps/api/src/database/run-migrations.spec.ts +12 -1
- package/dist/templates/fullstack/apps/api/src/database/sqlite-migrator.ts +2 -36
- package/dist/templates/fullstack/apps/api/src/main.ts +2 -2
- package/dist/templates/fullstack/apps/api/tsconfig.json +2 -0
- package/dist/templates/fullstack/apps/web/package.json +1 -1
- package/dist/templates/fullstack/apps/web/src/app.css +3 -0
- package/dist/templates/fullstack/apps/web/src/app.extensions.css +11 -0
- package/dist/templates/fullstack/apps/web/src/lib/components/site-runtime.svelte +3 -1
- package/dist/templates/fullstack/apps/web/src/lib/server/api.test.ts +19 -0
- package/dist/templates/fullstack/apps/web/src/lib/server/api.ts +4 -1
- package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.test.ts +79 -0
- package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.ts +55 -1
- package/dist/templates/fullstack/apps/web/src/lib/server/proxy-timeouts.ts +2 -1
- package/dist/templates/fullstack/apps/web/tsconfig.json +1 -1
- package/dist/templates/fullstack/apps/web/vite.config.test.ts +33 -0
- package/dist/templates/fullstack/apps/web/vite.config.ts +55 -13
- package/dist/templates/fullstack/apps/web/vite.extensions.ts +23 -0
- package/dist/templates/fullstack/compose.dev.yaml +7 -4
- package/dist/templates/fullstack/dot-agents/skills/podokit-deploy/SKILL.md +2 -2
- package/dist/templates/fullstack/dot-agents/skills/podokit-deploy/references/profile.md +12 -3
- package/dist/templates/fullstack/dot-claude/skills/podokit-update/SKILL.md +5 -2
- package/dist/templates/fullstack/dot-env.docker.example +5 -2
- package/dist/templates/fullstack/dot-env.example +4 -0
- package/dist/templates/fullstack/dot-podokit/dev.json +1 -1
- package/dist/templates/fullstack/infra/k3s/configmap.yaml +1 -0
- package/dist/templates/fullstack/tests/README.md +40 -1
- package/dist/templates/fullstack/tests/helpers/app-paths.ts +18 -0
- package/dist/templates/fullstack/tests/helpers/paths.ts +33 -0
- package/dist/templates/fullstack/tests/package.json +2 -2
- package/dist/templates/fullstack/tests/playwright.config.ts +7 -3
- package/dist/templates/fullstack/tests/playwright.extensions.ts +88 -1
- package/dist/templates/fullstack/tests/ui/smoke.ui.spec.ts +5 -4
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.test.ts +37 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.ts +15 -6
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-flag-settings.test.ts +31 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-flag-settings.ts +31 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-flags.ts +12 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-nav.ts +11 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/nav.test.ts +39 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/nav.ts +35 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/registry.svelte.ts +15 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-page.svelte +11 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/admin-sidebar.svelte +2 -23
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/data-table.svelte +7 -2
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/data-table.test.ts +30 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/impersonation-banner.svelte +3 -2
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/settings/app-flags-settings.svelte +65 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/sidebar-user-menu.svelte +32 -23
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/sidebar-user-menu.test.ts +27 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/site-footer.svelte +4 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/site-runtime.svelte +3 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/en.json +6 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/ko.json +6 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/app-hooks.ts +11 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/non-indexable-paths.ts +8 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/search-indexing.test.ts +31 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/search-indexing.ts +5 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/settings/+page.svelte +1 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/account/+page.server.ts +2 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/account/+page.svelte +1 -5
- package/dist/templates/modules/admin-dashboard/files/dot-claude/skills/podokit-data-table/SKILL.md +2 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/audit.api.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth-config.api.spec.ts +159 -170
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth.api.spec.ts +72 -81
- package/dist/templates/modules/admin-dashboard/files/tests/api/email.api.spec.ts +1 -6
- package/dist/templates/modules/admin-dashboard/files/tests/api/profile-image.api.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/api/site-settings.api.spec.ts +43 -53
- package/dist/templates/modules/admin-dashboard/files/tests/api/users-admin.api.spec.ts +3 -1
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/accounts.ts +24 -6
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/app-accounts.ts +24 -0
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/devices.ts +41 -11
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/disposable-users.ts +5 -3
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/mailpit.ts +3 -9
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/sms.ts +2 -4
- package/dist/templates/modules/admin-dashboard/files/tests/playwright.config.ts +11 -5
- package/dist/templates/modules/admin-dashboard/files/tests/ui/account-flows.ui.spec.ts +4 -5
- package/dist/templates/modules/admin-dashboard/files/tests/ui/account.ui.spec.ts +55 -55
- package/dist/templates/modules/admin-dashboard/files/tests/ui/app-flags.ui.spec.ts +63 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/appearance.ui.spec.ts +12 -16
- package/dist/templates/modules/admin-dashboard/files/tests/ui/audit.ui.spec.ts +3 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/auth.ui.spec.ts +32 -35
- package/dist/templates/modules/admin-dashboard/files/tests/ui/dashboard.ui.spec.ts +4 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/email-admin.ui.spec.ts +1 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/email.ui.spec.ts +1 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/general-effects.ui.spec.ts +82 -91
- package/dist/templates/modules/admin-dashboard/files/tests/ui/guards.ui.spec.ts +6 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/i18n.ui.spec.ts +16 -20
- package/dist/templates/modules/admin-dashboard/files/tests/ui/impersonation.ui.spec.ts +6 -3
- package/dist/templates/modules/admin-dashboard/files/tests/ui/login-methods.ui.spec.ts +2 -3
- package/dist/templates/modules/admin-dashboard/files/tests/ui/oidc.ui.spec.ts +2 -4
- package/dist/templates/modules/admin-dashboard/files/tests/ui/organizations.ui.spec.ts +5 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/passkey.ui.spec.ts +7 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/same-origin-requests.ui.spec.ts +89 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/sessions.ui.spec.ts +5 -4
- package/dist/templates/modules/admin-dashboard/files/tests/ui/settings-dialogs.ui.spec.ts +67 -73
- package/dist/templates/modules/admin-dashboard/files/tests/ui/settings.ui.spec.ts +84 -93
- package/dist/templates/modules/admin-dashboard/files/tests/ui/table-toolbar.ui.spec.ts +8 -7
- package/dist/templates/modules/admin-dashboard/files/tests/ui/theme-mode.ui.spec.ts +29 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/two-factor.ui.spec.ts +51 -54
- package/dist/templates/modules/admin-dashboard/files/tests/ui/users.ui.spec.ts +10 -9
- package/dist/templates/modules/admin-dashboard/module.manifest.json +7 -2
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.module.ts +5 -1
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.spec.ts +17 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.ts +18 -0
- package/dist/templates/modules/audit-log/module.manifest.json +15 -3
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-app-name.spec.ts +21 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-config-store.ts +13 -1
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-environment.ts +9 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-provider.ts +19 -3
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.module.ts +2 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.runtime.spec.ts +10 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +7 -5
- package/dist/templates/modules/auth/files/apps/api/src/auth/schema-recheck.spec.ts +86 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/schema-recheck.ts +44 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.service.ts +2 -1
- package/dist/templates/modules/auth/files/apps/api/src/migrate.ts +3 -1
- package/dist/templates/modules/auth/files/apps/api/src/settings/app-flags.ts +9 -0
- package/dist/templates/modules/auth/files/apps/api/src/settings/flag-defaults.spec.ts +43 -0
- package/dist/templates/modules/auth/files/apps/api/src/settings/flag-defaults.ts +30 -5
- package/dist/templates/modules/auth/module.manifest.json +8 -2
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.spec.ts +57 -1
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.ts +93 -9
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/worker.module.spec.ts +40 -0
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/worker.module.ts +52 -5
- package/dist/templates/modules/bullmq/files/apps/api/src/main-worker.ts +9 -7
- package/dist/templates/modules/bullmq/files/apps/api/src/worker.extensions.ts +13 -0
- package/dist/templates/modules/bullmq/module.manifest.json +5 -1
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.worker.spec.ts +2 -0
- package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.spec.ts +87 -1
- package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.ts +93 -3
- package/dist/templates/modules/jobs-local/module.manifest.json +3 -0
- package/dist/templates/modules/logging/files/apps/api/src/logging/logging.module.spec.ts +16 -0
- package/dist/templates/modules/logging/files/apps/api/src/logging/logging.module.ts +15 -10
- package/dist/templates/modules/mailer/module.manifest.json +1 -1
- package/dist/templates/modules/object-storage-local/files/tests/api/storage-local.api.spec.ts +1 -1
- package/dist/templates/modules/object-storage-s3/files/infra/docker/minio.compose.yml +5 -4
- package/dist/templates/modules/object-storage-s3/files/tests/api/storage.api.spec.ts +1 -1
- package/dist/templates/modules/rate-limit/files/tests/api/rate-limit.api.spec.ts +45 -20
- package/dist/templates/modules/sse/files/tests/api/events.api.spec.ts +1 -1
- package/dist/templates/todo/AGENTS.md +5 -1
- package/dist/templates/todo/README.md +6 -4
- package/dist/text-diff.d.ts +12 -0
- package/dist/text-diff.js +111 -0
- package/dist/update.d.ts +61 -3
- package/dist/update.js +152 -27
- package/package.json +2 -2
- package/dist/templates/modules/admin-dashboard/files/tests/api/exclusive.api.spec.ts +0 -75
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/exclusive.ts +0 -80
|
@@ -48,6 +48,7 @@ services:
|
|
|
48
48
|
- { action: sync, path: ./apps/web/src, target: /app/apps/web/src }
|
|
49
49
|
- { action: sync, path: ./apps/web/static, target: /app/apps/web/static }
|
|
50
50
|
- { action: sync+restart, path: ./apps/web/vite.config.ts, target: /app/apps/web/vite.config.ts }
|
|
51
|
+
- { action: sync+restart, path: ./apps/web/vite.extensions.ts, target: /app/apps/web/vite.extensions.ts }
|
|
51
52
|
- { action: rebuild, path: ./apps/web/package.json }
|
|
52
53
|
|
|
53
54
|
api:
|
|
@@ -121,7 +122,7 @@ services:
|
|
|
121
122
|
mem_limit: ${MINIO_MEM_LIMIT:-512m}
|
|
122
123
|
memswap_limit: ${MINIO_MEM_LIMIT:-512m}
|
|
123
124
|
profiles: [storage]
|
|
124
|
-
image: pgsty/silo:RELEASE.2026-09-16T00-00-00Z
|
|
125
|
+
image: pgsty/silo:RELEASE.2026-09-16T00-00-00Z@sha256:635197cb9f36d01bee221d34d1c7d7960f6a95c48b0b6c01d99cd13bdae51a46
|
|
125
126
|
command: server /data --console-address ":9001"
|
|
126
127
|
environment:
|
|
127
128
|
MINIO_ROOT_USER: podokit
|
|
@@ -134,15 +135,17 @@ services:
|
|
|
134
135
|
mem_limit: ${MINIO_INIT_MEM_LIMIT:-128m}
|
|
135
136
|
memswap_limit: ${MINIO_INIT_MEM_LIMIT:-128m}
|
|
136
137
|
profiles: [storage]
|
|
137
|
-
image: pgsty/mc:RELEASE.2026-09-16T00-00-00Z
|
|
138
|
+
image: pgsty/mc:RELEASE.2026-09-16T00-00-00Z@sha256:cfc83108c3abb371f8fb84d99c1fdc88f8c237e022409b0081fb7c0a3be634dd
|
|
138
139
|
depends_on:
|
|
139
140
|
minio: { condition: service_started }
|
|
141
|
+
# -e stops at the first failing command, so a bucket that cannot be
|
|
142
|
+
# created fails this service instead of leaving uploads to fail later.
|
|
140
143
|
entrypoint:
|
|
141
144
|
- sh
|
|
142
|
-
- -
|
|
145
|
+
- -ec
|
|
143
146
|
- |
|
|
144
147
|
until mc alias set loc http://minio:9000 podokit podokitsecret; do sleep 1; done
|
|
145
|
-
mc mb -
|
|
148
|
+
mc mb --ignore-existing loc/podokit
|
|
146
149
|
networks: [podokit]
|
|
147
150
|
|
|
148
151
|
networks:
|
|
@@ -16,9 +16,9 @@ Read the profile's `driver` first and say which one you are operating.
|
|
|
16
16
|
1. Run `{{packageExecutor}} @podosoft/podokit status`, `{{packageExecutor}} @podosoft/podokit diff`, and `{{packageExecutor}} @podosoft/podokit doctor`. Stop if managed-file conflicts or unsupported framework versions could make the image unreliable.
|
|
17
17
|
2. Read [references/profile.md](references/profile.md) before creating or changing a deployment profile.
|
|
18
18
|
3. Make sure the images exist for that tag. Read [references/release.md](references/release.md): tagging `vX.Y.Z` runs the release workflow, which builds and pushes both images and stops there. Never apply a release whose images were not published.
|
|
19
|
-
4. Run `{{packageExecutor}} @podosoft/podokit deploy doctor --profile <name>`. Resolve every
|
|
19
|
+
4. Run `{{packageExecutor}} @podosoft/podokit deploy doctor --profile <name>`. Resolve every `FAIL` line and show the user every `warn` line, without printing secret values.
|
|
20
20
|
5. Run `{{packageExecutor}} @podosoft/podokit deploy plan --profile <name> --release <stable-semver> --json`.
|
|
21
|
-
6. Show the user the driver, the target (context plus namespace and Helm release, or Docker context plus Compose project), the fingerprint result, the exact image digests, the current revision, the actions,
|
|
21
|
+
6. Show the user the driver, the target (context plus namespace and Helm release, or Docker context plus Compose project), the fingerprint result, the exact image digests, the current revision, the actions, every plan warning, and the plan hash.
|
|
22
22
|
7. Do not apply until the user explicitly approves that exact plan.
|
|
23
23
|
8. Apply with `{{packageExecutor}} @podosoft/podokit deploy apply --profile <name> --release <stable-semver> --confirm <plan-hash>`.
|
|
24
24
|
9. Run `{{packageExecutor}} @podosoft/podokit deploy status --profile <name> --json` and `{{packageExecutor}} @podosoft/podokit deploy verify --profile <name> --json`.
|
|
@@ -24,9 +24,16 @@ Review these fields before planning:
|
|
|
24
24
|
`stripPrefix` Middleware sends them to API without `/api`. It needs a
|
|
25
25
|
Traefik-served IngressClass, and `/api/auth` is rejected. `podo deploy init`
|
|
26
26
|
and `podo add` fill in the paths installed modules declare (file-upload:
|
|
27
|
-
`/api/files/uploads/`); keep them.
|
|
27
|
+
`/api/files/uploads/`); keep them. `edgeProxies` counts the reverse proxies in
|
|
28
|
+
front of the Ingress controller or NodePort (default 0); the web server then
|
|
29
|
+
trusts that many plus the controller in `X-Forwarded-For`. Never set
|
|
30
|
+
`XFF_DEPTH`, `ADDRESS_HEADER`, `TRUSTED_PROXY_HOPS`, or `TRUSTED_PROXY_HEADER`
|
|
31
|
+
in `runtimeConfig`; the profile refuses them.
|
|
28
32
|
- `workloads`: API/web replicas and resources plus an optional worker
|
|
29
|
-
- `dependencies`: PostgreSQL, Redis, and object storage mode, image, Secret name, and storage
|
|
33
|
+
- `dependencies`: PostgreSQL, Redis, and object storage mode, image, Secret name, and storage.
|
|
34
|
+
PostgreSQL also takes `maxConnections` (default 100) and `appPools` for pools the
|
|
35
|
+
application opens itself; the doctor and the plan check the connection budget
|
|
36
|
+
against them, so lower `DB_POOL_MAX`/`DB_AUTH_POOL_MAX` or replicas when they warn.
|
|
30
37
|
- `secrets`: API/optional web Secret names and required key names, plus image-pull Secret
|
|
31
38
|
- `runtimeConfig`: non-sensitive environment values only
|
|
32
39
|
- `verification`: HTTPS origin and checks with exact statuses and optional JSON fields
|
|
@@ -72,7 +79,9 @@ The fields that differ from the Kubernetes driver:
|
|
|
72
79
|
it, sends exact WebSocket paths to API, sends `apiPaths` prefixes to API with
|
|
73
80
|
`/api` removed, and sends every other request to web. Forwarding headers are trusted
|
|
74
81
|
only from the direct proxy CIDRs in `trustedProxyCidrs`; leave it empty without an
|
|
75
|
-
external proxy.
|
|
82
|
+
external proxy. `edgeProxies` counts the reverse proxies in front of the published
|
|
83
|
+
port (default 1); with the gateway it must match `trustedProxyCidrs`, which the
|
|
84
|
+
doctor checks. The bind address defaults to `127.0.0.1`, because the expected
|
|
76
85
|
front door is a trusted reverse proxy — binding every interface publishes the app
|
|
77
86
|
before TLS exists.
|
|
78
87
|
- `workloads`: replicas plus Compose `cpus` and `memory` limits
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: podokit-update
|
|
3
|
-
description: Use when the user wants to update a Bun-based project to a newer PodoKit v1 version, pull in template/module improvements, check local edits, or take ownership of a managed file. Covers podo status/diff/doctor/update/eject.
|
|
3
|
+
description: Use when the user wants to update a Bun-based project to a newer PodoKit v1 version, pull in template/module improvements, check local edits, or take ownership of a managed file. Covers podo status/diff/doctor/update/eject/restore.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Keep a PodoKit project up to date
|
|
@@ -11,15 +11,18 @@ its own files while keeping the user's changes.
|
|
|
11
11
|
```bash
|
|
12
12
|
podo status # version, modules, file tiers, and how many managed files were edited
|
|
13
13
|
podo diff # list the PodoKit-managed files you've edited (owned files ignored)
|
|
14
|
+
podo diff <path> # line diff of one file against the template (owned files too)
|
|
14
15
|
podo doctor # check framework versions vs. the supported ranges
|
|
15
16
|
podo update # dry-run: preview what a newer version would change (writes nothing)
|
|
16
17
|
podo update --apply # apply it — clean updates are written; your edits are 3-way merged
|
|
17
18
|
podo eject <path> # take ownership of a managed file so update skips it
|
|
19
|
+
podo restore <path> # rewrite a managed file from the template; re-manages an ejected file
|
|
18
20
|
```
|
|
19
21
|
|
|
20
22
|
Rules to respect:
|
|
21
23
|
- **owned** files (routes, your components, `AGENTS.md`, shadcn `ui/**`) are never
|
|
22
|
-
touched by `update`.
|
|
24
|
+
touched by `update`. When their template changes, the plan lists them as
|
|
25
|
+
`review`; compare with `podo diff <path>` and copy what the app needs.
|
|
23
26
|
- **assembled** files are recomputed from the module set — keep edits **outside**
|
|
24
27
|
the `// podokit:begin…end` fences.
|
|
25
28
|
- Run `podo update` (dry-run) first and read the plan before `--apply`. Commit or
|
|
@@ -18,11 +18,14 @@ TRUSTED_PROXY_HOPS=1
|
|
|
18
18
|
# response headers.
|
|
19
19
|
# BACKEND_TIMEOUT_MS=2000
|
|
20
20
|
# BACKEND_PROXY_TIMEOUT_MS=30000
|
|
21
|
-
CORS_ORIGIN
|
|
21
|
+
# podo dev sets CORS_ORIGIN and BETTER_AUTH_URL for web and api from
|
|
22
|
+
# .podokit/dev.json. These two serve compose.dev.yaml run without podo dev,
|
|
23
|
+
# such as from the devcontainer.
|
|
24
|
+
CORS_ORIGIN={{devCorsOrigin}}
|
|
22
25
|
REDIS_HOST=redis
|
|
23
26
|
REDIS_PORT=6379
|
|
24
27
|
BETTER_AUTH_SECRET=
|
|
25
|
-
BETTER_AUTH_URL=
|
|
28
|
+
BETTER_AUTH_URL={{devAuthUrl}}
|
|
26
29
|
ADMIN_EMAILS=admin@example.com
|
|
27
30
|
AUDIT_LOG_ENABLED=true
|
|
28
31
|
RATE_LIMIT_TTL=60
|
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
# ── Required (runtime) ───────────────────────────────────────────────
|
|
4
4
|
NODE_ENV=development
|
|
5
5
|
PORT=5002
|
|
6
|
+
# Address the API listens on (default 0.0.0.0); 127.0.0.1 keeps it off the network.
|
|
7
|
+
# HOST=0.0.0.0
|
|
6
8
|
|
|
7
9
|
# Canonical database connection. Set this for PostgreSQL or SQLite; POSTGRES_* remains a fallback.
|
|
8
10
|
# DATABASE_URL=postgres://podokit:podokit@localhost:5432/podokit
|
|
11
|
+
# With SQLite and no DATABASE_URL, the database file, relative to apps/api.
|
|
12
|
+
# SQLITE_PATH=./data/podokit.sqlite
|
|
9
13
|
POSTGRES_HOST=localhost
|
|
10
14
|
POSTGRES_PORT=5432
|
|
11
15
|
POSTGRES_USER=podokit
|
|
@@ -13,9 +13,48 @@ The suite runs against a live stack (web on `E2E_BASE_URL`, proxying `/api/*` to
|
|
|
13
13
|
the API). With the recommended containerized loop, start `podo dev watch` and run:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
E2E_BASE_URL=http://{{
|
|
16
|
+
E2E_BASE_URL=http://{{devHostname}} {{rootRun}} test:e2e
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
The default `http://localhost:5001` is for the alternative `{{rootRun}} dev`
|
|
20
20
|
host-process loop. Files ending in `*.api.spec.ts` run in the request-only `api`
|
|
21
21
|
project; `*.ui.spec.ts` run in the chromium `ui` project.
|
|
22
|
+
|
|
23
|
+
## Workers and exclusive tests
|
|
24
|
+
|
|
25
|
+
The suite runs on one worker unless `E2E_WORKERS` says otherwise. For the whole
|
|
26
|
+
suite, more workers finish sooner:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
E2E_WORKERS=2 {{rootRun}} test:e2e
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Set the count with `E2E_WORKERS`, not `--workers`: the global setup refuses more
|
|
33
|
+
than one worker when `E2E_WORKERS` is unset.
|
|
34
|
+
|
|
35
|
+
Every test shares one API, database, mail catcher, and SMS sink. A test that
|
|
36
|
+
changes state other tests rely on takes the `@exclusive` tag:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
test("maintenance mode holds visitors back", { tag: "@exclusive" }, async ({ page }) => {
|
|
40
|
+
// ...
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Tag a test when it changes a server-wide setting (a feature flag, the
|
|
45
|
+
authentication configuration such as SMTP, social providers, or sign-up
|
|
46
|
+
approval, the site settings, or a module's settings) or the profile of a seeded
|
|
47
|
+
account. Each project has an `<name>-exclusive` twin, such as `ui-exclusive`,
|
|
48
|
+
that runs only the tagged tests, one at a time. With more than one worker the
|
|
49
|
+
twins wait for every other test, so an exclusive test never runs alongside
|
|
50
|
+
another one. Never empty the shared mail catcher or SMS sink in a test: send to
|
|
51
|
+
an address or number of the test's own instead.
|
|
52
|
+
|
|
53
|
+
More than one worker suits the whole suite. When a run selects exclusive tests
|
|
54
|
+
with a file name, `--grep`, or `--project`, Playwright also runs every other test
|
|
55
|
+
first, because the twins depend on them, and a test that fails there marks the
|
|
56
|
+
exclusive tests as not run. Use the default single worker for a few files, and
|
|
57
|
+
do not combine `--no-deps` with `E2E_WORKERS` above 1.
|
|
58
|
+
|
|
59
|
+
Application-owned projects in `playwright.projects.cjs` get twins too. A project
|
|
60
|
+
that another project depends on is a setup project and keeps a single copy.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AppPaths } from "./paths";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Where this application's screens differ from the generated ones, for the
|
|
5
|
+
* managed e2e specs. PodoKit creates this file once and never overwrites it.
|
|
6
|
+
* Set only what differs; `paths.ts` documents each entry and its default. For
|
|
7
|
+
* example, an app whose first screen is a signed-in workspace without a
|
|
8
|
+
* heading, and which shows no public footer:
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* export const appPaths: Partial<AppPaths> = {
|
|
12
|
+
* homeReady: (page) => page.getByTestId("workspace"),
|
|
13
|
+
* indexable: [],
|
|
14
|
+
* footer: null,
|
|
15
|
+
* };
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export const appPaths: Partial<AppPaths> = {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Locator, Page } from "@playwright/test";
|
|
2
|
+
import { appPaths } from "./app-paths";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The application screens managed specs visit. The defaults describe the
|
|
6
|
+
* generated pages; `app-paths.ts`, which the application owns, overrides any
|
|
7
|
+
* of them, so a managed spec never needs editing for an app's own screens.
|
|
8
|
+
*/
|
|
9
|
+
export interface AppPaths {
|
|
10
|
+
/** The first screen a visitor opens. */
|
|
11
|
+
home: string;
|
|
12
|
+
/** Visible once the first screen has rendered. */
|
|
13
|
+
homeReady: (page: Page) => Locator;
|
|
14
|
+
/** Public pages search engines may index: they carry no `X-Robots-Tag`. */
|
|
15
|
+
indexable: string[];
|
|
16
|
+
/** A public page that renders the managed `SiteFooter`, or null when none does. */
|
|
17
|
+
footer: string | null;
|
|
18
|
+
/**
|
|
19
|
+
* A page that renders the managed `LanguageSwitch`, `ThemeToggle`, and
|
|
20
|
+
* `AccountMenu` for a signed-in user, or null when none does.
|
|
21
|
+
*/
|
|
22
|
+
accountMenu: string | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const defaults: AppPaths = {
|
|
26
|
+
home: "/",
|
|
27
|
+
homeReady: (page) => page.getByRole("heading", { level: 1 }),
|
|
28
|
+
indexable: ["/"],
|
|
29
|
+
footer: "/login",
|
|
30
|
+
accountMenu: "/",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const paths: AppPaths = { ...defaults, ...appPaths };
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
"private": true,
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test:e2e": "bunx playwright test",
|
|
7
|
-
"test:e2e:ui": "bunx playwright test --project=ui",
|
|
8
|
-
"test:e2e:api": "bunx playwright test --project=api",
|
|
7
|
+
"test:e2e:ui": "bunx playwright test --project=ui --project=ui-exclusive",
|
|
8
|
+
"test:e2e:api": "bunx playwright test --project=api --project=api-exclusive",
|
|
9
9
|
"test:e2e:report": "bunx playwright show-report"
|
|
10
10
|
},
|
|
11
11
|
"devDependencies": {
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { defineConfig, devices } from "@playwright/test";
|
|
2
|
-
import { loadPlaywrightProjects } from "./playwright.extensions";
|
|
2
|
+
import { e2eWorkers, loadPlaywrightProjects, withExclusivePhase } from "./playwright.extensions";
|
|
3
3
|
|
|
4
4
|
// The suite runs against a live stack: web on E2E_BASE_URL (proxying /api/* to
|
|
5
5
|
// the API). Start it first (see docs/testing.md) — locally via the dev harness,
|
|
6
6
|
// in CI via scripts/e2e-ci.mjs. Two projects: `api` (request-only, backend e2e)
|
|
7
7
|
// and `ui` (chromium, pages). Files are routed by suffix (*.api/*.ui.spec.ts).
|
|
8
|
+
// E2E_WORKERS sets the worker count (default 1); tests tagged @exclusive run in
|
|
9
|
+
// `<project>-exclusive` twins after the others (see playwright.extensions.ts).
|
|
8
10
|
const baseURL = process.env.E2E_BASE_URL ?? "http://localhost:5001";
|
|
11
|
+
const workers = e2eWorkers();
|
|
9
12
|
const coreProjects = [
|
|
10
13
|
{ name: "api", testMatch: /.*\.api\.spec\.ts/ },
|
|
11
14
|
{ name: "ui", testMatch: /.*\.ui\.spec\.ts/, use: { ...devices["Desktop Chrome"] } },
|
|
@@ -16,12 +19,13 @@ export default defineConfig({
|
|
|
16
19
|
fullyParallel: true,
|
|
17
20
|
forbidOnly: !!process.env.CI,
|
|
18
21
|
retries: process.env.CI ? 2 : 0,
|
|
19
|
-
workers
|
|
22
|
+
workers,
|
|
23
|
+
globalSetup: "./playwright.extensions.ts",
|
|
20
24
|
reporter: [["list"], ["html", { open: "never" }]],
|
|
21
25
|
use: {
|
|
22
26
|
baseURL,
|
|
23
27
|
trace: "on-first-retry",
|
|
24
28
|
screenshot: "only-on-failure",
|
|
25
29
|
},
|
|
26
|
-
projects: [...coreProjects, ...loadPlaywrightProjects(coreProjects)],
|
|
30
|
+
projects: withExclusivePhase([...coreProjects, ...loadPlaywrightProjects(coreProjects)], workers),
|
|
27
31
|
});
|
|
@@ -1,17 +1,33 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import type { PlaywrightTestConfig } from "@playwright/test";
|
|
4
|
+
import type { FullConfig, PlaywrightTestConfig } from "@playwright/test";
|
|
5
5
|
|
|
6
6
|
type Projects = NonNullable<PlaywrightTestConfig["projects"]>;
|
|
7
|
+
type Project = Projects[number];
|
|
7
8
|
|
|
8
9
|
const requireFromTests = createRequire(__filename);
|
|
9
10
|
|
|
11
|
+
// A test that changes state the other tests rely on, such as a server-wide
|
|
12
|
+
// setting or the profile of a seeded account, takes the `@exclusive` tag:
|
|
13
|
+
//
|
|
14
|
+
// test("maintenance mode holds visitors back", { tag: "@exclusive" }, async ({ page }) => { ... });
|
|
15
|
+
//
|
|
16
|
+
// Playwright matches project grep patterns against the title path followed by
|
|
17
|
+
// the test's tags, separated by spaces.
|
|
18
|
+
const TAGGED = /(?:^|\s)@exclusive(?=\s|$)/;
|
|
19
|
+
const UNTAGGED = /^(?![\s\S]*(?:^|\s)@exclusive(?=\s|$))/;
|
|
20
|
+
|
|
10
21
|
function projectName(project: unknown): string | null {
|
|
11
22
|
if (typeof project !== "object" || project === null || !("name" in project)) return null;
|
|
12
23
|
return typeof project.name === "string" && project.name.length > 0 ? project.name : null;
|
|
13
24
|
}
|
|
14
25
|
|
|
26
|
+
function patterns(value: Project["grepInvert"]): RegExp[] {
|
|
27
|
+
if (value === undefined) return [];
|
|
28
|
+
return Array.isArray(value) ? value : [value];
|
|
29
|
+
}
|
|
30
|
+
|
|
15
31
|
/**
|
|
16
32
|
* Merge application-owned Playwright projects into PodoKit's managed config.
|
|
17
33
|
* The optional CommonJS file stays owned so applications can add devices and
|
|
@@ -42,3 +58,74 @@ export function loadPlaywrightProjects(coreProjects: Projects): Projects {
|
|
|
42
58
|
|
|
43
59
|
return loaded as Projects;
|
|
44
60
|
}
|
|
61
|
+
|
|
62
|
+
/** The number of Playwright workers: `E2E_WORKERS`, or 1 when it is unset. */
|
|
63
|
+
export function e2eWorkers(value = process.env.E2E_WORKERS): number {
|
|
64
|
+
if (value === undefined || value.trim() === "") return 1;
|
|
65
|
+
const workers = Number(value);
|
|
66
|
+
if (!Number.isInteger(workers) || workers < 1) {
|
|
67
|
+
throw new Error(`E2E_WORKERS must be a whole number of at least 1, got "${value}".`);
|
|
68
|
+
}
|
|
69
|
+
return workers;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Splits each test project into one that skips tests tagged `@exclusive` and an
|
|
74
|
+
* `<name>-exclusive` twin that runs only them on one worker. Projects that
|
|
75
|
+
* another project depends on, or that tear one down, are setup and cleanup
|
|
76
|
+
* projects and stay as they are.
|
|
77
|
+
*
|
|
78
|
+
* With more than one worker the twins depend on every other test project, and
|
|
79
|
+
* each twin on the one before it, so an exclusive test never runs alongside
|
|
80
|
+
* another test. A dependency runs in full, though: a run that selects only
|
|
81
|
+
* exclusive tests, with a file, `--grep`, or `--project`, then also runs every
|
|
82
|
+
* other test first, and a test that fails there skips the exclusive tests. Use
|
|
83
|
+
* more than one worker for the whole suite. With one worker no dependency is
|
|
84
|
+
* needed, so filtered runs stay small.
|
|
85
|
+
*/
|
|
86
|
+
export function withExclusivePhase(projects: Projects, workers: number): Projects {
|
|
87
|
+
const support = new Set(
|
|
88
|
+
projects.flatMap((project) => [...(project.dependencies ?? []), ...(project.teardown ? [project.teardown] : [])]),
|
|
89
|
+
);
|
|
90
|
+
const tested = projects.filter((project) => !support.has(projectName(project) ?? ""));
|
|
91
|
+
const shared = new Map(
|
|
92
|
+
tested.map((project) => [project, { ...project, grepInvert: [...patterns(project.grepInvert), TAGGED] }]),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
let before = [...shared.values()].map((project) => projectName(project) ?? "");
|
|
96
|
+
const twins = tested.map((project) => {
|
|
97
|
+
const name = `${projectName(project) ?? ""}-exclusive`;
|
|
98
|
+
const twin: Project = {
|
|
99
|
+
...project,
|
|
100
|
+
name,
|
|
101
|
+
grepInvert: [...patterns(project.grepInvert), UNTAGGED],
|
|
102
|
+
workers: 1,
|
|
103
|
+
dependencies: workers > 1 ? [...(project.dependencies ?? []), ...before] : project.dependencies,
|
|
104
|
+
};
|
|
105
|
+
before = [name];
|
|
106
|
+
return twin;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const result = [...projects.map((project) => shared.get(project) ?? project), ...twins];
|
|
110
|
+
const names = new Set<string>();
|
|
111
|
+
for (const project of result) {
|
|
112
|
+
const name = projectName(project) ?? "";
|
|
113
|
+
if (names.has(name)) throw new Error(`Duplicate Playwright project name: ${name}`);
|
|
114
|
+
names.add(name);
|
|
115
|
+
}
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Global setup: refuses more workers than `E2E_WORKERS` allows for. `--workers`
|
|
121
|
+
* changes the count after the projects were built, so with it the exclusive
|
|
122
|
+
* twins would run alongside the other tests.
|
|
123
|
+
*/
|
|
124
|
+
export default function checkWorkers(config: FullConfig): void {
|
|
125
|
+
if (config.workers > 1 && e2eWorkers() === 1) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`Set the worker count with E2E_WORKERS=${config.workers} instead of --workers. ` +
|
|
128
|
+
"Tests tagged @exclusive run after the others only when E2E_WORKERS is above 1.",
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { expect, test } from "@playwright/test";
|
|
2
|
+
import { paths } from "../helpers/paths";
|
|
2
3
|
|
|
3
|
-
// Module-agnostic: the app
|
|
4
|
+
// Module-agnostic: the app's first screen responds (a page or a redirect to it)
|
|
5
|
+
// and renders. Its locator comes from tests/helpers/app-paths.ts.
|
|
4
6
|
test("app is reachable @smoke", async ({ page }) => {
|
|
5
|
-
const res = await page.goto(
|
|
7
|
+
const res = await page.goto(paths.home);
|
|
6
8
|
expect(res?.status()).toBeLessThan(400);
|
|
7
|
-
await expect(
|
|
8
|
-
await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
|
|
9
|
+
await expect(paths.homeReady(page)).toBeVisible();
|
|
9
10
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ApiError } from "@podosoft/podokit-api-client";
|
|
2
2
|
import type { RequestEvent } from "@sveltejs/kit";
|
|
3
|
+
import type { Handle } from "@sveltejs/kit/hooks";
|
|
3
4
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
5
|
import { BackendUnavailableError } from "#lib/server/api.js";
|
|
5
6
|
import { handle } from "./hooks.server";
|
|
@@ -14,12 +15,18 @@ const client = vi.hoisted(() => ({
|
|
|
14
15
|
getSession: vi.fn<() => Promise<SessionResult>>(),
|
|
15
16
|
}));
|
|
16
17
|
|
|
18
|
+
// Stands in for the application's own hook in the app-owned app-hooks.ts.
|
|
19
|
+
const appHook = vi.hoisted(() => ({
|
|
20
|
+
handle: vi.fn<Handle>(({ event, resolve }) => resolve(event)),
|
|
21
|
+
}));
|
|
22
|
+
vi.mock("#lib/server/app-hooks.js", () => ({ appHandle: appHook.handle }));
|
|
23
|
+
|
|
17
24
|
vi.mock("#lib/server/api.js", async (importOriginal) => ({
|
|
18
25
|
...(await importOriginal<typeof import("#lib/server/api.js")>()),
|
|
19
26
|
serverApiClient: () => ({ get: client.get, auth: { getSession: client.getSession } }),
|
|
20
27
|
}));
|
|
21
28
|
|
|
22
|
-
const resolve = vi.fn(async (): Promise<Response> => new Response("ok"));
|
|
29
|
+
const resolve = vi.fn(async (_event: RequestEvent): Promise<Response> => new Response("ok"));
|
|
23
30
|
|
|
24
31
|
function requestEvent(pathname: string, cookies: Record<string, string> = {}): RequestEvent {
|
|
25
32
|
return {
|
|
@@ -40,6 +47,7 @@ describe("handle", () => {
|
|
|
40
47
|
client.get.mockReset();
|
|
41
48
|
client.getSession.mockReset();
|
|
42
49
|
resolve.mockClear();
|
|
50
|
+
appHook.handle.mockClear();
|
|
43
51
|
});
|
|
44
52
|
|
|
45
53
|
it("requests the session without waiting for site settings", async () => {
|
|
@@ -144,4 +152,32 @@ describe("handle", () => {
|
|
|
144
152
|
expect(client.getSession).not.toHaveBeenCalled();
|
|
145
153
|
expect(resolve).toHaveBeenCalledTimes(1);
|
|
146
154
|
});
|
|
155
|
+
|
|
156
|
+
it("runs the application hook with the loaded runtime context", async () => {
|
|
157
|
+
client.get.mockResolvedValue({ locale: null });
|
|
158
|
+
client.getSession.mockResolvedValue(signedIn);
|
|
159
|
+
let seenUser: unknown;
|
|
160
|
+
appHook.handle.mockImplementationOnce(({ event, resolve: next }) => {
|
|
161
|
+
seenUser = event.locals.user;
|
|
162
|
+
event.locals.user = { id: "local", email: "local@localhost", name: "Local", role: "admin" };
|
|
163
|
+
return next(event);
|
|
164
|
+
});
|
|
165
|
+
const event = requestEvent("/account");
|
|
166
|
+
|
|
167
|
+
const response = await handle({ event, resolve });
|
|
168
|
+
|
|
169
|
+
expect(seenUser).toEqual({ id: "user-1" });
|
|
170
|
+
expect(resolve).toHaveBeenCalledTimes(1);
|
|
171
|
+
expect(resolve.mock.calls[0]?.[0].locals.user).toMatchObject({ id: "local", role: "admin" });
|
|
172
|
+
expect(response.headers.get("x-robots-tag")).toBe("noindex, nofollow");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("lets the application hook answer a request itself", async () => {
|
|
176
|
+
appHook.handle.mockImplementationOnce(() => new Response("teapot", { status: 418 }));
|
|
177
|
+
|
|
178
|
+
const response = await handle({ event: requestEvent("/api/site/settings"), resolve });
|
|
179
|
+
|
|
180
|
+
expect(response.status).toBe(418);
|
|
181
|
+
expect(resolve).not.toHaveBeenCalled();
|
|
182
|
+
});
|
|
147
183
|
});
|
|
@@ -3,6 +3,7 @@ import type { Handle } from "@sveltejs/kit/hooks";
|
|
|
3
3
|
import { building } from "$app/env";
|
|
4
4
|
import { routes } from "$app/manifest";
|
|
5
5
|
import { serverApiClient } from "#lib/server/api.js";
|
|
6
|
+
import { appHandle } from "#lib/server/app-hooks.js";
|
|
6
7
|
import { resolveLocale } from "#lib/i18n/messages.js";
|
|
7
8
|
import { applySearchIndexingHeaders } from "#lib/server/search-indexing.js";
|
|
8
9
|
import { isPublicPath } from "#lib/server/guards.js";
|
|
@@ -91,12 +92,20 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|
|
91
92
|
} else {
|
|
92
93
|
setEmptyRuntimeContext(event);
|
|
93
94
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
// The application's own hook from the app-owned app-hooks.ts runs next, with
|
|
96
|
+
// the runtime context loaded. Its page transform runs before the language one.
|
|
97
|
+
const response = await appHandle({
|
|
98
|
+
event,
|
|
99
|
+
resolve: (appEvent, options) =>
|
|
100
|
+
resolve(appEvent, {
|
|
101
|
+
...options,
|
|
102
|
+
transformPageChunk: async ({ html, done }) => {
|
|
103
|
+
const transformed = options?.transformPageChunk
|
|
104
|
+
? ((await options.transformPageChunk({ html, done })) ?? "")
|
|
105
|
+
: html;
|
|
106
|
+
return transformed.replace(/(<html\b[^>]*?\slang=)(["'])[^"']*\2/i, `$1"${event.locals.locale}"`);
|
|
107
|
+
},
|
|
108
|
+
}),
|
|
100
109
|
});
|
|
101
110
|
return applySearchIndexingHeaders(response, event.url.pathname);
|
|
102
111
|
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { Messages } from "#lib/i18n/messages.js";
|
|
3
|
+
import { appFlagCards } from "./app-flag-settings";
|
|
4
|
+
|
|
5
|
+
// Stands in for the application's own list in the app-owned app-flags.ts.
|
|
6
|
+
vi.mock("./app-flags", () => ({
|
|
7
|
+
APP_FLAG_SETTINGS: [
|
|
8
|
+
{ flag: "reportsEnabled", label: () => "Reports", description: () => "Show the reports page." },
|
|
9
|
+
{ flag: "betaSearch", label: () => "Beta search" },
|
|
10
|
+
],
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
const t = {} as Messages;
|
|
14
|
+
|
|
15
|
+
describe("application flag settings", () => {
|
|
16
|
+
it("shows each listed flag with the value the API reports", () => {
|
|
17
|
+
expect(appFlagCards(t, { appFlags: { reportsEnabled: true, betaSearch: false, hidden: true } })).toEqual([
|
|
18
|
+
{ flag: "reportsEnabled", name: "Reports", description: "Show the reports page.", available: true, on: true },
|
|
19
|
+
{ flag: "betaSearch", name: "Beta search", description: "", available: true, on: false },
|
|
20
|
+
]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("marks a flag the API does not report as unavailable", () => {
|
|
24
|
+
expect(appFlagCards(t, { appFlags: { reportsEnabled: false } })[1]).toMatchObject({
|
|
25
|
+
flag: "betaSearch",
|
|
26
|
+
available: false,
|
|
27
|
+
on: false,
|
|
28
|
+
});
|
|
29
|
+
expect(appFlagCards(t, undefined).every((card) => !card.available)).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
});
|
package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-flag-settings.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Capabilities } from "@podosoft/podokit-api-client";
|
|
2
|
+
import type { Messages } from "#lib/i18n/messages.js";
|
|
3
|
+
import { APP_FLAG_SETTINGS } from "./app-flags";
|
|
4
|
+
import type { AppFlagSetting } from "./registry.svelte.js";
|
|
5
|
+
|
|
6
|
+
export type AppFlagCard = {
|
|
7
|
+
flag: string;
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
/** False when the API does not report the flag, for example a name missing from APP_FLAG_DEFAULTS. */
|
|
11
|
+
available: boolean;
|
|
12
|
+
on: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** One card per listed flag, with the value `GET /account/capabilities` reports in `appFlags`. */
|
|
16
|
+
export function appFlagCards(
|
|
17
|
+
t: Messages,
|
|
18
|
+
capabilities: Pick<Capabilities, "appFlags"> | undefined,
|
|
19
|
+
settings: readonly AppFlagSetting[] = APP_FLAG_SETTINGS,
|
|
20
|
+
): AppFlagCard[] {
|
|
21
|
+
return settings.map(({ flag, label, description }) => {
|
|
22
|
+
const value = capabilities?.appFlags?.[flag];
|
|
23
|
+
return {
|
|
24
|
+
flag,
|
|
25
|
+
name: label(t),
|
|
26
|
+
description: description?.(t) ?? "",
|
|
27
|
+
available: typeof value === "boolean",
|
|
28
|
+
on: value === true,
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AppFlagSetting } from "./registry.svelte.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Application flags the admin Settings page shows, one switch each, on an
|
|
5
|
+
* Application tab after PodoKit's tabs. `flag` is a key of `APP_FLAG_DEFAULTS`
|
|
6
|
+
* in the API's owned `apps/api/src/settings/app-flags.ts`; the label and
|
|
7
|
+
* description come from the app-owned `i18n/catalogs/app/*.json`, for example
|
|
8
|
+
* `{ flag: "reportsEnabled", label: (t) => t.reports.flag, description: (t) => t.reports.flagDescription }`.
|
|
9
|
+
* The tab is hidden while the list is empty. PodoKit creates this file once and
|
|
10
|
+
* never overwrites it.
|
|
11
|
+
*/
|
|
12
|
+
export const APP_FLAG_SETTINGS: readonly AppFlagSetting[] = [];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AdminNavEntry } from "./registry.svelte.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Application-owned admin sidebar entries. The sidebar shows them after PodoKit's
|
|
5
|
+
* own pages and before module entries and Settings, for example
|
|
6
|
+
* `{ href: "/admin/reports", label: (t) => t.reports.nav, icon: ChartColumnIcon, adminOnly: true }`
|
|
7
|
+
* with the label in the app-owned `i18n/catalogs/app/*.json` and the icon from
|
|
8
|
+
* `@lucide/svelte/icons/*`. `adminOnly` hides an entry from other roles.
|
|
9
|
+
* PodoKit creates this file once and never overwrites it.
|
|
10
|
+
*/
|
|
11
|
+
export const APP_ADMIN_NAV: readonly AdminNavEntry[] = [];
|