@podosoft/podokit 1.5.0 → 1.6.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 +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 +60 -2
- package/dist/deploy-schema.js +141 -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 +30 -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 +7 -2
- 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 +121 -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 +72 -4
- package/dist/templates/fullstack/apps/api/src/core/api-contract.ts +78 -6
- 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 +13 -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 +50 -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/helpers/seeded-admin.ts +15 -0
- package/dist/templates/fullstack/tests/package.json +3 -3
- package/dist/templates/fullstack/tests/playwright.config.ts +10 -4
- package/dist/templates/fullstack/tests/playwright.extensions.ts +149 -1
- package/dist/templates/fullstack/tests/run-e2e.ts +28 -0
- 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 +53 -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/components/theme-toggle.svelte +1 -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/hydration.ts +13 -4
- 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 +14 -6
- 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 +35 -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 +107 -1
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.ts +113 -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 +174 -1
- package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.ts +150 -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 +3 -6
- 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 +3 -6
- 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 +3 -6
- 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
|
@@ -358,6 +358,23 @@ export function proxyTimeoutFor(
|
|
|
358
358
|
return entry ? entry.timeoutMs : backendProxyTimeoutMs();
|
|
359
359
|
}
|
|
360
360
|
|
|
361
|
+
/**
|
|
362
|
+
* Bun's fetch fails a request whose connection sends nothing for its idle
|
|
363
|
+
* timeout, 300 seconds unless BUN_CONFIG_HTTP_IDLE_TIMEOUT sets another, even
|
|
364
|
+
* while the proxy still waits for response headers.
|
|
365
|
+
*/
|
|
366
|
+
const FETCH_IDLE_TIMEOUT_MS = 300_000;
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Bun's fetch options for a request that waits up to timeoutMs for response
|
|
370
|
+
* headers. A wait without a deadline, or one Bun's idle timeout would cut
|
|
371
|
+
* first, turns that timeout off, so only the deadline, the health checks, and
|
|
372
|
+
* the browser end it. Shorter waits keep Bun's default.
|
|
373
|
+
*/
|
|
374
|
+
export function upstreamFetchOptions(timeoutMs: number | null): { timeout?: false } {
|
|
375
|
+
return timeoutMs === null || timeoutMs >= FETCH_IDLE_TIMEOUT_MS ? { timeout: false } : {};
|
|
376
|
+
}
|
|
377
|
+
|
|
361
378
|
/** Bun closes silent connections on a 4-second tick, up to 4 seconds before their idle timeout. */
|
|
362
379
|
const IDLE_TICK_SECONDS = 4;
|
|
363
380
|
/** The longest idle timeout Bun accepts; 0 turns it off. */
|
|
@@ -509,6 +526,40 @@ export function normalizeClientIp(address: string | undefined): string | undefin
|
|
|
509
526
|
return address;
|
|
510
527
|
}
|
|
511
528
|
|
|
529
|
+
let publicOriginsSource: string | undefined;
|
|
530
|
+
let publicOrigins: ReadonlySet<string> = new Set();
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* The public web origins listed in CORS_ORIGIN. Wildcard patterns name no single
|
|
534
|
+
* origin and are skipped, as are entries that are not absolute URLs.
|
|
535
|
+
*/
|
|
536
|
+
export function configuredPublicOrigins(value = process.env.CORS_ORIGIN): ReadonlySet<string> {
|
|
537
|
+
if (value !== publicOriginsSource) {
|
|
538
|
+
publicOriginsSource = value;
|
|
539
|
+
publicOrigins = new Set(
|
|
540
|
+
(value ?? "")
|
|
541
|
+
.split(",")
|
|
542
|
+
.map((entry) => entry.trim())
|
|
543
|
+
.filter((entry) => !entry.includes("*") && URL.canParse(entry))
|
|
544
|
+
.map((entry) => new URL(entry).origin),
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
return publicOrigins;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Tell the API which public origin a request came in on. SvelteKit resolves the
|
|
552
|
+
* request URL from the Host header, or from the gateway's forwarded headers that
|
|
553
|
+
* HOST_HEADER and PROTOCOL_HEADER name, so the value is sent only when that origin
|
|
554
|
+
* is listed in CORS_ORIGIN. The API can then build links for the origin a browser
|
|
555
|
+
* used without trusting a header a caller wrote.
|
|
556
|
+
*/
|
|
557
|
+
export function setForwardedOrigin(headers: Headers, url: URL): void {
|
|
558
|
+
if (!configuredPublicOrigins().has(url.origin)) return;
|
|
559
|
+
headers.set("x-forwarded-host", url.host);
|
|
560
|
+
headers.set("x-forwarded-proto", url.protocol.slice(0, -1));
|
|
561
|
+
}
|
|
562
|
+
|
|
512
563
|
export function resolveClientIp(getClientAddress: () => string): string | undefined {
|
|
513
564
|
try {
|
|
514
565
|
return normalizeClientIp(getClientAddress());
|
|
@@ -531,8 +582,10 @@ export async function proxyRequest(
|
|
|
531
582
|
// Forward the resolved client IP so the API (better-auth) can record it.
|
|
532
583
|
const forwardedIp = normalizeClientIp(clientAddress);
|
|
533
584
|
if (forwardedIp) headers.set("x-forwarded-for", forwardedIp);
|
|
585
|
+
const requestUrl = new URL(request.url);
|
|
586
|
+
setForwardedOrigin(headers, requestUrl);
|
|
534
587
|
|
|
535
|
-
const { pathname } =
|
|
588
|
+
const { pathname } = requestUrl;
|
|
536
589
|
if (!backendReachable()) {
|
|
537
590
|
request.body?.cancel().catch(() => undefined);
|
|
538
591
|
return backendFailure(pathname, 503);
|
|
@@ -578,6 +631,7 @@ export async function proxyRequest(
|
|
|
578
631
|
// closed EventSource or aborted request keeps its API socket open until Bun's
|
|
579
632
|
// fetch pool is exhausted and the web process stops serving requests.
|
|
580
633
|
signal: AbortSignal.any([request.signal, waiting.signal]),
|
|
634
|
+
...upstreamFetchOptions(timeoutMs),
|
|
581
635
|
});
|
|
582
636
|
} catch (cause: unknown) {
|
|
583
637
|
// Nobody waits for an answer to a request the browser abandoned.
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* headers before it answers 504 BACKEND_TIMEOUT; null waits as long as the API
|
|
11
11
|
* takes. Either way the proxy answers 503 BACKEND_UNAVAILABLE once the API's
|
|
12
12
|
* GET /health stops answering, and the deadline ends when the response headers
|
|
13
|
-
* arrive.
|
|
13
|
+
* arrive. For null or 300 seconds and more, the proxy also turns off Bun's fetch
|
|
14
|
+
* idle timeout, which would otherwise end a silent wait after 300 seconds.
|
|
14
15
|
*
|
|
15
16
|
* Bun closes a silent connection up to 4 seconds before its idle timeout, so a
|
|
16
17
|
* longer wait also needs the API's API_IDLE_TIMEOUT and the web server's
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { webSocketProxy } from "./vite.config.ts";
|
|
3
|
+
|
|
4
|
+
describe("dev-server WebSocket proxy", () => {
|
|
5
|
+
it("sends each exact path in .podokit/dev.json to the API", () => {
|
|
6
|
+
const proxy = webSocketProxy(
|
|
7
|
+
JSON.stringify({ hostname: "app.localhost", webSocketPaths: ["/events/ws", "/agent.v1/ws"] }),
|
|
8
|
+
"http://api:5002",
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
expect(proxy).toEqual({
|
|
12
|
+
"^/events/ws(?:\\?.*)?$": { target: "http://api:5002", ws: true, xfwd: true },
|
|
13
|
+
"^/agent\\.v1/ws(?:\\?.*)?$": { target: "http://api:5002", ws: true, xfwd: true },
|
|
14
|
+
});
|
|
15
|
+
const [events] = Object.keys(proxy).map((key) => new RegExp(key));
|
|
16
|
+
expect(events?.test("/events/ws")).toBe(true);
|
|
17
|
+
expect(events?.test("/events/ws?token=1")).toBe(true);
|
|
18
|
+
expect(events?.test("/events/ws/other")).toBe(false);
|
|
19
|
+
expect(events?.test("/events/wsx")).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("proxies nothing without the file or its paths", () => {
|
|
23
|
+
expect(webSocketProxy(null)).toEqual({});
|
|
24
|
+
expect(webSocketProxy(JSON.stringify({ hostname: "app.localhost" }))).toEqual({});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("rejects a path the gateway would reject", () => {
|
|
28
|
+
expect(() => webSocketProxy(JSON.stringify({ webSocketPaths: ["events/ws"] }))).toThrow(
|
|
29
|
+
'invalid path: "events/ws"',
|
|
30
|
+
);
|
|
31
|
+
expect(() => webSocketProxy(JSON.stringify({ webSocketPaths: "/events/ws" }))).toThrow("must be an array");
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
1
2
|
import adapter from "@sveltejs/adapter-bun";
|
|
2
3
|
import { sveltekit } from "@sveltejs/kit/vite";
|
|
3
4
|
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
|
4
5
|
import tailwindcss from "@tailwindcss/vite";
|
|
5
|
-
import { defineConfig } from "vite";
|
|
6
|
+
import { defineConfig, mergeConfig, type ProxyOptions } from "vite";
|
|
7
|
+
import { appSvelteKitConfig, appViteConfig } from "./vite.extensions.ts";
|
|
6
8
|
|
|
7
9
|
// Inside the dev container, bind to all interfaces and accept local gateway or
|
|
8
10
|
// HTTPS tunnel hostnames. Vite derives HMR's protocol, host, and port from the
|
|
@@ -10,16 +12,56 @@ import { defineConfig } from "vite";
|
|
|
10
12
|
const inDocker = process.env.VITE_DOCKER === "1";
|
|
11
13
|
const publicOrigin = process.env.PODOKIT_BUILD_ORIGIN;
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Dev-server proxies for the exact WebSocket paths in `.podokit/dev.json`. A
|
|
17
|
+
* SvelteKit route cannot answer an upgrade, so `bun run dev` sends these paths to
|
|
18
|
+
* the API the way the `podo dev` gateway and a deployment's edge do. The API must
|
|
19
|
+
* authenticate each upgrade itself.
|
|
20
|
+
*/
|
|
21
|
+
export function webSocketProxy(
|
|
22
|
+
devConfig: string | null,
|
|
23
|
+
target = process.env.BACKEND_INTERNAL_URL ?? "http://localhost:5002",
|
|
24
|
+
): Record<string, ProxyOptions> {
|
|
25
|
+
if (devConfig === null) return {};
|
|
26
|
+
const { webSocketPaths = [] } = JSON.parse(devConfig) as { webSocketPaths?: unknown };
|
|
27
|
+
if (!Array.isArray(webSocketPaths)) throw new Error(".podokit/dev.json webSocketPaths must be an array of paths");
|
|
28
|
+
return Object.fromEntries(
|
|
29
|
+
webSocketPaths.map((path: unknown) => {
|
|
30
|
+
if (typeof path !== "string" || !/^\/[A-Za-z0-9._~!$&'()+,;=:@/-]+$/.test(path)) {
|
|
31
|
+
throw new Error(`.podokit/dev.json webSocketPaths has an invalid path: ${JSON.stringify(path)}`);
|
|
32
|
+
}
|
|
33
|
+
const exact = `^${path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:\\?.*)?$`;
|
|
34
|
+
return [exact, { target, ws: true, xfwd: true }];
|
|
20
35
|
}),
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readDevConfig(): string | null {
|
|
40
|
+
try {
|
|
41
|
+
return readFileSync(new URL("../../.podokit/dev.json", import.meta.url), "utf8");
|
|
42
|
+
} catch (cause) {
|
|
43
|
+
if ((cause as { code?: unknown }).code === "ENOENT") return null;
|
|
44
|
+
throw cause;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default defineConfig((env) =>
|
|
49
|
+
mergeConfig(
|
|
50
|
+
{
|
|
51
|
+
plugins: [
|
|
52
|
+
tailwindcss(),
|
|
53
|
+
sveltekit({
|
|
54
|
+
adapter: adapter(),
|
|
55
|
+
preprocess: vitePreprocess(),
|
|
56
|
+
...appSvelteKitConfig,
|
|
57
|
+
...(publicOrigin ? { paths: { ...appSvelteKitConfig.paths, origin: publicOrigin } } : {}),
|
|
58
|
+
}),
|
|
59
|
+
],
|
|
60
|
+
server: {
|
|
61
|
+
...(inDocker ? { host: true, port: 5001, allowedHosts: true } : {}),
|
|
62
|
+
...(env.command === "serve" ? { proxy: webSocketProxy(readDevConfig()) } : {}),
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
typeof appViteConfig === "function" ? appViteConfig(env) : appViteConfig,
|
|
66
|
+
),
|
|
67
|
+
);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Config as SvelteKitConfig } from "@sveltejs/kit/vite";
|
|
2
|
+
import type { UserConfig, UserConfigFnObject } from "vite";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Application-owned additions to the managed vite.config.ts, combined with Vite's
|
|
6
|
+
* `mergeConfig`: lists such as `plugins` are appended and objects are merged key
|
|
7
|
+
* by key. Use it for dev-server warmup, dependency optimization, `define`
|
|
8
|
+
* constants, extra plugins, or extra allowed hosts instead of editing
|
|
9
|
+
* vite.config.ts. A function receives Vite's `{ command, mode }`. WebSocket paths
|
|
10
|
+
* listed in `.podokit/dev.json` already reach the API through the dev server.
|
|
11
|
+
* PodoKit creates this file once and never overwrites it.
|
|
12
|
+
*/
|
|
13
|
+
export const appViteConfig: UserConfig | UserConfigFnObject = {
|
|
14
|
+
// Transform the pages most sessions open first as soon as the dev server starts:
|
|
15
|
+
// server: { warmup: { clientFiles: ["./src/routes/+layout.svelte", "./src/routes/+page.svelte"] } },
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Application-owned SvelteKit options, such as `alias`, `csrf`, or `prerender`.
|
|
20
|
+
* PodoKit sets `adapter` and `preprocess`, and `paths.origin` from
|
|
21
|
+
* PODOKIT_BUILD_ORIGIN at build time.
|
|
22
|
+
*/
|
|
23
|
+
export const appSvelteKitConfig: SvelteKitConfig = {};
|
|
@@ -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,17 @@ 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, each sized by a `runtimeConfig` key (`sizeKey`) or a
|
|
36
|
+
fixed `size` when the code sets it; the doctor and the plan check the connection budget
|
|
37
|
+
against them, so lower `DB_POOL_MAX`/`DB_AUTH_POOL_MAX` or replicas when they warn.
|
|
30
38
|
- `secrets`: API/optional web Secret names and required key names, plus image-pull Secret
|
|
31
39
|
- `runtimeConfig`: non-sensitive environment values only
|
|
32
40
|
- `verification`: HTTPS origin and checks with exact statuses and optional JSON fields
|
|
@@ -72,7 +80,9 @@ The fields that differ from the Kubernetes driver:
|
|
|
72
80
|
it, sends exact WebSocket paths to API, sends `apiPaths` prefixes to API with
|
|
73
81
|
`/api` removed, and sends every other request to web. Forwarding headers are trusted
|
|
74
82
|
only from the direct proxy CIDRs in `trustedProxyCidrs`; leave it empty without an
|
|
75
|
-
external proxy.
|
|
83
|
+
external proxy. `edgeProxies` counts the reverse proxies in front of the published
|
|
84
|
+
port (default 1); with the gateway it must match `trustedProxyCidrs`, which the
|
|
85
|
+
doctor checks. The bind address defaults to `127.0.0.1`, because the expected
|
|
76
86
|
front door is a trusted reverse proxy — binding every interface publishes the app
|
|
77
87
|
before TLS exists.
|
|
78
88
|
- `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,58 @@ 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. Never empty the shared mail
|
|
49
|
+
catcher or SMS sink in a test: send to an address or number of the test's own
|
|
50
|
+
instead.
|
|
51
|
+
|
|
52
|
+
The `test:e2e` scripts run `run-e2e.ts`, which hands its arguments (a file,
|
|
53
|
+
`--grep`, `--project`) to Playwright. With more than one worker it runs
|
|
54
|
+
Playwright twice: first every test not tagged `@exclusive` on those workers,
|
|
55
|
+
then the tagged tests on one worker, even when a test failed in the first run.
|
|
56
|
+
The suite fails when either run fails. The second run reports to
|
|
57
|
+
`playwright-report/exclusive` and `test-results/exclusive`:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
bunx playwright show-report playwright-report/exclusive
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
A plain `bunx playwright test` with `E2E_WORKERS` above 1 makes one run in which
|
|
64
|
+
the twins depend on every other test: a failing test there leaves the exclusive
|
|
65
|
+
tests not run, and selecting an exclusive test runs every other test first. Use
|
|
66
|
+
`{{rootRun}} test:e2e` on more workers, and do not combine `--no-deps` with
|
|
67
|
+
`E2E_WORKERS` above 1.
|
|
68
|
+
|
|
69
|
+
Application-owned projects in `playwright.projects.cjs` get twins too. A project
|
|
70
|
+
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 };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The administrator the admin-dashboard Playwright setup seeds, with the address
|
|
6
|
+
* and password `accounts.ts` reads from `E2E_*` variables and the application's
|
|
7
|
+
* `app-accounts.ts`, or null in an app without that module. Specs of modules
|
|
8
|
+
* that do not require admin-dashboard sign in with this instead of importing
|
|
9
|
+
* `accounts.ts`, which only admin-dashboard installs.
|
|
10
|
+
*/
|
|
11
|
+
export async function seededAdmin(): Promise<{ email: string; password: string } | null> {
|
|
12
|
+
if (!existsSync(join(__dirname, "accounts.ts"))) return null;
|
|
13
|
+
const { ADMIN } = await import("./accounts");
|
|
14
|
+
return { email: ADMIN.email, password: ADMIN.password };
|
|
15
|
+
}
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
"version": "0.0.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"scripts": {
|
|
6
|
-
"test:e2e": "
|
|
7
|
-
"test:e2e:ui": "
|
|
8
|
-
"test:e2e:api": "
|
|
6
|
+
"test:e2e": "bun run-e2e.ts",
|
|
7
|
+
"test:e2e:ui": "bun run-e2e.ts --project=ui --project=ui-exclusive",
|
|
8
|
+
"test:e2e:api": "bun run-e2e.ts --project=api --project=api-exclusive",
|
|
9
9
|
"test:e2e:report": "bunx playwright show-report"
|
|
10
10
|
},
|
|
11
11
|
"devDependencies": {
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { defineConfig, devices } from "@playwright/test";
|
|
2
|
-
import { loadPlaywrightProjects } from "./playwright.extensions";
|
|
2
|
+
import { e2eRunSettings, 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, in a second run when run-e2e.ts
|
|
10
|
+
// starts the suite on more workers (see playwright.extensions.ts).
|
|
8
11
|
const baseURL = process.env.E2E_BASE_URL ?? "http://localhost:5001";
|
|
12
|
+
const run = e2eRunSettings();
|
|
9
13
|
const coreProjects = [
|
|
10
14
|
{ name: "api", testMatch: /.*\.api\.spec\.ts/ },
|
|
11
15
|
{ name: "ui", testMatch: /.*\.ui\.spec\.ts/, use: { ...devices["Desktop Chrome"] } },
|
|
@@ -16,12 +20,14 @@ export default defineConfig({
|
|
|
16
20
|
fullyParallel: true,
|
|
17
21
|
forbidOnly: !!process.env.CI,
|
|
18
22
|
retries: process.env.CI ? 2 : 0,
|
|
19
|
-
workers:
|
|
20
|
-
|
|
23
|
+
workers: run.workers,
|
|
24
|
+
outputDir: run.outputDir,
|
|
25
|
+
globalSetup: "./playwright.extensions.ts",
|
|
26
|
+
reporter: [["list"], ["html", { open: "never", outputFolder: run.reportFolder }]],
|
|
21
27
|
use: {
|
|
22
28
|
baseURL,
|
|
23
29
|
trace: "on-first-retry",
|
|
24
30
|
screenshot: "only-on-failure",
|
|
25
31
|
},
|
|
26
|
-
projects: [...coreProjects, ...loadPlaywrightProjects(coreProjects)],
|
|
32
|
+
projects: withExclusivePhase([...coreProjects, ...loadPlaywrightProjects(coreProjects)], run.workers, run.phase),
|
|
27
33
|
});
|